콜백 함수란?
콜백 함수는 다른 함수에 인수로 전달되는 함수이다. 자바스크립트는 함수를 1급 객체(First-class object)로 취급하기 때문에 함수를 변수에 할당하거나 함수의 인수로 전달할 수 있다. 콜백 함수는 일반적으로 비동기 작업을 처리하기 위해 사용된다. 비동기 작업이 완료된 후 호출될 함수를 미리 전달해두고 작업이 끝났을 때 해당 함수가 실행되도록 만드는 방식이다.
콜백함수의 구분
콜백함수는 주로 비동기 작업을 처리하기 위해 사용되지만 동기 작업에도 사용할 수 있다. 두 가지 방법의 예시를 통해 콜백 함수를 알아보자.
먼저 두 가지 방식에 대한 차이점을 간단하게 설명하면 동기적 콜백 함수는 코드가 순차적으로 실행된다. 콜백 함수가 완료되기 전까지 다음 코드로 넘어가지 않는다. 비동기적 콜백 함수는 특정 작업이 완료될 때까지 기다리지 않고 다음 코드를 비동기적으로 실행한 후 작업이 완료되면 나중에 콜백을 실행한다. 아래 예시를 통해 확인해보자.
동기적 콜백 함수의 예시
// 배열의 forEach 메서드의 사용한 동기적 콜백 함수 예제
function processNumber(number) {
console.log(`숫자 세기: ${number}`);
}
const numbers = [1, 2, 3, 4, 5];
// forEach는 동기적으로 콜백 함수를 실행
numbers.forEach(processNumber);
console.log("모든 숫자 세기 끝");
동기적으로 진행되는 코드로 순차적으로 코드가 실행되므로 아래와 같은 결과가 나온다.
숫자 세기: 1
숫자 세기: 2
숫자 세기: 3
숫자 세기: 4
숫자 세기: 5
모든 숫자 세기 끝
비동기적 콜백 함수의 예시
// setTimeout을 사용한 비동기적 콜백 콜백 함수 예제
function asyncCallback() {
console.log('비동기 작업 완료 후 실행됨');
}
// 2초 후에 콜백 함수 실행
setTimeout(asyncCallback, 2000);
console.log('비동기 작업이 끝나기 전에 실행됨');
setTimeout은 비동기 함수로, 2초 후에 asyncCallback 콜백 함수가 실행된다. 하지만 setTimeout이 설정된 직후 바로 다음 코드("비동기 작업이 끝나기 전에 실행됨")가 실행되어 순차적이지 않다. 이는 비동기 작업의 특성으로 콜백이 호출될 때까지 자바스크립트는 다른 작업을 계속 처리한다. 실행 결과는 아래와 같을 것이다.
비동기 작업이 끝나기 전에 실행됨
비동기 작업 완료 후 실행됨
콜백지옥
콜백 함수는 편리하지만 중첩하여 사용할 때는 코드가 매우 복잡해질 수 있다. 이 문제를 흔히 "콜백 지옥(Callback Hell)"이라고 부르는데 콜백 지옥의 예시를 확인해보고 추가로 콜백 지옥에 해결 방법에 대해서 간단하게 알아보자.
콜백 지옥 예시
function firstTask(callback) {
setTimeout(() => {
console.log('첫 번째 작업 완료');
callback();
}, 1000);
}
function secondTask(callback) {
setTimeout(() => {
console.log('두 번째 작업 완료');
callback();
}, 1000);
}
function thirdTask(callback) {
setTimeout(() => {
console.log('세 번째 작업 완료');
callback();
}, 1000);
}
firstTask(() => {
secondTask(() => {
thirdTask(() => {
console.log('모든 작업 완료!');
});
});
});
위 예시에서 3개의 비동기 작업을 순서대로 실행하기 위해 각 작업마다 콜백 함수를 전달하고 있다. 그러나 콜백 함수를 중첩하다 보면 코드의 가독성이 떨어지고 유지보수가 어려워지는 문제가 발생한다. 예제보다 더 복잡하고 많은 작업을 수행한다면 코드를 보고 싶지도 않을 것이다.
콜백 지옥의 해결법
콜백 지옥 문제를 해결하기 위해 ES6에서는 Promise라는 객체가 도입되었다. 그리고 ES8에서는 async/await 문법이 추가되어 비동기 작업을 더욱 직관적으로 처리할 수 있게 되었다. 콜백 지옥 문제를 해결하는 방법에 대해서는 다른 글에서 더 자세히 다루고 여기서는 간단한 예시만 참고하도록 하자.
Promise 예시
function firstTask() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('첫 번째 작업 완료');
resolve();
}, 1000);
});
}
function secondTask() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('두 번째 작업 완료');
resolve();
}, 1000);
});
}
function thirdTask() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('세 번째 작업 완료');
resolve();
}, 1000);
});
}
firstTask()
.then(() => secondTask())
.then(() => thirdTask())
.then(() => {
console.log('모든 작업 완료!');
});
async/await 예시
function firstTask() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('첫 번째 작업 완료');
resolve();
}, 1000);
});
}
function secondTask() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('두 번째 작업 완료');
resolve();
}, 1000);
});
}
function thirdTask() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('세 번째 작업 완료');
resolve();
}, 1000);
});
}
async function runTasks() {
await firstTask();
await secondTask();
await thirdTask();
console.log('모든 작업 완료!');
}
runTasks();
2 가지 방법으로 더욱 직관적이고 콜백의 중첩을 해결할 수 있게 되었다.
콜백 함수에서 this의 문제
자바스크립트에서 함수가 호출될 때 그 함수가 어떻게 호출되었는지에 따라 this가 달라진다. 특히 콜백 함수에서의 this는 호출하는 주체에 의해 결정되기 때문에 객체 메서드 내에서 콜백 함수를 사용할 때 주의가 필요하다.
this가 의도한 대로 동작하지 않는 경우 예시
const user = {
name: 'fabric0de',
printName: function() {
console.log(`사용자 이름: ${this.name}`);
setTimeout(function() {
console.log(`콜백에서의 이름: ${this.name}`);
}, 1000);
}
};
user.printName();
//실행 결과
사용자 이름: fabric0de
콜백에서의 이름: undefined
위 코드에서 user.printName() 메서드의 첫 번째 console.log는 this가 user 객체를 가리키므로 fabric0de가 출력된다. 그러나 setTimeout 내의 콜백 함수에서는 전역 컨텍스트에서 호출되기 때문에 this가 전역 객체(window 또는 global 객체)를 가리킨다. 그 결과 this.name은 undefined가 된다.
이러한 문제는 bind() 메서드를 사용하거나 화살표 함수(Arrow Function)를 사용하여 해결할 수 있다.
주로 사용되는 화살표 함수의 예시를 살펴보자.
화살표 함수를 사용하면 this가 메서드가 정의된 렉시컬 스코프에서 고정되기 때문에 콜백 함수 내부에서도 this가 여전히 user 객체를 가리키게 된다.
const user = {
name: 'fabri0de',
printName: function() {
console.log(`사용자 이름: ${this.name}`);
setTimeout(() => {
console.log(`콜백에서의 이름: ${this.name}`); // 여기서 this는 user 객체를 가리킴
}, 1000);
}
};
user.printName();
사용자 이름: fabri0de
콜백에서의 이름: fabri0de
참고 문헌
'Frontend > JavaScript' 카테고리의 다른 글
JS 스코프 개념 (0) | 2024.09.11 |
---|---|
JS Promise란? (0) | 2024.09.09 |
JS 순수함수 (0) | 2024.09.04 |
JS 엔진의 구조와 작동원리 (JS 런타임 환경까지) (0) | 2024.08.16 |
JS 동기/비동기 처리 - 콜백부터 async/await (0) | 2024.08.14 |