동기 vs 비동기 · Promise · async/await
JS/TS 코드에 도배되는 async/await가 뭔지, 이벤트 루프가 어떻게 일을 나누는지.
Claude가 만든 코드의 80%는 `await`, `.then()`, `async function`으로 가득하다. 이걸 모르면 '왜 undefined가 찍히지?' 패닉. 비동기의 기본만 잡으면 JS 에러 절반이 풀린다.
동기(sync)는 줄 서기, 비동기(async)는 번호표 뽑기다. 동기는 앞사람 주문 다 끝나야 다음 사람. 비동기는 주문 넣고 번호표 받은 뒤 자리에 앉음 → 완성되면 호출. API 호출·파일 읽기·DB 질의는 밀리초~초가 걸리므로 비동기로 처리해 그 사이에 다른 일을 한다.
// ❌ 동기처럼 생각하면 틀림
function getUser() {
const res = fetch("/api/me"); // Promise 반환, 데이터 아직 없음
return res.name; // undefined! res는 Promise임
}
// ✅ 비동기 정식 표현 — async/await
async function getUser() {
const res = await fetch("/api/me"); // 응답 대기
const data = await res.json(); // 본문 파싱 대기
return data.name; // 반환도 Promise에 감싸짐
}
// 호출하는 쪽도 async거나 .then()
const name = await getUser(); // 최상단에서도 (top-level await)
getUser().then(name => console.log(name));`await`는 '이 Promise가 풀릴 때까지 이 함수만 잠깐 멈춰라'. 스레드 전체가 멈추는 게 아니라 다른 일도 동시에 처리된다.
Promise는 '미래의 값에 대한 약속'이다. 셋 중 하나의 상태. pending(대기) → fulfilled(완료, 값 있음) 또는 rejected(실패, 에러 있음). `.then()`으로 성공을, `.catch()`로 실패를, `.finally()`로 정리를 연결.
| 문법 | 같은 의미 |
|---|---|
| `const x = await promise;` | `promise.then(x => ...)` |
| `try { await p; } catch(e) {}` | `p.catch(e => ...)` |
| `async function fn() { return 1 }` | `function fn() { return Promise.resolve(1) }` |
await를 줄줄이 쓰면 느려진다. 각 기다림이 순차. 서로 독립적이라면 Promise.all로 병렬 실행하면 총 시간이 가장 느린 하나만큼으로 줄어든다.
// ❌ 순차 — 총 3초 (1+1+1)
const a = await fetchA();
const b = await fetchB();
const c = await fetchC();
// ✅ 병렬 — 총 1초 (최악 하나만큼)
const [a, b, c] = await Promise.all([fetchA(), fetchB(), fetchC()]);
// 하나 실패해도 다른 결과를 받고 싶다면
const results = await Promise.allSettled([a, b, c]);
// [{status:"fulfilled",value:...}, {status:"rejected",reason:...}]Claude에게 "독립적인 fetch 3개를 Promise.all로 묶어줘"라고 요청하면 즉시 병렬화. 체감 속도 차이 큼.
Node.js·브라우저 JS는 싱글 스레드다. 어떻게 비동기가 될까? 이벤트 루프 덕분이다. JS는 한 줄씩 실행하다가 I/O가 필요하면 OS에 위임하고 바로 다음 코드로 넘어감. 결과가 오면 큐에 들어가 차례로 처리.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// 출력 순서?
// 1 → 4 → 3 → 2
//
// 이유:
// "1", "4" = 동기 스택 (최우선)
// "3" = 마이크로태스크 (Promise) — 동기 끝나고 먼저
// "2" = 매크로태스크 (setTimeout) — 마이크로 큐 비운 뒤마이크로태스크(Promise) > 매크로태스크(setTimeout/setInterval). 이 우선순위는 면접·실전 디버깅에서 꼭 나온다.
3대 함정. ① `forEach`는 `async`를 기다리지 않는다 → `for..of` 또는 `Promise.all(arr.map(...))`. ② `await`를 `try/catch` 없이 쓰면 reject 시 앱이 크래시 → 반드시 에러 처리. ③ `.then`과 `await` 섞어 쓰지 말 것 → 가독성 파괴.
// ❌ forEach는 async 못 기다림
items.forEach(async (item) => {
await process(item);
});
// 이 줄 다음에 오는 코드가 process 끝나기 전에 실행됨!
// ✅ 순차 처리
for (const item of items) {
await process(item);
}
// ✅ 병렬 처리
await Promise.all(items.map(process));
// ✅ 에러 처리
try {
const data = await fetchRisky();
} catch (err) {
console.error("실패:", err);
}Claude가 `forEach` 안에 `await`를 쓴 패턴을 생성했다면 보자마자 수정 요청 타이밍.
한 줄 요약: 느린 작업(API·파일·DB)은 전부 비동기. `async`로 선언, `await`로 기다림, 에러는 `try/catch`, 독립적이면 `Promise.all`로 병렬. `forEach`에 `await` 금지.
다음 코드의 출력 순서는?
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));
console.log('D');`await`는 현재 스레드 전체를 멈추어서 다른 코드도 실행되지 않는다.
독립적인 비동기 작업 3개를 병렬 실행해 결과를 배열로 받는 메서드는?