[코어 자바스크립트] 4. 콜백 함수
콜백 함수의 개념을 제어권 위임 관점에서 이해하고, this와 비동기 흐름까지 정리해보기
✍🏻 핵심 정리
- 콜백 함수는 다른 함수의 인자로 전달되는 함수로, 호출 시점, 인자, 그리고 경우에 따라 this에 대한 제어권을 위임하게 된다.
콜백 함수란?
다른 함수의 argument로 전달되어, 그 함수에 의해 나중에 호출되는 함수이다.
단순히 함수를 넘기는 것이 아니라, 아래와 같은 제어권(Control)을 함께 위임한다.
- 언제 호출할지
- 어떤 인자를 넘길지
- 어떤 this로 호출할지
parameter vs. argument
parameter: 함수 정의 시 사용하는 변수argument: 함수 호출 시 전달하는 실제 값
제어권 위임
1. 호출 시점
콜백 함수의 실행 시점은 콜백을 전달받은 함수가 결정한다.
1
2
3
4
5
6
7
8
let count = 0;
const cbFunc = function () {
console.log(count);
if (++count > 4) clearInterval(timer);
};
const timer = setInterval(cbFunc, 1000); // setInterval이 1초에 한 번 cbFunc를 호출
cbFunc는 setInterval에게 ‘언제 실행할지’의 제어권을 위임한다.
2. 인자 (argument)
콜백 함수에 어떤 값들을 어떤 순서로 전달할지도 콜백을 전달받은 함수가 결정한다.
1
2
[1, 2, 3].map((value, index, array) => {});
// 콜백 시그니처(인자 순서와 종류)는 map이 결정함
3. this
콜백이라는 이유로 특별한 규칙이 있는 것이 아니라, 일반 함수와 동일하게 호출 방식에 따라 결정된다.
- 일반 함수 호출 → default binding
- non-strict: 전역 객체
- strict mode:
undefined
- 호출한 함수가 thisArg를 지정하는 경우 → 해당 값으로 바인딩
메소드를 콜백으로 전달하더라도, 일반 함수로 호출되기 때문에
this가 원래 객체를 가리키지 않을 수 있다.
참고: 코드로 확인하기의 메소드 참조 전달 (this 깨짐)
콜백 함수 내부 this 바인딩 방법
1. 전통적인 방식 (self/that 패턴)
1
2
3
4
5
6
7
8
9
const obj = {
x: 10,
f() {
var self = this; // this를 변수에 저장
setTimeout(function () {
console.log(self.x); // 10
}, 0);
}
};
2. bind 메소드 사용
1
2
func(cb.bind(obj));
// 명시적으로 this를 고정한 새 함수를 전달
3. 화살표 함수
1
2
const cb = () => console.log(this);
// lexical this → 상위 스코프의 this 유지
콜백 지옥과 비동기 제어
콜백 지옥이란?
콜백을 중첩해서 사용하는 경우 들여쓰기가 깊어지고 가독성이 떨어지는 문제다.
1
2
3
4
5
6
7
8
9
10
// 콜백 지옥 예시
step1(function (result1) {
step2(result1, function (result2) {
step3(result2, function (result3) {
step4(result3, function (result4) {
console.log(result4);
});
});
});
});
이 문제는 비동기 작업의 실행 흐름에 대한 제어권이 콜백을 호출하는 쪽에 있고,
비동기 작업의 실행 흐름이 콜백 내부로 들어가면서 코드의 흐름을 예측하고 제어하기 어려워진다는 점이다.
해결법 1: 기명 함수로 변환
함수를 분리하여 가독성을 개선할 수 있지만, 흐름 파악이 어려워질 수 있다.
해결법 2: Promise
비동기 작업의 상태를 관리하는 객체이다.
pending→fulfilled/rejected
1
2
3
4
fetchData()
.then((result) => process(result))
.then((processed) => save(processed))
.catch((error) => handleError(error));
콜백 중첩을 줄이고 흐름을 체이닝으로 표현할 수 있다.
해결법 3: Generator
*이 붙은 함수로 Iterator를 반환하여 next()를 호출하면 yield에서 실행을 멈춘다.
async/await 이전에 비동기 흐름 제어를 위해 사용되던 방식이며, 현재는 거의 사용되지 않는다.
1
2
3
4
function* gen() {
const result1 = yield fetchData();
const result2 = yield process(result1);
}
해결법 4: async / await (현재 권장)
1
2
3
4
5
6
7
8
9
async function f() {
try {
const result1 = await fetchData();
const result2 = await process(result1);
await save(result2);
} catch (error) {
handleError(error);
}
}
async: 함수가 Promise를 반환await: Promise가 resolve될 때까지 대기
비동기 코드를 동기 코드처럼 표현 가능하여 가독성이 크게 향상된다.
🔍 코드로 확인하기
제어권 위임 확인
1
2
3
4
5
6
7
8
9
function repeat(callback) {
for (let i = 0; i < 3; i++) {
callback(i);
}
}
repeat((i) => console.log(i));
// 결과: 0, 1, 2
// → repeat 함수가 콜백의 실행 시점과 인자를 모두 결정 (제어권이 repeat에게 있음)
메소드 참조 전달 (this 깨짐)
1
2
3
4
5
6
7
8
9
const obj = {
x: 10,
getX() {
return this.x;
}
};
setTimeout(obj.getX, 0); // undefined ← 함수 참조 전달 → 일반 함수 호출
setTimeout(() => obj.getX(), 0); // 10 ← 화살표 함수 내에서 메소드 호출
forEach의 thisArg
1
2
3
4
5
const obj = { x: 10 };
[1].forEach(function () {
console.log(this.x);
}, obj); // 결과: 10 ← forEach의 두 번째 인자로 thisArg를 명시적으로 전달
일반 콜백 vs 화살표 함수 콜백
1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
x: 10,
f() {
[1].forEach(function () {
console.log(this.x); // undefined ← default binding
});
[1].forEach(() => {
console.log(this.x); // 10 ← lexical this (f의 this = obj)
});
}
};
obj.f();
💼 실무 연결 포인트
실무에서 콜백 this 문제가 자주 발생하는 상황은 아래와 같다.
- 이벤트 핸들러에서
this가 깨지는 문제: 메소드를 이벤트 핸들러로 전달하면this가 element가 됨 - setTimeout / 비동기 콜백에서
this유실: 일반 함수로 호출되어 전역 객체 바인딩
따라서
- 메소드를 콜백으로 넘길 때는
this가 깨질 수 있음을 항상 인지하고 필요하면bind또는 화살표 함수를 사용한다. - 비동기 흐름은
Promise/async/await로 관리한다.
🗣️ 면접 대비 Q&A
Q1. 콜백 함수란 무엇인가요?
콜백 함수란 다른 함수의 인자로 넘겨지는 함수를 말합니다. 콜백 함수는 함수의 호출 시점, 함수의 호출 인자, this의 제어권을 전달받은 함수에게 넘기게 됩니다.
Q2. 콜백 함수에서 this는 어떻게 결정되나요?
this는 콜백이 어떻게 호출되느냐에 따라 결정됩니다. 대부분의 경우 콜백은 일반 함수로 호출되기 때문에 this가 바인딩되지 않고, 필요한 경우 bind나 thisArg로 명시적으로 지정할 수 있습니다. 화살표 함수는 상위 스코프의 this를 그대로 사용합니다.
Q3. 콜백 지옥이란 무엇이고 어떻게 해결하나요?
콜백 지옥은 콜백 함수가 중첩되면서 코드의 가독성과 유지보수성이 크게 떨어지는 문제입니다. 이 문제의 본질은 비동기 작업의 실행 흐름에 대한 제어권이 콜백을 호출하는 쪽에 있기 때문에 코드의 흐름을 예측하고 제어하기 어려워진다는 점입니다.
이를 해결하기 위해 Promise가 도입되었습니다. Promise는 비동기 작업의 결과를 객체로 표현하여 then 체이닝을 통해 흐름을 순차적으로 구성할 수 있게 합니다. 하지만 체이닝이 길어지면 가독성이 다시 떨어질 수 있기 때문에, async/await를 사용하여 비동기 코드를 동기 코드처럼 표현할 수 있게 되었습니다.
💡 최종 인사이트
콜백 함수는 단순히 함수를 전달하는 개념이 아니라, 함수의 실행 시점, 인자, this에 대한 제어권을 넘기는 것이다.
이로 인해 코드의 실행 흐름을 직접 제어하기 어려워지는 문제가 발생하며, 특히 비동기 상황에서는 예측이 어려워진다.
Promise는 이러한 제어권 문제를 해결하기 위해 등장했으며, async/await는 이를 더 직관적으로 표현하기 위한 문법이다.