고차 함수와 콜백
함수를 인자로 받거나 반환하는 고차 함수를 익히고 동기·비동기 콜백과 콜백 헬이 생기는 흐름을 이해합니다.
앞선 객체 지향과 함수형 프로그래밍에서 순수 변환과 고차 함수를 살펴봤습니다. 이번에는 전달하는 함수 값과 그 함수를 호출한 결과를 구분합니다.
자바스크립트에서 함수는 일급 객체(First-Class Citizen)이므로, 다른 함수의 인자로 전달하거나 반환값으로 사용할 수 있습니다.
이러한 고차 함수의 특성은 특히 콜백 함수(Callback Function) 개념과 결합될 때 엄청난 시너지를 발휘합니다.
콜백 함수는 다른 함수에 인자로 전달되어 그 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의 호출 계약이 실행 시점을 정한다 |
고차 함수 (Higher-Order Functions)
add는 함수 값이고 add(2, 3)은 지금 함수를 호출한 결과입니다. 아래 performOperation(10, 5, add)가 함수를 전달하는 이유도 이 차이입니다. 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
...
*/이러한 메서드들은 내부적인 반복 로직을 추상화하여 개발자가 무엇을 할지에만 집중할 수 있게 해줍니다.
함수를 반환하는 고차 함수 (클로저의 활용)
함수를 반환하는 고차 함수는 주로 클로저와 함께 사용되어 특정 설정을 가진 함수를 동적으로 생성할 때 유용합니다.
이를 통해 함수를 재사용하고, 유연한 동작을 만들 수 있습니다.
앞선 클로저 예시의 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이라는 외부 변수에 접근할 수 있는 클로저입니다.
콜백 함수 (Callback Functions)
콜백 함수는 다른 함수의 인자로 전달되어, 특정 시점에 그 함수 내부에서 호출되는 함수를 의미합니다.
마치 "내가 어떤 작업을 끝내면, 너(콜백 함수)를 불러줄게"라고 약속하는 것과 같습니다.
콜백 함수는 자바스크립트의 비동기적 특성(논블로킹)을 구현하는 데 핵심적인 역할을 합니다.
동기적 콜백 vs 비동기적 콜백
-
동기적 콜백: 호출한 함수가 반환하기 전에 그 호출 흐름 안에서 실행됩니다. 위
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가 끝난 후 바로 실행 -
비동기적 콜백: 등록한 호출이 반환한 뒤, API가 정한 조건에서 실행됩니다. 브라우저 타이머 콜백은 태스크로 실행되지만, Promise 반응은 마이크로태스크로 처리되므로 모든 콜백을 하나의 태스크 큐로 설명하지는 않습니다. 콜백도 실행되는 동안에는 호출 스택을 사용하며, 오래 걸리는 콜백은 화면 반응을 지연시킬 수 있습니다.
// 1. setTimeout (타이머) console.log("시작!"); setTimeout(function() { // 이 익명 함수가 콜백 함수입니다. console.log("2초 후에 실행됩니다."); }, 2000); // 2000밀리초 = 2초 console.log("끝!"); // "시작!" 바로 다음에 "끝!"이 출력되고, 2초 후에 "2초 후에 실행됩니다."가 출력됩니다. // 이는 setTimeout이 비동기적으로 동작하기 때문입니다.// 2. 이벤트 핸들러 (사용자 이벤트) // (HTML 파일에 <button id="myBtn">클릭</button>이 있다고 가정) const myButton = document.getElementById('myBtn'); myButton.addEventListener('click', function() { // 이 익명 함수가 콜백 함수입니다. console.log("버튼이 클릭되었습니다!"); }); // 사용자가 버튼을 클릭하기 전까지는 이 콜백 함수가 실행되지 않습니다.
원래 타이머 예시의 출력 순서는 시작! → 끝! → 2초 후에 실행됩니다.입니다. 2000은 최소 지연 요청이며 정확히 2초에 실행된다는 보장은 없습니다. 버튼 예시의 코드는 해당 HTML 뒤에 배치하세요. 일반 사용자 클릭과 달리 코드에서 dispatchEvent()로 직접 이벤트를 보내면 리스너가 그 호출이 반환하기 전에 동기적으로 실행됩니다.
콜백 헬 (Callback Hell)
콜백 함수는 비동기 처리의 기본이지만, 여러 비동기 작업이 순차적으로 의존성을 가질 때 코드가 깊게 중첩되는 현상이 발생할 수 있습니다.
이를 콜백 헬(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("모든 단계 완료!");
});
});
});위 예시는 Step 1 완료 → Step 2 완료 → Step 3 완료 → 모든 단계 완료! 순서입니다. 각 단계가 완료 콜백을 호출한 뒤에 다음 타이머를 등록하므로 세 타이머를 한 번에 등록한 병렬 코드와 다릅니다. 실패·취소 경로는 생략된 성공 흐름이며, 실제 작업에는 오류가 나면 다음 단계를 실행할지와 자원 정리를 누가 할지 정해야 합니다. Promise와 async/await는 다음 장에서 이 의존 흐름을 다룹니다.
(다음 장에서 다룰 예정)
콜백 함수의 유연성
콜백 함수는 코드의 유연성을 극대화합니다.
함수가 수행할 작업을 미리 정의하지 않고, 실행 시점에 동적으로 전달할 수 있게 해줍니다.
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 = 15
performOperation(10, 5, multiply); // 결과: 10 * 5 = 50
// 화살표 함수를 이용해 인라인으로 콜백 정의
performOperation(7, 3, (num1, num2) => {
console.log(`${num1} 빼기 ${num2} = ${num1 - num2}`);
}); // 결과: 7 빼기 3 = 4performOperation은 첫 안내 줄도 출력합니다. 따라서 각 호출은 두 값 ...에 대해 연산 수행: 다음에 해당 계산 줄을 출력하며, 원래 주석은 둘째 계산 줄만 요약합니다. operation(a, b)를 실행해도 return으로 그 값을 전달하지 않으므로 performOperation 자체의 반환값은 undefined입니다.
실행 순서를 먼저 예상하기
아래 코드를 새 콘솔 또는 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은 콜백을 호출하고 [6]을 만들어 반환합니다. 타이머는 등록만 끝낸 뒤 현재 코드가 계속 실행되며, 지연 시간이 0이어도 현재 호출 흐름 중간에 끼어들지 않습니다.
독립 연습
위 map 콜백의 return value * 3; 한 줄을 value * 3;으로 바꾸세요. 로그의 순서와 C 결과 뒤 값이 어떻게 달라지는지 예측하세요. 곱셈이 실행된 것과 그 결과가 바깥으로 반환된 것을 구분해야 합니다.
return을 제거한 결과
순서는 여전히 A·B·C·D입니다. 콜백이 값을 반환하지 않아 map은 [undefined]를 만들고 셋째 로그는 C 결과 undefined가 됩니다. 타이머 순서나 비동기 여부가 반환값 누락을 해결해 주지는 않습니다.
근거: MDN map의 콜백과 반환값, HTML 타이머 알고리즘, DOM 이벤트 전송.
비동기 전환 요약
이번 장에서는 자바스크립트의 함수형 프로그래밍을 가능하게 하는 핵심 요소인 고차 함수와 비동기 처리에 필수적인 콜백 함수에 대해 상세히 학습했습니다.
이번 절에서는 고차 함수와 콜백 함수의 동작 방식, 동기/비동기 콜백 차이를 정리했습니다.
특히, 콜백 함수가 웹 개발에서 비동기 작업(타이머, 이벤트 등)을 처리하는 데 얼마나 중요한지, 그리고 여러 콜백이 중첩될 때 발생하는 콜백 헬 문제를 인지하는 것이 중요합니다.
콜백 중첩 문제를 이해하면 다음 장의 Promise와 async/await 필요성을 연결할 수 있습니다.
앞의 고차 함수와 callback 실행 계약 다이어그램은 콜백의 동기·비동기 성격과 중첩 위험을 함께 판단하는 기준입니다.
고차 함수와 콜백을 고를 때는 코드가 즉시 실행되는지, 나중에 호출되는지, 실패 흐름까지 표현해야 하는지를 함께 봐야 합니다.
콜백 기반 코드는 호출 시점, 결과·오류 전달, 정리 소유권 순서로 읽고 개선합니다.
고차 함수와 콜백은 처리할 동작을 인자로 넘기거나 나중에 실행해야 할 때 사용합니다.
예제를 바꿔 보면서 호출 시점과 전달되는 인자의 흐름을 확인해 보세요.
고차 함수와 콜백을 사용할 때는 함수가 즉시 실행되는지, 나중에 실행되는지, 실패 흐름을 어떻게 표현할지까지 함께 봐야 합니다.