1. 개요
비동기 처리는 콜백 → Promise → async/await 순으로 겉모습만 바뀐 게 아니다. 각 단계가 실제로 무슨 문제를 풀었는지 보면 async/await를 그냥 “예쁜 문법”으로만 알고 쓰다가 실수하는 걸 줄일 수 있다. await가 실행을 “멈추는” 것처럼 보이지만 실제로 스레드를 막지 않는다는 것도 짚는다.
2. 핵심 내용
2-1. 콜백 지옥
비동기 작업이 끝나면 콜백을 호출하는 방식은 작업을 순서대로 여러 개 엮을 때 중첩이 깊어진다.
getUser(id, (user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
console.log(comments);
}, onError);
}, onError);
}, onError);에러 처리도 콜백마다 따로 넘겨야 한다. 순서를 바꾸거나 하나를 건너뛰려면 코드 구조 자체를 다시 짜야 한다.
2-2. Promise 가 푼 것: 체이닝과 에러 전파
Promise 는 “나중에 값이 온다”를 하나의 객체로 표현한다. .then으로 이어 붙이면 중첩 대신 평평한 체인이 된다.
getUser(id)
.then((user) => getPosts(user.id))
.then((posts) => getComments(posts[0].id))
.then((comments) => console.log(comments))
.catch((err) => console.error(err));에러는 체인 어디서 터지든 가장 가까운 .catch로 전파된다. 콜백마다 에러 인자를 따로 챙길 필요가 없어졌다.
“나중에 값이 온다”를 객체 하나로 공유하는 이 성질은 스크립트 로딩에도 그대로 쓰인다. 위젯이 여러 개 떠도 외부 스크립트 태그는 한 번만 만들고, 그 로딩 결과를 모듈 수준 Promise 하나로 여러 호출부가 함께 기다리게 하는 패턴이 Turnstile 위젯 실전 체크리스트에 나온다.
2-3. async/await 가 푼 것: 동기 코드처럼 읽기
Promise 체이닝도 콜백보다는 낫지만 여전히 .then 함수 안에 로직을 쪼개 넣어야 한다. async/await는 Promise 체인을 동기 코드처럼 위에서 아래로 읽히게 해준다.
async function loadComments(id) {
try {
const user = await getUser(id);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
return comments;
} catch (err) {
console.error(err);
}
}try/catch로 동기 코드와 똑같이 에러를 잡을 수 있다. 사실 이건 Promise 위에 얹은 문법일 뿐이다. async 함수는 항상 Promise를 반환하고, await는 .then을 걸어주는 것과 같다.
2-4. await 가 실제로 하는 일: 스레드를 막지 않는다
await를 보면 “그 줄에서 멈춘다”고 생각하기 쉽지만, 정확히는 그 함수의 나머지 실행을 마이크로태스크로 미루고 콜스택에서 빠져나온다. 스레드는 비어서 다른 코드(다른 이벤트 핸들러, 다른 타이머)가 그 사이에 실행될 수 있다. Node.js 서버가 await db.query(...)를 하는 동안에도 다른 요청을 처리할 수 있는 이유가 이거다. 언어 차원에서 스레드를 새로 만들거나 블로킹하는 게 아니다.
async function f() {
console.log("start");
await Promise.resolve();
console.log("end");
}
f();
console.log("sync");
// 출력: start, sync, endawait 이후 코드(end)는 지금 실행 중인 동기 코드(sync)보다 늦게 돈다. 서버 응답을 받아 JSON 봉투를 벗기는 코드를 짤 때도 이 순서를 착각하면 실수가 난다 — fetch(...).then(r => r.json())에서 .json()도 별도의 Promise라는 걸 깜빡하면 언래핑이 한 박자 어긋난다. 실제로 이 종류의 실수가 어떻게 장애로 이어졌는지는 응답 봉투는 어디서 벗기나에 정리해뒀다.
3. 마무리
요약
- 콜백은 중첩과 에러 전파가 문제였다, Promise는 체이닝으로 그걸 평평하게 만들었다
async/await는 Promise 체인을 동기 코드처럼 읽히게 하는 문법이다async함수는 항상 Promise를 반환한다await는 스레드를 막지 않는다, 나머지 실행을 마이크로태스크로 미루고 콜스택을 비운다
다음은 동등 비교와 삼중 등호 이다.