브라우저/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를 “모두” 비움 → C
→ D
7) 다음 틱에서 Task Queue의 A
실행 → console.log('A')
정리: 동기(B, E) → 마이크로태스크(C, D) → 매크로태스크(A)
마이크로태스크 내부 순서는 “등록된 순서”입니다. 위 코드에서는 Promise.then
이 먼저, queueMicrotask
가 나중에 등록되어 C
→ D
순으로 실행됩니다.
비교 표: 어떤 큐로 가는가, 언제 실행되는가
항목 | setTimeout | Promise.then | queueMicrotask |
---|---|---|---|
큐 유형 | Task(매크로태스크) | Microtask | Microtask |
실행 시점 | 다음 틱 이후 | 현재 틱의 콜스택 종료 직후 | 현재 틱의 콜스택 종료 직후 |
순서 보장 | 동일 지연이면 상대적 | FIFO(등록 순) | FIFO(등록 순) |
렌더링 영향 | 보통 렌더 후 실행 | 렌더 전 마이크로태스크가 모두 비워짐 | 렌더 전 마이크로태스크가 모두 비워짐 |
에러 전파 | 타이머 콜백에서 throw 시 비동기 에러 | 잡히지 않으면 전역 Unhandled Rejection | 잡히지 않으면 전역 에러 |
취소 | clearTimeout |
불가(체인 취소는 별도 로직) | 불가 |
대표 용도 | 지연 실행, 렌더 이후 작업 | 비동기 후크/체인 | 매우 짧은 후처리, 동 틱 내 정합 보장 |
실무 팁과 주의사항
- 마이크로태스크 “고갈” 규칙을 이용해 일관성 보장
- 동일 틱 내 후처리는
queueMicrotask
가 가장 명확합니다.
- 동일 틱 내 후처리는
- 과도한 마이크로태스크 생성은 이벤트 루프 기아를 유발
- 무한 재귀적 마이크로태스크는 렌더링을 막습니다. 반드시 종료 조건을 두세요.
Promise.then
vsqueueMicrotask
- 둘 다 마이크로태스크이지만, 체인 작성과 에러 핸들링이 필요하면
Promise
가 편리합니다. 아주 경량 후처리는queueMicrotask
가 적합합니다.
- 둘 다 마이크로태스크이지만, 체인 작성과 에러 핸들링이 필요하면
- 브라우저 vs Node.js 차이 인지
- Node.js에서는
process.nextTick
이 마이크로태스크보다 우선 실행되어 기아를 유발하기 쉽습니다. 범용적으로는queueMicrotask
/setImmediate
사용을 권장합니다.
- Node.js에서는
- 테스트에서 플러시하기
- 마이크로태스크만 비우고 싶다면
await Promise.resolve()
로 한 틱을 양보하는 패턴을 사용할 수 있습니다.
- 마이크로태스크만 비우고 싶다면
변형 실험으로 개념 고정
1) 타이머 내부의 마이크로태스크는 “다음 매크로태스크 안”에서 먼저 비워집니다.
setTimeout(() => {
console.log('A1');
queueMicrotask(() => console.log('A2')); // 같은 틱에서 A2가 A1 바로 뒤에 실행
}, 0);
console.log('B1');
예상: B1
→ (다음 틱) A1
→ A2
2) 등록 순서가 곧 마이크로태스크 실행 순서입니다.
queueMicrotask(() => console.log('M1'));
Promise.resolve().then(() => console.log('M2'));
queueMicrotask(() => console.log('M3'));
예상: M1
→ M2
→ M3
결론
- 상태 정합이 중요한 “동 틱 내 후처리”는
queueMicrotask
로, 체인/에러 전파가 필요하면Promise
로. - 렌더 이후로 미루고 싶다면
setTimeout(0)
이 아니라, 의도에 맞는 API(requestAnimationFrame
,requestIdleCallback
)를 고려하세요. - Node.js에서는
process.nextTick
남용을 지양하고queueMicrotask
/setImmediate
를 선택하세요.
핵심 공식: 동기(B/E) → 마이크로태스크(C/D 전체 소진) → 매크로태스크(A). 이 규칙으로 대부분의 실행 순서를 안정적으로 예측할 수 있습니다.
'Frontend Development' 카테고리의 다른 글
useEffect에서 setInterval이 상태를 못 따라오는 이유 (stale closure) (0) | 2025.09.07 |
---|---|
React Error Boundary: 왜 아직도 클래스일까? (0) | 2025.09.07 |
Next.js 최신 캐싱 전략 총정리 (0) | 2025.09.03 |
면접에서 묻는 "의존성 주입 경험이 있나요?"의 의미 (1) | 2025.08.26 |
useState vs useRef vs let: 언제 무엇을 써야 할까? (0) | 2025.08.21 |