Promise 와 async/await
콜백 헬 탈출. then/catch 와 async/await, 그리고 Promise.all 패턴.
15편의 콜백 헬을 푸는 표준 도구가 Promise(ES2015) 와 그 문법적 설탕 async/await(ES2017). 16편은 두 도구의 핵심 사용법, 동시 실행 패턴(Promise.all/race/allSettled), 그리고 자주 만나는 함정까지.
Promise — 3가지 상태
// Promise 는 "미래의 값" 을 담은 객체
const p = new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) resolve("성공!");
else reject(new Error("실패"));
}, 1000);
});
// 상태
// pending → fulfilled (resolve 호출)
// pending → rejected (reject 호출)
// 한 번 settle 되면 다시 안 바뀜
then · catch · finally
p
.then(value => {
console.log("성공:", value);
return value.toUpperCase(); // 다음 then 에 전달
})
.then(upper => console.log(upper))
.catch(err => console.error(err)) // 어디서 reject 됐든 여기로
.finally(() => console.log("끝")); // 성공/실패 무관
async/await — 같은 일을 보기 좋게
// Promise 체이닝
function loadUser(id) {
return fetch(`/api/users/${id}`)
.then(res => res.json())
.then(user => {
return fetch(`/api/orders?userId=${user.id}`);
})
.then(res => res.json())
.then(orders => ({ user, orders })) // ← user 가 안 보임 (스코프)
.catch(err => console.error(err));
}
// async/await — 동기처럼 읽힘
async function loadUser(id) {
try {
const res1 = await fetch(`/api/users/${id}`);
const user = await res1.json();
const res2 = await fetch(`/api/orders?userId=${user.id}`);
const orders = await res2.json();
return { user, orders };
} catch (err) {
console.error(err);
}
}
한 줄. async 함수는 항상 Promise 를 반환합니다. 본문에서 return 한 값이 자동으로 wrap. throw 한 에러는 reject 가 됨. await 는 Promise 의 값을 꺼냄.
Promise.all — 동시 실행
// ❌ 순차 — 두 fetch 가 차례로 (느림)
const u = await fetch("/users").then(r => r.json());
const p = await fetch("/posts").then(r => r.json());
// 총 시간: 두 응답 시간의 합
// ✅ 병렬 — Promise.all
const [u, p] = await Promise.all([
fetch("/users").then(r => r.json()),
fetch("/posts").then(r => r.json()),
]);
// 총 시간: 두 응답 중 느린 쪽
Promise.all 의 함정. 하나라도 reject 되면 즉시 전체가 reject — 나머지 결과를 받을 수 없음. 일부 실패해도 나머지를 보고 싶으면 Promise.allSettled 사용.
Promise.allSettled · race · any
// allSettled — 모두 끝날 때까지
const results = await Promise.allSettled([p1, p2, p3]);
for (const r of results) {
if (r.status === "fulfilled") console.log(r.value);
else console.error(r.reason);
}
// race — 가장 먼저 settle (성공/실패 무관)
const winner = await Promise.race([
fetch("/data"),
new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), 5000)),
]);
// any (ES2021) — 가장 먼저 성공 (실패는 무시)
const fastest = await Promise.any([
fetch("https://mirror1.example.com"),
fetch("https://mirror2.example.com"),
fetch("https://mirror3.example.com"),
]);
// 모두 실패하면 AggregateError
에러 처리 — try/catch + .catch
// async 안에서는 try/catch
async function load() {
try {
const data = await fetchData();
return data;
} catch (err) {
console.error("로드 실패:", err);
return null;
}
}
// Promise.then 체인이면 .catch
fetchData()
.then(data => processData(data))
.catch(err => console.error(err));
// 둘 다 사용 (await 결과를 then 처럼)
const data = await fetchData().catch(err => null); // 실패 시 null
마이크로태스크 우선순위 (15편 다시)
console.log("1");
setTimeout(() => console.log("2"), 0); // 매크로
Promise.resolve().then(() => console.log("3")); // 마이크로
console.log("4");
// 출력: 1, 4, 3, 2 ← 동기 다 끝나고, 마이크로(3) → 매크로(2)
async + map 의 함정
// ❌ async + map — 결과는 Promise 배열
const results = users.map(async (u) => {
return await loadProfile(u.id);
});
console.log(results); // [Promise, Promise, ...] ← !!
// ✅ Promise.all 로 풀기
const profiles = await Promise.all(users.map(u => loadProfile(u.id)));
// ^ Promise 배열을 한 번에 await
// 순서 보장하면서 순차로
const profiles = [];
for (const u of users) {
profiles.push(await loadProfile(u.id));
}
매핑 패턴 한 표. ① 병렬 OK · 결과 배열 → Promise.all(arr.map(async ...)). ② 순차 (앞 결과로 다음 결정) → for of + await. ③ N 개 동시 제한 (rate limiting) → p-limit 같은 라이브러리.
top-level await (ES2022, ESM 한정)
// 모듈 최상위에서 직접 await — ESM 에서만
// app.mjs
const config = await loadConfig();
const db = await connect(config.dbUrl);
export { db };
npm 라이브러리는 가급적 사용 자제 — top-level await 가 모듈 로딩 시간에 영향을 줍니다. 앱 진입점·CLI 에서는 OK.
실전 패턴 — 재시도 + 타임아웃
async function withRetry<T>(fn, max = 3, delay = 1000) {
for (let i = 0; i < max; i++) {
try {
return await fn();
} catch (err) {
if (i === max - 1) throw err;
await new Promise(r => setTimeout(r, delay * 2 ** i)); // 지수 백오프
}
}
}
async function withTimeout(promise, ms) {
const t = new Promise((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms)
);
return Promise.race([promise, t]);
}
// 사용
const data = await withTimeout(
withRetry(() => fetch("/api").then(r => r.json())),
10000
);
안티패턴 5가지
1. async 함수에 await 안 씀. "왜 async 인지" 모름. 동기 함수로 충분하면 그렇게.
2. await 결과를 then 으로. 둘 섞으면 가독성 폭락. 한 함수 안에서는 하나만.
3. forEach + async. forEach 는 Promise 를 무시. for...of + await 또는 Promise.all + map.
4. catch 빠뜨림. 처리 안 된 reject 는 콘솔 경고만 — 사용자엔 무음.
5. fetch 결과 .ok 검사 안 함. fetch 는 4xx/5xx 응답에 reject 안 함. if (!res.ok) throw ... 직접.
17편 — fetch API · API 호출
fetch 의 모든 모양, headers, POST/JSON, AbortController, CORS 한 줄.
이전: 15편 비동기 입문 · 현재: 16편 (중급) · 다음 → 17편 fetch · 진행: 16/26