고차 함수와 콜백
함수를 인자로 받거나 반환하는 고차 함수를 익히고 동기·비동기 콜백과 콜백 헬이 생기는 흐름을 이해합니다.
앞 글 객체 지향과 함수형 프로그래밍에서는 map(), filter(), reduce() 같은 고차 함수로 데이터를 변환해 봤습니다. 이번에는 고차 함수를 직접 만들어 보고, 다른 함수에 넘겨져 나중에 호출되는 콜백 함수(callback function)가 언제 실행되는지 살펴봅니다.
자바스크립트에서 함수는 일급 객체(first-class object)라서, 숫자나 문자열처럼 변수에 담고 다른 함수의 인수로 넘기거나 결과로 돌려줄 수 있습니다. 콜백 함수는 이렇게 다른 함수에 인수로 넘겨져, 넘겨받은 API가 정한 시점에 호출되는 함수입니다. 넘겨받은 함수가 끝나기 전에 바로 실행되는 동기 콜백도 있고, 타이머나 사용자 이벤트처럼 나중에 실행되는 비동기 콜백도 있습니다.
고차 함수와 callback 실행 계약 — 상태 추적
map은 반환 전에 callback을 실행하고 setTimeout은 등록 호출이 반환된 뒤 나중에 callback을 실행한다. 고정 호출자/API/callback의 스택·반환·출력 상태를 비교할 가치가 있다.
EXECUTION TIMING · CALLBACK
콜백: 실행 시점과 호출 횟수는 API가 정한다
콜백은 함수 값으로 전달된다. 배열 메서드는 반환 전에 실행하고, 타이머·사용자 이벤트는 이후 조건이 충족되면 실행한다. 콜백이라는 이름만으로 비동기나 한 번 호출을 뜻하지는 않는다.
동기 콜백 · map / forEach
배열 메서드가 반환하기 전에 콜백을 호출한다. 요소마다 반복 호출될 수 있고, 콜백의 throw는 현재 동기 호출로 전파된다.
비동기 콜백 · timer / 사용자 이벤트
등록 함수는 먼저 반환하고, 이후 타이머나 실제 이벤트의 조건에 따라 콜백이 실행된다. 이벤트 리스너는 여러 이벤트에 반복 호출될 수 있다.
함수를 반환하는 고차 함수
createGreeter는 함수를 결과로 반환한다. 모든 고차 함수가 전달받은 콜백을 실행하는 것은 아니다.
등록과 정리의 책임
이벤트 리스너는 필요 없어지면 제거하고 타이머는 지원되는 API로 취소한다. 한 번 완료하는 작업인지 반복 알림인지도 확인한다.
나중 콜백의 오류는 등록 시점의 try/catch로 잡히지 않는다
동기 콜백의 throw는 현재 호출로 전파되지만, 나중 실행되는 콜백은 별도의 오류 처리가 필요하다. 결과 전달·실패 처리·취소와 정리 규칙을 API마다 확인한다. Promise의 한 번 settle 규칙을 일반 콜백 전체에 적용하지 않는다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 동기 map은 호출→콜백 실행→map 반환 순서다. 타이머는 등록→등록 반환과 현재 코드 계속 실행→나중 조건과 실행 기회→콜백 실행 순서다. 두 경로는 비교 대상이며 서로 연결되지 않는다. 콜백도 실행되는 동안 스택을 사용하고, 모든 콜백이 비동기인 것은 아니다. Promise 마이크로태스크와 직접 dispatchEvent의 동기 전송은 본문에서 별도로 구분한다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| map 호출 | callback: 동기 호출 / 반환: 아직 안 함 | map callback은 현재 호출 안에서 실행된다 | callback이라는 이름은 비동기를 뜻하지 않는다 |
| map 결과 | map: 반환 완료 / 현재 task: 계속 | 모든 callback 뒤 map이 배열을 반환한다 | 동기 callback 결과가 반환값에 포함된다 |
| timer 비교 | 등록: 반환됨 / callback: 후속 task | setTimeout 등록은 먼저 반환하고 후속 task에서 callback이 실행된다 | 호스트 API의 호출 계약이 실행 시점을 정한다 |
고차 함수와 callback 실행 계약 — 상태 추적
map은 반환 전에 callback을 실행하고 setTimeout은 등록 호출이 반환된 뒤 나중에 callback을 실행한다. 고정 호출자/API/callback의 스택·반환·출력 상태를 비교할 가치가 있다.
EXECUTION TIMING · CALLBACK
콜백: 실행 시점과 호출 횟수는 API가 정한다
콜백은 함수 값으로 전달된다. 배열 메서드는 반환 전에 실행하고, 타이머·사용자 이벤트는 이후 조건이 충족되면 실행한다. 콜백이라는 이름만으로 비동기나 한 번 호출을 뜻하지는 않는다.
동기 콜백 · map / forEach
배열 메서드가 반환하기 전에 콜백을 호출한다. 요소마다 반복 호출될 수 있고, 콜백의 throw는 현재 동기 호출로 전파된다.
비동기 콜백 · timer / 사용자 이벤트
등록 함수는 먼저 반환하고, 이후 타이머나 실제 이벤트의 조건에 따라 콜백이 실행된다. 이벤트 리스너는 여러 이벤트에 반복 호출될 수 있다.
함수를 반환하는 고차 함수
createGreeter는 함수를 결과로 반환한다. 모든 고차 함수가 전달받은 콜백을 실행하는 것은 아니다.
등록과 정리의 책임
이벤트 리스너는 필요 없어지면 제거하고 타이머는 지원되는 API로 취소한다. 한 번 완료하는 작업인지 반복 알림인지도 확인한다.
나중 콜백의 오류는 등록 시점의 try/catch로 잡히지 않는다
동기 콜백의 throw는 현재 호출로 전파되지만, 나중 실행되는 콜백은 별도의 오류 처리가 필요하다. 결과 전달·실패 처리·취소와 정리 규칙을 API마다 확인한다. Promise의 한 번 settle 규칙을 일반 콜백 전체에 적용하지 않는다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 동기 map은 호출→콜백 실행→map 반환 순서다. 타이머는 등록→등록 반환과 현재 코드 계속 실행→나중 조건과 실행 기회→콜백 실행 순서다. 두 경로는 비교 대상이며 서로 연결되지 않는다. 콜백도 실행되는 동안 스택을 사용하고, 모든 콜백이 비동기인 것은 아니다. Promise 마이크로태스크와 직접 dispatchEvent의 동기 전송은 본문에서 별도로 구분한다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| map 호출 | callback: 동기 호출 / 반환: 아직 안 함 | map callback은 현재 호출 안에서 실행된다 | callback이라는 이름은 비동기를 뜻하지 않는다 |
| map 결과 | map: 반환 완료 / 현재 task: 계속 | 모든 callback 뒤 map이 배열을 반환한다 | 동기 callback 결과가 반환값에 포함된다 |
| timer 비교 | 등록: 반환됨 / callback: 후속 task | setTimeout 등록은 먼저 반환하고 후속 task에서 callback이 실행된다 | 호스트 API의 호출 계약이 실행 시점을 정한다 |
위 그림은 두 가지 콜백의 실행 순서를 나란히 비교합니다. 왼쪽의 map()은 호출되면 넘겨받은 콜백을 요소마다 실행하고, 콜백이 return한 값으로 새 배열을 만든 뒤에야 반환합니다. 오른쪽의 setTimeout()은 콜백을 등록만 하고 바로 반환하므로, 현재 코드는 다음 문장을 계속 실행합니다. 콜백은 지정한 시간이 지나고 실행할 기회가 왔을 때 별도의 호출 흐름에서 실행됩니다. 두 경로는 서로 이어진 단계가 아니라 비교를 위한 것입니다.
그림 아래의 설명처럼 콜백이라는 이름만으로 비동기인지, 한 번만 호출되는지가 정해지지는 않습니다. 동기 콜백은 요소마다 여러 번 호출될 수 있고, 콜백에서 던진 오류는 지금 실행 중인 호출로 그대로 전해집니다. 이벤트 리스너는 이벤트가 생길 때마다 반복해서 호출되며, 나중에 실행되는 콜백의 오류는 등록할 때 감싼 try...catch로 잡히지 않습니다. 비동기 콜백도 실행되는 동안에는 호출 스택을 쓰므로, 오래 걸리는 콜백은 화면의 반응을 늦출 수 있습니다.
고차 함수
고차 함수(higher-order function)는 함수를 인수로 받거나, 함수를 결과로 돌려주거나, 둘 다 하는 함수입니다. 자바스크립트에서는 함수를 값처럼 다룰 수 있어서 고차 함수를 쉽게 만들 수 있으며, 함수형 프로그래밍도 이 특성을 바탕으로 합니다.
먼저 함수 값과 함수를 호출한 결과를 구분해야 합니다. add는 함수 그 자체이고, add(2, 3)은 지금 함수를 호출해서 얻은 결과입니다. 함수를 다른 함수에 넘길 때 add()처럼 괄호를 붙이면, 넘기기 전에 이미 호출되어 함수 대신 그 결과가 넘어갑니다.
함수를 인수로 받는 고차 함수
가장 흔한 형태이며, 배열의 map(), filter(), reduce(), forEach()가 대표적입니다. 이 메서드들은 안에서 반복을 수행하면서, 각 요소로 무엇을 할지는 인수로 받은 콜백 함수에 맡깁니다.
const numbers = [1, 2, 3, 4, 5];
// 1. Array.prototype.map(): 배열의 모든 요소에 특정 함수를 적용하여 새 배열 반환
// map은 콜백 함수(num => num * 2)를 인자로 받습니다.
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubledNumbers); // 결과: [2, 4, 6, 8, 10]
// 2. Array.prototype.filter(): 특정 조건을 만족하는 요소만 걸러내 새 배열 반환
// filter는 콜백 함수(num => num % 2 === 0)를 인자로 받습니다.
const evenNumbers = numbers.filter(num => num % 2 === 0);
console.log(evenNumbers); // 결과: [2, 4]
// 3. Array.prototype.reduce(): 배열의 모든 요소를 하나의 값으로 축소
// reduce는 콜백 함수((acc, curr) => acc + curr)를 인자로 받습니다.
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(sum); // 결과: 15 (초기값 0부터 시작)
// 4. Array.prototype.forEach(): 배열의 각 요소에 대해 콜백 함수 실행 (반환값 없음)
// forEach는 콜백 함수(num => console.log(num))를 인자로 받습니다.
numbers.forEach(num => console.log(`숫자: ${num}`));
/*
결과:
숫자: 1
숫자: 2
...
*/반복하는 코드는 메서드 안에 감춰져 있으므로, 쓰는 쪽은 각 요소로 무엇을 할지만 콜백으로 적으면 됩니다. forEach()는 다른 세 메서드와 달리 새 값을 만들지 않고 언제나 undefined를 돌려주므로, 요소마다 출력처럼 어떤 동작을 할 때 씁니다.
더 깊이: 콜백이 배열을 바꾸면 반복은 어떻게 될까
map()이나 forEach()의 콜백 안에서 같은 배열에 요소를 넣거나 빼면, 반복이 어디까지 갈지 헷갈립니다. 명세는 이 경우를 정해 두었습니다. 반복할 범위는 메서드를 호출한 순간의 길이로 정해지므로, 콜백이 push()로 뒤에 붙인 요소는 방문하지 않습니다. 그래서 콜백마다 요소를 하나씩 더해도 반복이 끝없이 이어지지 않습니다.
이미 지나간 위치의 값을 바꿔도 그 위치를 다시 방문하지는 않습니다. 반대로 아직 방문하지 않은 위치의 값을 바꾸면, 콜백은 그 위치에 도착한 시점의 새 값을 받습니다. 아직 방문하지 않은 요소를 지우면 그 요소는 방문하지 않습니다.
빈자리도 다르게 다룹니다. arr[5] = "x"처럼 중간을 건너뛰어 만든 희소 배열(sparse array)에서 forEach()는 값이 없는 빈자리를 아예 건너뛰고, 그 자리에 대해 콜백을 호출하지 않습니다. undefined를 직접 넣은 자리는 빈자리가 아니므로 방문합니다. MDN은 이런 동시 수정이 코드를 이해하기 어렵게 만들므로 피하라고 권하며, 규칙은 MDN의 Array 설명에 정리되어 있습니다.
함수를 반환하는 고차 함수
함수를 반환하는 고차 함수는 클로저와 함께 쓰여, 설정값을 기억하는 함수를 만들어 낼 때 유용합니다. 클로저와 실행 컨텍스트의 multiply처럼 바깥 호출의 값을 기억하는 함수를 만드는 방식입니다.
// 인사를 커스터마이징하는 함수를 반환하는 고차 함수
function createGreeter(greeting) {
return function(name) { // 이 내부 함수가 클로저이며, greeting을 기억합니다.
console.log(`${greeting}, ${name}!`);
};
}
const sayHello = createGreeter("Hello"); // 'Hello'라는 인사를 가진 함수 반환
const sayHi = createGreeter("Hi"); // 'Hi'라는 인사를 가진 함수 반환
const sayBonjour = createGreeter("Bonjour"); // 'Bonjour'라는 인사를 가진 함수 반환
sayHello("Alice"); // 결과: Hello, Alice!
sayHi("Bob"); // 결과: Hi, Bob!
sayBonjour("Charlie"); // 결과: Bonjour, Charlie!createGreeter()는 함수를 결과로 돌려주는 고차 함수이고, 돌려받은 sayHello, sayHi, sayBonjour는 각자의 호출에서 받은 greeting을 기억하는 클로저입니다. 이 함수는 받은 콜백을 실행하는 것이 아니라 새 함수를 만들어 주므로, 모든 고차 함수가 콜백을 실행하는 것은 아니라는 점도 알 수 있습니다.
콜백 함수
콜백 함수는 다른 함수에 인수로 넘겨져, 그 함수 안에서 정해진 시점에 호출되는 함수입니다. “이 작업이 끝나면 이 함수를 불러 달라”고 맡겨 두는 것과 같습니다. 자바스크립트는 오래 걸리는 작업을 기다리는 동안 다른 코드를 멈추지 않고 실행하는데, 콜백은 이런 비동기 처리의 기본 수단입니다.
동기 콜백
동기 콜백(synchronous callback)은 넘겨받은 함수가 반환하기 전에, 그 호출 흐름 안에서 실행됩니다. 앞의 map(), filter(), forEach()가 콜백을 이렇게 호출하며, 다음처럼 직접 만든 함수도 마찬가지입니다.
function processArray(arr, callback) {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i); // 배열 요소마다 콜백을 즉시 호출
}
}
processArray([1, 2, 3], function(item, index) {
console.log(`인덱스 ${index}의 값: ${item}`);
});
/*
결과:
인덱스 0의 값: 1
인덱스 1의 값: 2
인덱스 2의 값: 3
*/
console.log("모든 처리 완료 (동기적)"); // processArray가 끝난 후 바로 실행processArray()가 반복을 마치고 반환한 다음에야 마지막 줄이 실행되므로, “모든 처리 완료 (동기적)”는 세 줄의 인덱스 출력 뒤에 나옵니다. 동기 콜백이라고 해서 등록하자마자 함수의 첫 줄에서 실행된다는 뜻은 아니며, 넘겨받은 함수가 끝나기 전에 실행된다는 뜻입니다.
비동기 콜백
비동기 콜백(asynchronous callback)은 콜백을 등록한 호출이 먼저 반환한 뒤, API가 정한 조건이 되었을 때 실행됩니다. 브라우저의 타이머가 대표적입니다.
// 1. setTimeout (타이머)
console.log("시작!");
setTimeout(function() { // 이 익명 함수가 콜백 함수입니다.
console.log("2초 후에 실행됩니다.");
}, 2000); // 2000밀리초 = 2초
console.log("끝!"); // "시작!" 바로 다음에 "끝!"이 출력되고, 2초 후에 "2초 후에 실행됩니다."가 출력됩니다.
// 이는 setTimeout이 비동기적으로 동작하기 때문입니다.출력 순서는 “시작!”, “끝!”, “2초 후에 실행됩니다.”입니다. setTimeout()은 콜백을 등록만 하고 바로 반환하므로 “끝!”이 먼저 출력됩니다. 2000은 최소한 기다릴 시간이며, 다른 코드가 실행 중이면 콜백은 더 늦게 실행될 수 있습니다.
사용자 이벤트의 콜백도 비동기로 실행됩니다. 다음 코드는 <button id="myBtn">클릭</button> 요소보다 뒤에 있는 <script>에 넣어야 버튼을 찾을 수 있습니다.
// 2. 이벤트 핸들러 (사용자 이벤트)
// (HTML 파일에 <button id="myBtn">클릭</button>이 있다고 가정)
const myButton = document.getElementById('myBtn');
myButton.addEventListener('click', function() { // 이 익명 함수가 콜백 함수입니다.
console.log("버튼이 클릭되었습니다!");
});
// 사용자가 버튼을 클릭하기 전까지는 이 콜백 함수가 실행되지 않습니다.리스너는 등록만 해 두면 버튼을 클릭할 때마다 반복해서 실행됩니다. 다만 사용자의 클릭과 달리, 코드에서 dispatchEvent()로 이벤트를 직접 보내면 리스너가 dispatchEvent() 호출이 반환하기 전에 동기적으로 실행됩니다. 같은 콜백이라도 어떻게 호출되는지에 따라 실행 시점이 달라지는 것입니다.
비동기 콜백은 등록만큼 정리도 중요합니다. 더 필요 없는 이벤트 리스너는 removeEventListener()로 제거하고, 아직 실행되지 않은 타이머는 setTimeout()이 돌려준 값을 clearTimeout()에 넘겨 취소합니다. 또 브라우저의 타이머 콜백은 태스크(task)로 실행되지만, 다음 장에서 다룰 Promise의 콜백은 마이크로태스크(microtask)로 처리되므로 모든 비동기 콜백이 같은 순서 규칙을 따르지는 않습니다.
더 깊이: 타이머가 정한 시간보다 늦는 이유
setTimeout()의 지연 시간은 최소한 기다릴 시간일 뿐이고, 브라우저가 일부러 더 늦추는 경우도 있습니다. 타이머 콜백 안에서 다시 setTimeout()을 거는 식으로 호출이 다섯 번 넘게 중첩되면, 지연을 0으로 적어도 브라우저는 최소 4밀리초를 기다립니다. HTML 표준이 정한 규칙이라, setTimeout(fn, 0)을 이어 거는 반복이 생각보다 느린 이유가 됩니다.
탭이 화면에 보이지 않을 때는 더 크게 늦어집니다. MDN에 따르면 Firefox 데스크톱은 비활성 탭의 타이머를 최소 1초 간격으로 늦춥니다. Chrome은 버전 88부터 페이지가 5분 넘게 보이지 않고 30초 넘게 소리가 없는 등의 조건에서 중첩된 타이머를 1분에 한 번만 확인하는 강한 제한을 둡니다. 그래서 타이머의 호출 횟수로 시간을 재지 말고, 실제로 지난 시간은 Date.now()나 performance.now()로 계산합니다.
지연 시간에는 상한도 있습니다. 브라우저는 지연 값을 부호 있는 32비트 정수로 바꾸므로 최댓값은 2147483647밀리초, 약 24.8일이며, 이보다 큰 값을 넣으면 값이 넘쳐 콜백이 곧바로 실행될 수 있습니다. 한 달 뒤에 실행할 일을 setTimeout() 하나로 예약하면 안 되는 이유입니다. 자세한 조건은 MDN의 setTimeout 설명에 있습니다.
콜백으로 동작 바꾸기
콜백을 쓰면 함수가 할 일을 미리 정해 두지 않고, 호출할 때 넘겨서 정할 수 있습니다. 다음 코드의 performOperation()은 두 수를 받아 안내 줄을 출력하는 일까지만 정해 두고, 실제 계산은 세 번째 인수로 받은 함수에 맡깁니다. 같은 함수에 add, multiply, 화살표 함수를 차례로 넘겨 세 가지 다른 계산을 시킵니다.
function performOperation(a, b, operation) {
// operation은 콜백 함수입니다.
console.log(`두 값 ${a}, ${b}에 대해 연산 수행:`);
operation(a, b);
}
// 덧셈 연산을 수행하는 콜백
function add(x, y) {
console.log(`${x} + ${y} = ${x + y}`);
}
// 곱셈 연산을 수행하는 콜백
function multiply(x, y) {
console.log(`${x} * ${y} = ${x * y}`);
}
performOperation(10, 5, add);
// 두 값 10, 5에 대해 연산 수행:
// 10 + 5 = 15
performOperation(10, 5, multiply);
// 두 값 10, 5에 대해 연산 수행:
// 10 * 5 = 50
// 화살표 함수를 이용해 인라인으로 콜백 정의
performOperation(7, 3, (num1, num2) => {
console.log(`${num1} 빼기 ${num2} = ${num1 - num2}`);
});
// 두 값 7, 3에 대해 연산 수행:
// 7 빼기 3 = 4performOperation()은 안내 줄을 출력한 뒤 넘겨받은 operation을 호출하므로, 호출마다 안내 줄과 계산 줄이 차례로 출력됩니다. 이때 add 대신 add(10, 5)를 넘기면 계산 줄이 안내 줄보다 먼저 출력되고, operation에는 함수가 아닌 undefined가 들어가 TypeError가 납니다. 또 performOperation()은 operation(a, b)의 결과를 return하지 않으므로 자신의 반환값은 undefined입니다.
콜백 헬
여러 비동기 작업이 앞 작업의 완료에 의존해 차례로 실행되어야 하면, 다음 작업을 앞 작업의 콜백 안에 넣어야 해서 코드가 점점 깊게 중첩됩니다. 이를 콜백 헬(callback hell) 또는 파멸의 피라미드(pyramid of doom)라고 부르며, 코드를 읽고 고치기 어렵게 만듭니다.
// 콜백 헬 예시 (가상의 비동기 함수)
function step1(callback) {
setTimeout(function() {
console.log("Step 1 완료");
callback();
}, 1000);
}
function step2(callback) {
setTimeout(function() {
console.log("Step 2 완료");
callback();
}, 1000);
}
function step3(callback) {
setTimeout(function() {
console.log("Step 3 완료");
callback();
}, 1000);
}
// 콜백 헬의 모습
step1(function() {
step2(function() {
step3(function() {
console.log("모든 단계 완료!");
});
});
});출력은 약 1초 간격으로 “Step 1 완료”, “Step 2 완료”, “Step 3 완료”가 나오고, “모든 단계 완료!”는 기다리는 타이머 없이 “Step 3 완료” 바로 뒤에 나옵니다. 각 단계가 완료 콜백을 호출한 다음에야 다음 단계의 타이머가 등록되므로, 세 타이머를 한꺼번에 등록하는 코드와 달리 단계가 하나씩 이어집니다.
이 예제는 성공하는 흐름만 담고 있습니다. 실제 작업이라면 중간 단계가 실패했을 때 다음 단계를 실행할지, 이미 연 자원은 어디서 정리할지를 단계마다 따로 처리해야 해서 중첩은 더 깊어집니다. 다음 장에서 다룰 Promise와 async/await가 이렇게 차례로 이어지는 흐름을 평평하게 쓰는 방법입니다.
정리
- 함수는 값처럼 넘기고 돌려줄 수 있으며, 함수를 인수로 받거나 함수를 돌려주는 함수를 고차 함수라고 합니다.
add는 함수 값이고add()는 호출한 결과이므로, 콜백을 넘길 때는 괄호 없이 함수 자체를 넘깁니다.- 동기 콜백은 넘겨받은 함수가 반환하기 전에 실행되고, 비동기 콜백은 등록한 호출이 반환한 뒤 정해진 조건에서 실행됩니다.
- 콜백의 실행 시점과 호출 횟수는 API가 정하며, 같은 리스너도
dispatchEvent()로 보내면 동기로 실행됩니다. - 나중에 실행되는 콜백의 오류는 등록할 때의
try...catch로 잡히지 않고, 필요 없어진 리스너와 타이머는 직접 정리해야 합니다. - 앞 작업에 의존하는 비동기 작업을 콜백으로 이으면 중첩이 깊어지는 콜백 헬이 생깁니다.
연습 문제
1. 실행 순서 예상하기
다음 코드를 새 HTML의 <script>에서 실행합니다. 타이머의 지연 시간 0은 지금 바로 실행하라는 뜻일까요? 네 로그의 순서와 map()이 돌려주는 값을 먼저 적어 보세요.
console.log("A 시작");
setTimeout(() => console.log("D 타이머"), 0);
const mappedForCallback = [2].map(value => {
console.log("B 동기 콜백");
return value * 3;
});
console.log("C 결과", mappedForCallback[0]);해설 보기
A 시작, B 동기 콜백, C 결과 6, D 타이머 순서입니다. map()은 반환하기 전에 콜백을 호출하므로 B가 C보다 먼저 나오고, 콜백이 돌려준 6으로 [6]을 만들어 반환합니다.
setTimeout()은 등록만 하고 반환하며, 지연 시간이 0이어도 콜백은 지금 실행 중인 스크립트가 끝난 뒤에 실행됩니다. 그래서 D가 가장 마지막입니다.
2. return을 빠뜨리면
앞 코드의 map() 콜백에서 return value * 3;을 value * 3;으로 바꿨습니다. 로그의 순서와 C 결과 뒤의 값은 어떻게 달라질까요? 곱셈이 실행되는 것과 그 결과가 바깥으로 전달되는 것을 구분해 보세요.
해설 보기
순서는 그대로 A, B, C, D입니다. 곱셈은 실행되지만 콜백이 값을 return하지 않으므로 콜백의 결과는 undefined이고, map()은 [undefined]를 만들어 셋째 로그가 C 결과 undefined가 됩니다. 중괄호를 쓴 화살표 함수는 return을 적어야 값을 돌려줍니다.
3. 나중에 던진 오류
다음 코드의 출력은 어떻게 될까요? catch 블록은 실행될까요?
try {
setTimeout(() => {
throw new Error("나중 오류");
}, 0);
} catch (error) {
console.log("잡음:", error.message);
}
console.log("등록 끝");해설 보기
등록 끝이 먼저 출력되고, 그 뒤 콘솔에 처리되지 않은 오류 Error: 나중 오류가 표시됩니다. catch 블록은 실행되지 않습니다.
try 블록 안에서 실행되는 것은 타이머를 등록하는 일뿐이고, 등록은 오류 없이 끝납니다. 콜백은 try...catch가 이미 끝난 뒤 별도의 호출 흐름에서 실행되므로, 오류를 잡으려면 콜백 안에 try...catch를 넣어야 합니다.
map()의 콜백과 반환값은 MDN의 Array.prototype.map()에서, 타이머가 콜백을 실행하는 과정은 HTML 표준의 타이머에서, 직접 보낸 이벤트의 처리는 DOM 표준의 dispatchEvent()에서 확인할 수 있습니다.
이것으로 6장을 마칩니다. 6장에서는 실행 컨텍스트와 클로저, this 바인딩, 프로토타입과 class, 객체 지향과 함수형 프로그래밍, 고차 함수와 콜백을 차례로 살펴보며 자바스크립트 코드가 어떻게 동작하는지를 들여다봤습니다. 다음 장의 비동기 프로그래밍부터는 Promise와 async/await로 콜백 헬을 풀어 쓰는 방법을 시작으로, 이터레이터와 제너레이터, 모듈 시스템, 메모리 관리, 디자인 패턴을 다룹니다.