Frontend Development

setTimeout vs Promise.then vs queueMicrotask

Kun Woo Kim 2025. 9. 5. 10:37
728x90

브라우저/Node.js 런타임에서 비동기 코드의 실행 순서를 정확히 이해하는 것은 디버깅과 성능 최적화의 출발점입니다. 특히 마이크로태스크 큐는 렌더링 타이밍과 상태 일관성에 직접적인 영향을 주므로, 실무에서 정확한 모델을 갖추는 것이 중요합니다.


핵심 개념 정리

  • Call Stack: 자바스크립트가 함수를 동기적으로 실행하는 스택.
  • Web APIs(환경): 타이머, DOM, Fetch 등 비동기 작업이 대기하는 영역.
  • Task Queue(매크로태스크 큐): setTimeout, setInterval, setImmediate(Node) 등이 들어가는 큐.
  • Microtask Queue(마이크로태스크 큐): Promise.then/catch/finally, queueMicrotask, MutationObserver 등이 들어가는 큐.
  • Event Loop 규칙(중요):
    1) 콜스택이 빌 때까지 동기 코드 실행
    2) Microtask Queue를 “완전히” 비울 때까지 실행
    3) 그 다음 Task Queue에서 작업 1개를 실행(그리고 다시 2로)

요약하면, “마이크로태스크는 같은 틱에서 모두 비워진 뒤에야 다음 매크로태스크로 넘어간다”가 핵심입니다.


예제 코드

아래 코드는 test.js와 동일합니다.

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

console.log('B');

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

queueMicrotask(() => {
    console.log('D');
});

console.log('E');

기대 출력(브라우저 기준)

B
E
C
D
A

단계별 실행 타임라인

1) setTimeout(..., 0) 등록 → Web APIs로 타이머 위임 → 만료 후 Task Queue에 콜백(A) 대기

2) 동기 코드 실행: console.log('B')

3) Promise.resolve().then(...) → Microtask Queue에 콜백(C) 등록

4) queueMicrotask(...) → Microtask Queue에 콜백(D) 등록

5) 동기 코드 실행: console.log('E')

6) 콜스택이 비면 Event Loop가 Microtask Queue를 “모두” 비움 → CD

7) 다음 틱에서 Task Queue의 A 실행 → console.log('A')

정리: 동기(B, E) → 마이크로태스크(C, D) → 매크로태스크(A)

마이크로태스크 내부 순서는 “등록된 순서”입니다. 위 코드에서는 Promise.then이 먼저, queueMicrotask가 나중에 등록되어 CD 순으로 실행됩니다.


비교 표: 어떤 큐로 가는가, 언제 실행되는가

항목 setTimeout Promise.then queueMicrotask
큐 유형 Task(매크로태스크) Microtask Microtask
실행 시점 다음 틱 이후 현재 틱의 콜스택 종료 직후 현재 틱의 콜스택 종료 직후
순서 보장 동일 지연이면 상대적 FIFO(등록 순) FIFO(등록 순)
렌더링 영향 보통 렌더 후 실행 렌더 전 마이크로태스크가 모두 비워짐 렌더 전 마이크로태스크가 모두 비워짐
에러 전파 타이머 콜백에서 throw 시 비동기 에러 잡히지 않으면 전역 Unhandled Rejection 잡히지 않으면 전역 에러
취소 clearTimeout 불가(체인 취소는 별도 로직) 불가
대표 용도 지연 실행, 렌더 이후 작업 비동기 후크/체인 매우 짧은 후처리, 동 틱 내 정합 보장

실무 팁과 주의사항

  • 마이크로태스크 “고갈” 규칙을 이용해 일관성 보장
    • 동일 틱 내 후처리는 queueMicrotask가 가장 명확합니다.
  • 과도한 마이크로태스크 생성은 이벤트 루프 기아를 유발
    • 무한 재귀적 마이크로태스크는 렌더링을 막습니다. 반드시 종료 조건을 두세요.
  • Promise.then vs queueMicrotask
    • 둘 다 마이크로태스크이지만, 체인 작성과 에러 핸들링이 필요하면 Promise가 편리합니다. 아주 경량 후처리는 queueMicrotask가 적합합니다.
  • 브라우저 vs Node.js 차이 인지
    • Node.js에서는 process.nextTick이 마이크로태스크보다 우선 실행되어 기아를 유발하기 쉽습니다. 범용적으로는 queueMicrotask/setImmediate 사용을 권장합니다.
  • 테스트에서 플러시하기
    • 마이크로태스크만 비우고 싶다면 await Promise.resolve()로 한 틱을 양보하는 패턴을 사용할 수 있습니다.

변형 실험으로 개념 고정

1) 타이머 내부의 마이크로태스크는 “다음 매크로태스크 안”에서 먼저 비워집니다.

setTimeout(() => {
    console.log('A1');
    queueMicrotask(() => console.log('A2')); // 같은 틱에서 A2가 A1 바로 뒤에 실행
}, 0);

console.log('B1');

예상: B1 → (다음 틱) A1A2

2) 등록 순서가 곧 마이크로태스크 실행 순서입니다.

queueMicrotask(() => console.log('M1'));
Promise.resolve().then(() => console.log('M2'));
queueMicrotask(() => console.log('M3'));

예상: M1M2M3


결론

  • 상태 정합이 중요한 “동 틱 내 후처리”는 queueMicrotask로, 체인/에러 전파가 필요하면 Promise로.
  • 렌더 이후로 미루고 싶다면 setTimeout(0)이 아니라, 의도에 맞는 API(requestAnimationFrame, requestIdleCallback)를 고려하세요.
  • Node.js에서는 process.nextTick 남용을 지양하고 queueMicrotask/setImmediate를 선택하세요.

핵심 공식: 동기(B/E) → 마이크로태스크(C/D 전체 소진) → 매크로태스크(A). 이 규칙으로 대부분의 실행 순서를 안정적으로 예측할 수 있습니다.

728x90