JS 5.2 · 5부. 비동기 프로그래밍

콜백과 콜백 지옥

콜백으로 순서를 만들고, 그 방식의 한계를 봅니다.

예상 학습 시간 14분 실행 단추가 있는 예제는 고쳐서 실행해 볼 수 있습니다 난이도 중급
개념 설명

끝난 뒤에 할 일을 넘깁니다

앞 단원에서 맡긴 일의 결과를 바로 아래에서 읽을 수 없다는 것을 봤습니다. 그러면 언제 읽어야 할까요.

끝났을 때 부를 함수를 함께 넘기면 됩니다. 2.6 에서 배운 콜백입니다. 오래 걸리는 일이 끝나면 그쪽에서 이 함수를 불러 줍니다.

최소 예제 · 편집 가능

끝나면 알려 주기

script.js
function loadUser(id, callback) {
	setTimeout(() => {
		callback({ id: id, name: "홍길동" });
	}, 100);
}

loadUser(1, user => {
	console.log(`받았습니다: ${user.name}`);
});

console.log("요청했습니다");
출력
요청했습니다 받았습니다: 홍길동

loadUser 는 값을 돌려주지 않습니다. 대신 끝났을 때 넘겨받은 함수를 부릅니다. 그래서 결과를 쓰는 코드가 그 함수 안에 들어갑니다.

상세 사용법

차례대로 해야 한다면

앞의 결과가 있어야 다음을 할 수 있다면, 콜백 안에서 다시 부릅니다.

script.js
function step(name, callback) {
	setTimeout(() => {
		console.log(name);
		callback();
	}, 100);
}

step("첫째", () => {
	step("둘째", () => {
		step("셋째", () => {
			console.log("끝");
		});
	});
});
출력
첫째 둘째 셋째 끝

동작은 합니다. 다만 단계가 늘어날수록 오른쪽으로 계속 밀립니다. 이것을 콜백 지옥 이라고 부릅니다 — 중첩이 깊어져 어디가 어디인지 알기 어려워지는 모양을 가리키는 말입니다.

상세 사용법

오류는 어떻게 알리나

콜백 방식에는 오류를 알릴 자리가 정해져 있지 않습니다. 그래서 첫 인자를 오류로 두는 약속이 널리 쓰였습니다.

script.js
function loadUser(id, callback) {
	setTimeout(() => {
		if (id <= 0) {
			callback(new Error("잘못된 번호입니다"), null);
			return;
		}
		callback(null, { id: id, name: "홍길동" });
	}, 100);
}

loadUser(1, (error, user) => {
	if (error) {
		console.log("실패:", error.message);
		return;
	}
	console.log("성공:", user.name);
});

loadUser(0, (error, user) => {
	if (error) {
		console.log("실패:", error.message);
		return;
	}
	console.log("성공:", user.name);
});
출력
성공: 홍길동 실패: 잘못된 번호입니다

단계마다 이 검사를 되풀이해야 합니다. 중첩이 깊어지면 오류 처리도 그만큼 흩어져, 한 군데를 빠뜨리기 쉽습니다.

상세 사용법

콜백이 나쁜 것은 아닙니다

지금까지 사용해 온 forEach · map · addEventListener 도 모두 콜백입니다. 그것들은 아무 문제가 없습니다.

문제가 되는 것은 오래 걸리는 일을 여러 단계로 이어야 할 때입니다. 그 한 가지를 해결하려고 나온 것이 다음 단원의 Promise 입니다.

흔한 실수

처음에 자주 걸리는 것

1. 결과를 return 으로 받으려 합니다

콜백 안의 return 은 그 콜백을 끝낼 뿐입니다. 바깥 함수의 반환값이 아닙니다(2.6).

function load() {
    setTimeout(() => { return "값"; }, 100);
}
console.log(load());   // undefined 입니다
2. 오류 검사를 빠뜨립니다

첫 인자를 확인하지 않으면 오류가 났는데도 그냥 지나갑니다. 그다음 줄에서 엉뚱한 오류가 나 원인을 찾기 어렵습니다.

3. 콜백을 두 번 부릅니다

오류를 알린 뒤 return 을 빠뜨리면 아래 줄이 이어져 콜백이 두 번 불립니다.

실습 문제

직접 해보기

1. 두 단계로 잇기 난이도 하

0.1초 뒤 "준비" 를 출력하고, 그것이 끝나면 다시 0.1초 뒤 "시작" 을 출력하세요.

function later(message, callback) { setTimeout(() => { console.log(message); callback(); }, 100); } later("준비", () => { later("시작", () => {}); });
2. 오류를 알리는 함수 난이도 중

숫자를 받아 0.1초 뒤 두 배를 돌려주되, 숫자가 아니면 오류를 알리는 함수를 만드세요. 첫 인자를 오류로 두는 약속을 따릅니다.

function twice(value, callback) { setTimeout(() => { if (typeof value !== "number") { callback(new Error("숫자가 아닙니다"), null); return; } callback(null, value * 2); }, 100); } twice(5, (e, v) => console.log(e ? e.message : v)); twice("다섯", (e, v) => console.log(e ? e.message : v));
요약

이 단원의 정리

  • 오래 걸리는 일에는 끝났을 때 부를 함수를 함께 넘깁니다.
  • 결과를 쓰는 코드가 그 함수 안에 들어갑니다.
  • 차례대로 이어야 하면 콜백 안에서 다시 부릅니다 — 깊어지면 콜백 지옥입니다.
  • 오류는 첫 인자로 알리는 약속이 널리 쓰였습니다.
  • 콜백 자체가 나쁜 것은 아닙니다. 여러 단계를 잇는 자리만 문제입니다.