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

왜 이 문제를 따로 연습해야 하는가

이벤트 루프를 글로 이해했다고 해서, 실제 코드의 실행 순서를 바로 예측할 수 있는 것은 아니다.

특히 많은 사람이 여기서 헷갈린다.

이 글은 그 감각을 짧은 예제로 잡는 데 집중한다.


규칙 하나 먼저

먼저 이 규칙만 고정해 두자.

  1. 동기 코드를 먼저 끝낸다
  2. 콜스택이 비면 마이크로태스크를 먼저 모두 처리한다
  3. 그다음 태스크를 처리한다

여기서 보통

로 보면 된다.


예제 1 — 가장 기본

console.log('A')

setTimeout(() => {
  console.log('B')
}, 0)

Promise.resolve().then(() => {
  console.log('C')
})

console.log('D')

정답은 A, D, 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. 동기 코드 1, 5
  2. 마이크로태스크 2
  3. 그 안에서 새 setTimeout이 등록됨
  4. 태스크 큐에는 먼저 등록된 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이다.

마이크로태스크를 하나만 처리하고 끝내는 것이 아니라, 큐가 빌 때까지 계속 처리한다.


실전에서 어떻게 써먹는가

이 연습은 단순 콘솔 문제 풀이가 아니다.

여기서 막히면 "렌더링 버그"처럼 보이는 문제도 사실은 이벤트 루프 이해 부족인 경우가 많다.


연습 방법

가장 좋은 연습은 다음 순서다.

  1. 코드를 본다
  2. 출력 순서를 종이에 적는다
  3. 이유를 "동기 → 마이크로태스크 → 태스크"로 설명한다
  4. 실제 실행 결과와 비교한다

설명을 못 하면 아직 외운 것이다. 설명이 되면 이해한 것이다.

조금 더 깊게 보기

왜 이 문제는 면접 단골인가

setTimeoutPromise 실행 순서는 단순 암기 문제가 아니다. 이 문제는 지원자가 JavaScript를 "위에서 아래로만 실행되는 코드"로 보는지, 아니면 런타임과 큐까지 포함한 실행 모델로 이해하는지 확인하기 좋다.

개발자가 주의해야 할 포인트

실무에서는 이 순서가 테스트에서 많이 드러난다. 컴포넌트 테스트에서 클릭 직후 바로 값을 확인하면 아직 Promise 콜백이 반영되지 않았을 수 있다. 반대로 타이머 기반 UI는 fake timer를 쓰지 않으면 테스트가 불안정해진다.

디버깅 팁

비동기 순서가 헷갈릴 때는 로그에 숫자만 찍지 말고 "sync start", "microtask", "timer"처럼 큐의 성격을 같이 적는다. 등록 순서와 실행 우선순위는 다르다.


참고

  1. [1] MDN — In depth: Microtasks and the JavaScript runtime environment
  2. [2] MDN — Promise

관련 글