setTimeout vs Promise — 실행 순서를 예측하는 가장 중요한 연습
setTimeout과 Promise.then이 함께 있을 때 어떤 순서로 실행되는지 예제로 익힌다. 마이크로태스크와 태스크의 우선순위를 손으로 예측하는 연습용 글이다.
Seobway · · 10분
이 시리즈 구성
| 포스트 | 내용 |
|---|---|
| 로드맵 인덱스 → | 01~19 전체 학습 경로 |
| 01-1. JS 이벤트 루프와 비동기 → | 콜스택, 큐, 마이크로태스크 |
| 01-2. setTimeout vs Promise → | 비동기 실행 순서 예측 |
| 01-3. React 단방향 데이터 흐름 → | props/state, state 끌어올리기 |
| 01-4. controlled vs uncontrolled → | React 폼 설계 |
| 01-5. TypeScript 타입 시스템 기초 → | any, unknown, union, narrowing |
왜 이 문제를 따로 연습해야 하는가
이벤트 루프를 글로 이해했다고 해서, 실제 코드의 실행 순서를 바로 예측할 수 있는 것은 아니다.
특히 많은 사람이 여기서 헷갈린다.
setTimeout(..., 0)이면 바로 실행되는가Promise.then()은 언제 끼어드는가- Promise 안의
setTimeout과 setTimeout 안의 Promise는 누가 먼저인가
이 글은 그 감각을 짧은 예제로 잡는 데 집중한다.
규칙 하나 먼저
먼저 이 규칙만 고정해 두자.
- 동기 코드를 먼저 끝낸다
- 콜스택이 비면 마이크로태스크를 먼저 모두 처리한다
- 그다음 태스크를 처리한다
여기서 보통
Promise.then,catch,finally→ 마이크로태스크setTimeout, DOM 이벤트 → 태스크
로 보면 된다.
예제 1 — 가장 기본
console.log('A')
setTimeout(() => {
console.log('B')
}, 0)
Promise.resolve().then(() => {
console.log('C')
})
console.log('D')
정답은 A, D, C, B다.
이유:
A,D는 동기 코드라서 먼저 실행- Promise 콜백
C는 마이크로태스크 - 타이머 콜백
B는 태스크
예제 2 — Promise 안에 setTimeout
console.log('1')
Promise.resolve().then(() => {
console.log('2')
setTimeout(() => console.log('3'), 0)
})
setTimeout(() => console.log('4'), 0)
console.log('5')
정답은 1, 5, 2, 4, 3이다.
왜 그런가:
- 동기 코드
1,5 - 마이크로태스크
2 - 그 안에서 새
setTimeout이 등록됨 - 태스크 큐에는 먼저 등록된
4, 그다음3
즉 태스크끼리는 먼저 들어온 것이 먼저 실행된다.
예제 3 — setTimeout 안에 Promise
console.log('a')
setTimeout(() => {
console.log('b')
Promise.resolve().then(() => console.log('c'))
}, 0)
setTimeout(() => console.log('d'), 0)
console.log('e')
정답은 a, e, b, c, d다.
핵심은 첫 번째 타이머 콜백 안에서 Promise.then()이 등록되면, 그 타이머 콜백이 끝난 직후 마이크로태스크인 c를 먼저 처리한다는 점이다.
%% desc: 첫 번째 setTimeout 콜백 안에서 생성된 Promise.then은 다음 태스크보다 먼저 실행된다
flowchart TD
S["동기 코드 실행"] --> T1["첫 번째 setTimeout 콜백"]
T1 --> M["Promise.then 등록"]
M --> C["현재 태스크 종료 직후\n마이크로태스크 실행"]
C --> T2["두 번째 setTimeout 콜백"]
예제 4 — 마이크로태스크는 모두 비운다
console.log('start')
Promise.resolve().then(() => {
console.log('m1')
Promise.resolve().then(() => console.log('m2'))
})
setTimeout(() => console.log('t1'), 0)
정답은 start, m1, m2, t1이다.
마이크로태스크를 하나만 처리하고 끝내는 것이 아니라, 큐가 빌 때까지 계속 처리한다.
실전에서 어떻게 써먹는가
이 연습은 단순 콘솔 문제 풀이가 아니다.
- React 이벤트 핸들러 뒤에 붙는 비동기 흐름 이해
- 테스트 코드에서 비동기 완료 시점 예측
- 타이머와 API 응답, 상태 업데이트가 섞일 때 디버깅
여기서 막히면 "렌더링 버그"처럼 보이는 문제도 사실은 이벤트 루프 이해 부족인 경우가 많다.
연습 방법
가장 좋은 연습은 다음 순서다.
- 코드를 본다
- 출력 순서를 종이에 적는다
- 이유를 "동기 → 마이크로태스크 → 태스크"로 설명한다
- 실제 실행 결과와 비교한다
설명을 못 하면 아직 외운 것이다. 설명이 되면 이해한 것이다.
조금 더 깊게 보기
왜 이 문제는 면접 단골인가
setTimeout과 Promise 실행 순서는 단순 암기 문제가 아니다. 이 문제는 지원자가 JavaScript를 "위에서 아래로만 실행되는 코드"로 보는지, 아니면 런타임과 큐까지 포함한 실행 모델로 이해하는지 확인하기 좋다.
개발자가 주의해야 할 포인트
실무에서는 이 순서가 테스트에서 많이 드러난다. 컴포넌트 테스트에서 클릭 직후 바로 값을 확인하면 아직 Promise 콜백이 반영되지 않았을 수 있다. 반대로 타이머 기반 UI는 fake timer를 쓰지 않으면 테스트가 불안정해진다.
디버깅 팁
비동기 순서가 헷갈릴 때는 로그에 숫자만 찍지 말고 "sync start", "microtask", "timer"처럼 큐의 성격을 같이 적는다. 등록 순서와 실행 우선순위는 다르다.
참고
관련 글
- JS 이벤트 루프와 비동기 큰 그림 →
- React 단방향 데이터 흐름 →
- Node.js · Bun · Deno 런타임 비교 →
- AI 웹개발자 로드맵 — Foundation 01~19 →
- javascript
- setTimeout
- promise
- microtask
- macrotask
- event-loop