고차 함수와 콜백
우리는 4장에서 함수형 프로그래밍의 개념을 살짝 엿보았습니다. 그중에서도 고차 함수(Higher-Order Function) 는 함수형 프로그래밍의 강력함을 보여주는 핵심 요소입니다. 자바스크립트에서 함수는 다른 변수와 마찬가지로 '일급 객체(First-Class Citizen)'로 취급되기 때문에, 함수를 마치 데이터처럼 다른 함수의 인자로 전달하거나 함수의 반환값으로 사용할 수 있습니다.
이러한 고차 함수의 특성은 특히 콜백 함수(Callback Function) 개념과 결합될 때 엄청난 시너지를 발휘합니다. 콜백 함수는 어떤 함수의 실행이 끝난 뒤에 나중에 호출될 함수를 의미하며, 자바스크립트의 비동기 처리(시간이 걸리는 작업)와 사용자 이벤트 처리에서 거의 필수적으로 사용됩니다.
이번 장에서는 고차 함수의 정의와 다양한 활용법을 알아보고, 콜백 함수가 어떻게 비동기 작업을 관리하고 코드의 유연성을 높이는지 깊이 있게 탐구해 보겠습니다. 이 두 가지 개념을 마스터하는 것은 복잡한 웹 애플리케이션을 효과적으로 설계하고 구현하는 데 매우 중요합니다.
고차 함수 (Higher-Order Functions)
고차 함수는 다음 중 하나 이상의 조건을 만족하는 함수를 의미합니다.
하나 이상의 함수를 인자로 받는다.
함수를 결과(return 값)로 반환한다.
자바스크립트에서 함수는 값처럼 취급될 수 있기 때문에 이러한 동작이 가능하며, 이는 함수형 프로그래밍 패러다임의 근간이 됩니다.
함수를 인자로 받는 고차 함수
가장 흔하게 볼 수 있는 고차 함수 형태로, 배열의 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
...
*/
이러한 메서드들은 내부적인 반복 로직을 추상화하여 개발자가 '무엇을 할지'에만 집중할 수 있게 해줍니다.
함수를 반환하는 고차 함수 (클로저의 활용)
함수를 반환하는 고차 함수는 주로 클로저와 함께 사용되어 특정 설정을 가진 함수를 동적으로 생성할 때 유용합니다. 이를 통해 함수를 재사용하고, 유연한 동작을 만들 수 있습니다. (4장의 함수형 프로그래밍에서 '부분 적용' 예시와 유사)
// 인사를 커스터마이징하는 함수를 반환하는 고차 함수
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가 끝난 후 바로 실행
-
비동기적 콜백: 특정 작업(예: 타이머, 네트워크 요청, 파일 읽기, 사용자 이벤트)이 완료되기를 기다렸다가 실행되는 콜백입니다. 이 경우 콜백 함수는 호출 스택에서 바로 실행되지 않고, 태스크 큐(Task Queue)에 들어갔다가 자바스크립트 엔진이 한가해질 때(이벤트 루프) 실행됩니다.
// 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("버튼이 클릭되었습니다!"); }); // 사용자가 버튼을 클릭하기 전까지는 이 콜백 함수가 실행되지 않습니다.
비동기적 콜백은 웹 페이지가 멈추지 않고 다른 작업을 계속 수행할 수 있도록 해줍니다.
(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("모든 단계 완료!");
});
});
});
이러한 콜백 헬 문제는 자바스크립트의 비동기 처리를 위한 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 = 4
performOperation
함수는 어떤 연산을 수행할지 모르지만, 콜백 함수를 통해 그 동작을 위임받아 실행합니다.
마무리하며
이번 장에서는 자바스크립트의 함수형 프로그래밍을 가능하게 하는 핵심 요소인 고차 함수와 비동기 처리에 필수적인 콜백 함수에 대해 상세히 학습했습니다.
여러분은 map
, filter
, reduce
와 같이 함수를 인자로 받아 데이터를 처리하는 고차 함수들을 살펴보았고, 함수를 반환하여 특정 동작을 커스터마이징하는 방법도 이해했습니다. 또한, 콜백 함수가 다른 함수의 인자로 전달되어 나중에 호출되는 함수라는 것을 배웠고, 동기적/비동기적 콜백의 차이점을 파악했습니다.
특히, 콜백 함수가 웹 개발에서 비동기 작업(타이머, 이벤트 등)을 처리하는 데 얼마나 중요한지, 그리고 여러 콜백이 중첩될 때 발생하는 콜백 헬 문제를 인지하는 것이 중요합니다. 이 콜백 헬의 대안으로 다음 장에서 다룰 Promise
와 async/await
가 등장했음을 기억하며, 이번 장의 내용을 잘 숙지하시길 바랍니다.
고차 함수와 콜백은 여러분의 자바스크립트 코드를 더욱 유연하고 강력하게 만들어줄 것입니다. 다양한 예제들을 직접 작성해보면서 이 개념들을 완벽하게 이해하고 활용하는 능력을 길러보세요.