콜백 함수는 다른 함수에 인자로 전달되어, 그 함수의 실행이 끝난 뒤 호출되는 함수를 말합니다.
자바스크립트에서는 함수도 일급 객체이기 때문에, 함수를 변수처럼 전달하거나 인자로 넘길 수 있습니다.콜백 함수는 주로 비동기 작업이 완료된 후 실행해야 할 로직을 정의할 때 사용됩니다.
예를 들어,setTimeout()의 두 번째 인자처럼 "일정 시간이 지난 뒤 실행될 코드"를 콜백 함수로 전달합니다.예를 들어 데이터를 요청한 뒤 결과를 처리하는 상황을 생각해볼 수 있습니다.
서버 요청이 끝난 후 실행할 함수를 콜백으로 넘기면, 요청이 완료된 시점에 자동으로 호출되어 흐름을 제어할 수 있습니다.다만, 콜백 함수를 중첩해서 많이 사용하면 콜백 지옥이 발생할 수 있습니다.
따라서 최근에는 Promise나 async/await 문법을 사용해 가독성을 높이는 방식으로 대체하는 경우가 많습니다.
콜백 함수란, 다른 함수의 인자로 전달되어, 그 함수의 내부에서 실행되는 함수를 말한다.
자바스크립트에서는 함수가 일급 객체(First-class Object)이기 때문에
이러한 특징 덕분에 콜백 함수를 자유롭게 사용할 수 있다.
콜백 함수는 호출 주체가 자신이 아닌 다른 함수라는 점이 핵심이다.
즉, "내가 직접 실행하지 않고, 특정 조건이 충족되면 다른 함수가 실행시킨다"는 구조이다.
function greeting(name) {
console.log(`Hello, ${name}!`);
}
function processUserInput(callback) {
const name = "현진";
callback(name); // 인자로 받은 함수를 여기서 실행
}
processUserInput(greeting); // Hello, 현진!
위 코드에서 greeting 함수는 콜백 함수이다.
→ processUserInput() 함수에 인자로 전달되고, 내부에서 호출되기 때문이다.
🔍 실행 흐름
1.processUserInput호출
2.callback(name)실행
3.greeting("현진")호출 →Hello, 현진!출력
자바스크립트는 싱글 스레드 기반 언어이므로, 한 번에 하나의 작업만 수행할 수 있다.
이때 오래 걸리는 작업(예: 네트워크 요청, 타이머 등)을 비동기 처리로 분리할 때 콜백이 자주 사용된다.
console.log("1");
setTimeout(() => {
console.log("2 (1초 뒤 실행)");
}, 1000);
console.log("3");
🔍 출력 순서: 1 → 3 → 2 (1초 뒤 실행)
setTimeout()의 첫 번째 인자로 전달된 함수는 콜백이며,
지정된 시간이 지난 후 비동기적으로 실행된다.
getUser(id, (user) => {
getPosts(user, (posts) => {
getComments(posts[0], (comments) => {
console.log(comments);
});
});
});콜백 지옥 문제를 해결하기 위해 ES6에서 Promise가 도입되었다.
이후 ES2017(ES8)에서는 async/await 문법이 추가되어, 비동기 코드를 마치 동기 코드처럼 작성할 수 있게 되었다.
Promise 예제
getUser(id)
.then(getPosts)
.then(getComments)
.then(console.log)
.catch(console.error);
async/await 예제
async function fetchData() {
try {
const user = await getUser(id);
const posts = await getPosts(user);
const comments = await getComments(posts[0]);
console.log(comments);
} catch (err) {
console.error(err);
}
}
💡 비동기 코드란?
- 자바스크립트의 비동기 코드(asynchronous code)는 보통 결과가 바로 나오지 않고 나중에 실행되는 코드이다.
- 비동기 코드의 예시
- 서버에서 데이터를 가져오는
fetch()setTimeout()같은 타이머- 이벤트 처리(
addEventListener)- 비동기 코드는 실행 순서가 코드 작성 순서와 다르게 동작할 수 있다.
💡 기존 비동기 처리 방식: 콜백 & Promise
콜백이나
then()체이닝 방식에서는 중첩되거나 계속 이어지는 형태가 생긴다.fetchUser() .then(user => fetchPosts(user)) .then(posts => fetchComments(posts[0])) .then(console.log) .catch(console.error);이 코드의 실행 순서는 비동기적이지만, 눈으로 보기에는 중첩된 체인 구조이다.
💡 async/await의 의도
async/await문법은 위 코드를 다음처럼 작성할 수 있게 해준다.async function getData() { const user = await fetchUser(); const posts = await fetchPosts(user); const comments = await fetchComments(posts[0]); console.log(comments); }이건 실제로는 비동기 처리(=
await가 완료될 때까지 다음 줄을 잠시 멈춤)지만,
코드의 작성 형태와 흐름이 동기 코드처럼 보인다는 의미이다.즉, "비동기 코드를 동기 코드처럼 작성한다"는 말은
"비동기 작업을 마치 일반 순차 코드처럼 위에서 아래로 자연스럽게 작성하고 읽을 수 있다"는 뜻이지,
단순히 "한 줄씩 쓴다"는 문장 형태를 의미하는 건 아니다.
타이머: setTimeout
console.log("A");
const timerId = setTimeout(function onTimeout() {
console.log("B (1초 뒤 실행)");
}, 1000);
console.log("C");
- 콜백 함수:
onTimeout(첫 번째 인자)- 코드 설명
"A"출력setTimeout이 콜백을 예약만 하고 즉시 리턴"C"출력- 1초 뒤, 이벤트 루프가 콜백 큐에서
onTimeout을 꺼내 실행 →"B"출력- 실제 출력 순서: A → C → B
- 주의/팁
- 타이머가 필요 없어지면
clearTimeout(timerId)로 취소한다.- 지연 시간이
0이어도 콜백은 다음 틱에 실행된다. (즉시 실행 아님)➡️ 실제 출력 순서: A → C → B (지연 0ms)console.log("A"); setTimeout(function onTimeout() { console.log("B (지연 0ms)"); }, 0); console.log("C");
이벤트 핸들러: addEventListener
<button id="buy">Buy</button>
const $btn = document.getElementById("buy");
function onClick(event) {
console.log("clicked!", event.target.id); // "buy"
}
$btn.addEventListener("click", onClick, { once: true });
- 콜백 함수:
onClick- 코드 설명
- 사용자가 버튼을 클릭하는 순간, 브라우저가
onClick콜백을 호출event객체가 첫 번째 인자로 전달됨 (event.target,event.currentTarget등 사용 가능)- 옵션
{ once: true }이면 한 번만 실행되고 자동으로 제거된다.- 주의/팁
- 이름 있는 함수로 등록하면 제거가 쉽다. (익명 함수는 제거 어려움)
- 전통 함수식(
function onClick() {})에서는this === event.currentTarget- 화살표 함수에서는
this가 상위 스코프를 가리킴
fetch + .then 콜백fetch("/api/users/1") // 1) 요청 보내고 즉시 Promise 반환
.then(function onResponse(res) { // 2) 응답 수신 시 호출될 콜백
if (!res.ok) throw new Error(res.statusText);
return res.json(); // 3) JSON 파싱도 비동기 → Promise 반환
})
.then(function onData(data) { // 4) JSON 파싱 완료 시 호출될 콜백
console.log("user:", data);
})
.catch(function onError(err) { // 5) 위 단계에서의 에러를 처리
console.error("request failed:", err.message);
});
- 콜백 함수들:
onResponse,onData,onError- 코드 설명
fetch가 즉시 Promise를 반환 (네트워크는 백그라운드 진행)- 응답이 오면
onResponse콜백 실행res.json()은 또 다른 Promise- 파싱 끝나면
onData콜백 실행- 네트워크 오류/
throw는catch콜백으로 전달- 주의/팁
fetch는 HTTP 4xx/5xx도 reject하지 않는다.
따라서 직접res.ok로 상태 확인 후 에러를 던져야 한다.- 체이닝에서 반환값을 꼭 리턴헤야 다음
.then이 값을 받는다.- 같은 로직을
async/await으로 쓰면 가독성이 올라간다.async function loadUser() { try { const res = await fetch("/api/users/1"); if (!res.ok) throw new Error(res.statusText); const data = await res.json(); console.log("user:", data); } catch (e) { console.error("request failed:", e.message); } } loadUser();
💡 "즉시 Promise를 반환한다"의 의미
fetch(url)을 호출하는 그 순간, 브라우저는
- 네트워크 요청을 시작하고(백그라운드에서 진행)
- 곧바로(다 끝날 때까지 기다리지 않고)
Promise객체를 돌려준다.즉,
fetch는 결과(응답)이 아니라, 나중에 결과를 담을 상자(=Promise)를 즉시 건네준다는 뜻이다.
그 상자는 처음에는 pending(보류) 상태이고, 요청이 끝나면 fulfilled/rejected로 바뀐다.💡 왜 "즉시"가 핵심인가?
- 메인 스레드가 막히지 않음
- 요청이 끝날 때까지 코드가 멈추지 않고, 다음 줄이 바로 실행된다.
- 콜백/체이닝을 즉시 연결 가능
- 응답이 오기 전이라도
then/catch/finally를 바로 붙여둘 수 있다.- 요청이 끝나는 순간, 붙여둔 콜백이 실행된다.
- 요청은 이미 시작됨
fetch는 호출과 동시에 요청을 보낸다.- 즉, Promise는 대기표이고, 네트워크는 이미 출발한 것이다.
💡 타임라인으로 보기
console.log("1"); const p = fetch("/api/users/1"); // 여기서 요청 시작 + Promise 즉시 반환 (pending) console.log("2", p); // Promise {<pending>} 같은 모습 p.then(() => console.log("3 응답 처리")); // 응답이 오르면 나중에 실행될 콜백 등록 console.log("4");
- 출력: 1 → 2 (pending) → 4 → 3
- 포인트
fetch호출 직후p를 찍으면 대부분pendingthen에 넘긴 콜백은 응답이 도착하고 Promise가 해결된 뒤 마이크로태스크 큐에서 실행된다.💡 직관 비유
- 택배 비유
fetch는 택배를 즉시 접수하고, 고객에게 운송장 번호(=Promise)를 바로 준다.- 물건(=응답)은 아직 안 왔지만, 운송장 번호(=약속)를 받았으니 도착 후에 할 일(=
then콜백)을 미리 등록해 둘 수 있다.- 영화 예매 비유
- 예매하면 바로 예약번호(=
Promise)를 받는다.- 영화가 아직 시작하지 않았어도, 예약번호로 이후 절차를 진행할 수 있다.
💡
Promise와 콜백 실행 타이밍
fetch가 반환한Promise는 pending → fulfilled/rejected로 바뀐다.- 상태가 바뀌는 순간,
.then/.catch에 등록된 콜백이 마이크로태스크(microtask)로 실행된다.
⚠️setTimeout콜백은 태스크(task)라서 우선순위가 더 낮다.💡
await와의 관계// then 체이닝 fetch("/api").then(res => res.json()).then(data => { ... }); // async/await const res = await fetch("/api"); // 여기서민 기다리는 것처럼 보이는 거지, const data = await res.json(); // fetch 자체는 여전히 즉시 Promise를 반환함
await는 해당 줄에서만 "기다리는" 문법적 설탕이다. (함수 전체는 비동기)- 사실상 내부적으로는
Promise를 기다리는 것이고,fetch자체의 성격(즉시 Promise 반환)은 동일하다.
배열 메서드: Array.prototype.map
const prices = [10, 12, 15];
// 1) 단순 변환
const withTax = prices.map(function calc(price, index, array) {
return Math.round(price * 1.1);
});
console.log(withTax); // [11, 13, 17]
// 2) thisArg 사용 예시
const tax = { rate: 0.1 };
const withTax2 = prices.map(function (p) {
return Math.round(p * (1 + this.rate));
}, tax);
console.log(withTax2); // [11, 13, 17]
- 콜백 함수:
calc(각 요소마다 동기적으로 호출)- 코드 설명
map은 원본 배열을 변경하지 않고, 콜백의 반환값들로 새 배열을 만든다.- 콜백은
(value, index, array)서명으로 호출된다.- 두 번째 인자
thisArg로 콜백 내부의this를 지정할 수 있다.(전통 함수(함수 선언식, 함수 표현식) 사용 시)- 주의/팁
forEach와 달리map은 반환값을 모아 새로운 배열을 만든다는 점이 핵심이다.- 콜백이 비동기라면
map은 기다려주지 않는다.
비동기 변환이 필요하면await Promise.all(arr.amp(async ...))패턴을 사용해야 한다.
💡 "동기적으로"의 뜻
동기적(synchronous) 실행이란, 이전 작업이 끝나야 다음 작업이 실행되는 순차적 흐름을 의미한다.
즉, 콜백 함수 하나가 끝나야 그 다음 요소에 대한 콜백이 실행된다.const arr = [1, 2, 3]; arr.map((n) => console.log("처리 중:", n)); console.log("끝!");실행 결과
처리 중: 1 처리 중: 2 처리 중: 3 끝!➡️ 순서가 보장된다.
map은 첫 번째 요소의 콜백 실행이 끝난 후 두 번째 콜백 요소를 실행하고,
모든 요소를 다 처리한 뒤에야 다음 줄(console.log("끝!"))로 넘어간다.💡 비동기와 비교
만약 콜백 내부에서 비동기 코드를 썼다면, 그 비동기 로직은 나중에 실행된다.
const arr = [1, 2, 3]; arr.map((n) => setTimeout(() => console.log("처리 중:", n), 0)); console.log("끝!");실행 결과
끝! 처리 중: 1 처리 중: 2 처리 중: 3➡️ 여기에서는
setTimeout덕분에 콜백 내부 로직이 비동기적(async)으로 큐에 들어가 버리기 때문에
"map이 끝나기 전에" 다음 줄(console.log("끝!"))이 먼저 실행된다.즉,
map자체는 동기적으로 동작하지만- 콜백 내부에 비동기 로직이 있으면, 그 로직만 따로 나중에 실행된다.
💡 실행 타이밍을 단계별로 보면
map의 내부 동작은 다음과 같다.const arr = [10, 12, 15]; const result = arr.map(function calc(price, index, array) { console.log(index, "번째 콜백 시작"); const val = Math.round(price * 1.1); console.log(index, "번째 콜백 끝"); return val; }); console.log("모두 완료:", result);실행 순서
0번째 콜백 시작 0번째 콜백 끝 1번째 콜백 시작 1번째 콜백 끝 2번째 콜백 시작 2번째 콜백 끝 모두 완료: [11, 13, 17]➡️ 콜백이 한 번에 하나씩, 끝나야 다음으로 넘어가는 구조이다.
이게 바로 "동기적으로 호출한다"는 의미이다.