들어가며
자바스크립트에서 콜백 함수(callback function)는 너무 자주 등장하는 개념이라,
처음에는 당연하게 사용하지만 정확히 어떻게 동작하고 어떤 상황에서 쓰는지 깊이 있게 이해하는 게 중요합니다.
이 글에서는:
- 콜백 함수의 기본 개념
- 함수 인자로서의 역할
- 중첩 호출 (콜백 지옥)
- return, 실행 시점, 흐름의 이해
- 실무에서 콜백을 대체하는 구조 (Promise, async/await) 까지 전체 흐름을 쉽게 정리합니다.
1. 콜백 함수란?
**“다른 함수에 인자로 전달되는 함수”**를 콜백 함수라고 부릅니다.
function greet(name, callback) { //콜백함수를 쓰는 함수 greet를 정의
const message = `안녕하세요, ${name}`;
callback(message); //넘겨받은 함수를 실행하면서 전달받은값 혹은 내부처리값을 인자로 전달
}
greet("Tom", function(msg) { //해당 함수를 호출하면서, 함수 정의하면서 넘김
console.log(msg); // 출력: 안녕하세요, Tom
});
- greet()는 문자열을 만들고 callback(message)으로 콜백을 실행합니다
- function(msg)는 콜백 함수로, message를 넘겨받아 실행됩니다
- 함수 내부에 넣는 함수 (콜백함수)에 정의하는 인자는 내부함수에서 return 하는 값이 있을 경우 그 결과를 받는 자리로 약속되어 있습니다(콜백 내부에서 단순실행만 하고 결과를 돌려주지 않는 경우에는 쓰지 않아도 됨)
2. 콜백은 언제 호출되는가?
콜백은 단순히 "함수의 인자"일 뿐이고, 실행 시점은 넘겨받은 함수가 정합니다
function doSomething(callback) { //함수를 인자로 받는 doSomething함수 정의. 실행로직 시점 정의
console.log("1. 준비 완료");
callback(); // ← 여기서 직접 실행함
}
doSomething(function() { //정의한 doSomething함수를 호출하면서 콜백을 넘김
console.log("2. 콜백 실행됨");
});
📌 포인트: 함수 정의 시점이 아닌 호출되는 타이밍이 중요합니다.
3. 콜백의 매개변수와 return 구조
✅ 값을 넘기고 처리하기
function doSomething(callback) { // 함수를 위임받아 내부 처리 시기와 인자값을 정의하는 함수 (외부함수 doSomething)
const result = "🍎";
callback(result); // 값을 넘김
}
//실제 호출 하면서 전달하는 콜백함수를 정의하면서 시기는 외부함수에 맡기고 외부에서 전달받은 인자를 어떻게 가공할지를 결정함 (내부함수 콜백함수 익명함수)
doSomething(function(fruit) {
console.log("받은 값:", fruit); // 받은 값: 🍎
});
✅ return을 활용하는 경우
function doSomething(callback) { // 함수를 인자로 받아 처리 시기와 인자를 정하는 외부함수 정의
const result = "🍕";
return callback(result); // 콜백 결과를 리턴
}
// 실제 사용하면서 doSomething내부에서 받은 값을 어떻게 처리할지 결정하는 콜백함수를 정의해서 그 결과를 message로 받음
const message = doSomething(function(food) {
return `오늘은 ${food} 먹자!`;
});
console.log(message); // 오늘은 🍕 먹자!
- 콜백 함수 안에서 return을 하면, 그 결과를 바깥에서 받아올 수 있음
4. 콜백 지옥 (Callback Hell)
콜백을 중첩해 사용하다 보면 들여쓰기가 깊어지고 코드 흐름이 눈에 안 보이는 현상이 생깁니다.
doSomething(function(result1) { //result1은 doSomething내부에서 받은 인자값임
doSomethingElse(result1, function(result2) { //result2는 doSomething 안에 콜백1에서 처리해서 넘겨주는 인자값임
doMore(result2, function(result3) { //result3은 doSomething 안에 콜백1에서 처리해서 넘겨준 인자값을 콜백2에서 처리해서 넘겨준 인자값임
console.log("마지막 결과:", result3);
});
});
});
- 들여쓰기가 깊어지고 디버깅이 어려움
- 비동기 작업이 많을수록 이런 패턴이 자주 발생함
5. 콜백을 대체하는 방식: Promise / async-await
콜백 지옥을 피하기 위해 등장한 패턴이 바로 Promise, 그리고 그 위에서 작동하는 async/await입니다.
// Promise
getUser() //getUser메서드는 Promise객체를 반환하는 함수로 정의가 되어있기 때문에 .then().catch()구조를 적요알 수 있게 됨
.then(user => getPosts(user))
.then(posts => show(posts))
.catch(err => console.error(err));
// async/await
async function run() { //Promese와 달리 체이닝구조가 아니라 위에서 아래로 순차적 흐름으로 작성하는 구조
try {
const user = await getUser();
const posts = await getPosts(user);
show(posts);
} catch (err) {
console.error(err);
}
}
✅ Promise vs async/await – 장단점 비교
항목 | Promise | async/await |
문법 구조 | .then() 체인 방식 | 동기 코드처럼 await 사용 |
에러 처리 | .catch() 사용 | try/catch 블록 사용 |
가독성 | 단순할 땐 괜찮지만 중첩되면 헷갈릴 수 있음 | 더 읽기 쉬운 순차적 흐름 |
디버깅 | then 체인 중간에서 디버깅 어려움 | 디버깅하기 쉬움 (중단점 사용 용이) |
병렬 처리 | .then() 체인 혹은 Promise.all() | Promise.all 같이 사용 가능 |
학습 곡선 | 초보자에게 더 익숙할 수 있음 | async/await은 Promise 기반 개념 이해 필요 |
🔍 Promise 장점
- 체이닝 구조가 명확할 때는 간결
- 병렬 처리(Promise.all 등) 표현이 직관적
- 오래된 코드에서도 지원 (ES6)
🔍 Promise 단점
- .then() 안에서 또 .then()을 쓰면 가독성 떨어짐
- 에러가 어디서 났는지 추적 어려움
🔍 async/await 장점
- 동기 코드처럼 순차적으로 읽혀서 가독성 좋음
- try/catch로 예외 흐름을 자연스럽게 제어
- 디버깅 도구에서 한 줄씩 실행 확인 가능
🔍 async/await 단점
- 내부적으로는 Promise 기반이므로 await 전에 반드시 Promise를 반환해야 함
- 병렬 처리하려면 여전히 Promise.all()을 함께 써야 함
📌 추천 기준:
상황 | 추천 방식 |
간단한 체이닝 또는 라이브러리 내부 처리 | Promise |
비동기 흐름이 길고 순차적으로 진행되는 로직 | async/await |
>>>
토글버튼
"async/await은 Promise 위에서 작동한다" 는 말의 의미는 async/await 가 Promise를 기반으로 한 추상화된 문법이라는 뜻
🔁 콜백 → Promise → async/await
- 콜백 함수(callback)
전통적인 비동기 처리 방식.
→ 단점: 중첩되면 콜백 지옥(callback hell) 발생 - Promise
콜백을 함수 밖으로 빼내서 .then(), .catch()로 연결 가능하게 만든 비동기 처리 방식
→ 콜백보다 가독성이 좋고, 에러 처리도 용이 - async/await
Promise를 더 직관적이고 동기 코드처럼 작성할 수 있게 만든 문법적 설탕(syntactic sugar)
→ 실제로 내부에서는 Promise가 동작 중
🎯 즉, "async/await은 Promise 위에서 작동한다"는 건?
- await은 Promise가 resolve될 때까지 기다리는 문법
- async 함수는 자동으로 Promise를 반환
- 내부적으로는 결국 Promise 체이닝이 일어남
예제 비교:
// Promise 기반
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve("🍎 데이터 도착!");
}, 1000);
});
}
fetchData().then((res) => {
console.log(res); // 🍎 데이터 도착!
});
// async/await 기반 (Promise 위에서 작동)
async function main() {
const res = await fetchData(); // fetchData는 Promise를 리턴해야 함
console.log(res); // 🍎 데이터 도착!
}
main();
요약
"async/await은 Promise 위에서 작동한다"
→ 내부적으로는 await가 Promise의 .then()처럼 동작하며,
→ async 함수는 항상 Promise를 반환해요.
그래서 Promise 없이 async/await는 존재할 수 없어요.
async/await는 Promise를 더 읽기 쉬운 문법으로 감싼 것이라고 보면 됩니다.
6. 콜백 함수 관련 잘 나오는 질문 정리
❓ result1은 누가 넘기는 건가요?
→ callback(result1)처럼 외부함수(doSomething) 내부에서 넘기는 값입니다.
콜백 안의 result1은 기다리는 자리, 실제로 값을 넣어주는 건 외부 함수입니다.
❓ 콜백 함수에 return을 꼭 써야 하나요?
→ 꼭 필요하지는 않아요.
- 값을 가공해서 외부로 전달하고 싶다면 return 사용
- 단순히 실행만 한다면 return 없이도 OK
❓ 함수 안에 함수를 왜 중첩하나요?
→ 작업의 순서, 즉 앞의 결과값을 받아서 다음 작업을 하고 싶을 때 사용합니다.
→ 콜백 체이닝이라고도 부릅니다.
마치며
콜백 함수는 자바스크립트의 핵심 개념입니다.
단순히 "함수를 넘긴다" 수준을 넘어서:
- 언제 실행되는지 (실행 타이밍)
- 어떤 값을 주고받는지 (매개변수 전달)
- 어떻게 순서를 제어하는지 (비동기 흐름) 을 이해하면 실제 코드를 더 명확하게 읽고 쓸 수 있습니다.
콜백은 단순하지만, 깊게 파고들면 자바스크립트 실행 흐름과 함수형 프로그래밍의 기반이 됩니다.
이제는 콜백을 왜 쓰고, 언제 피해야 하는지까지 구분해서 사용할 수 있는 단계입니다.
📂 다음 글에서는:
- 비동기와 이벤트 루프의 관계
- 콜백 큐와 마이크로태스크 큐
- setTimeout vs Promise의 실행 우선순위
등을 이어서 다루겠습니다.
'language > javascript' 카테고리의 다른 글
setTimeout vs Promise 실행 순서 비교와 마이크로태스크 큐 (0) | 2025.04.26 |
---|---|
함수 선언 vs 표현식, this 바인딩 완벽 정리 (call, apply, bind 포함) (1) | 2025.04.21 |
실행 컨텍스트와 호이스팅 – JavaScript 실행 구조의 핵심 이해하기 (1) | 2025.04.21 |
자바스크립트의 이벤트 루프와 태스크 큐 완전 이해하기 (0) | 2025.04.20 |
프로미스(Promise), async/await – JavaScript의 비동기 처리 구조 이해하기 (0) | 2025.04.20 |
댓글