탐색으로 돌아가기
V8 Engine19 / 24 단계

Event Loop

태스크 · 마이크로태스크

콜스택이 비었을 때, 이벤트 루프가 마이크로태스크(Promise) → 매크로태스크(setTimeout) 순으로 콜백을 실행한다.

이벤트 루프(event loop)는 단일 스레드인 JavaScript가 비동기 작업을 논블로킹으로 처리하게 해주는 조정자다. 콜스택이 비면, 이벤트 루프는 태스크 큐(task queue, 매크로태스크)에서 태스크를 '하나' 꺼내 콜스택에 올려 실행하고, 그 태스크가 끝나 콜스택이 다시 비면 마이크로태스크 큐(microtask queue)를 '완전히' 비운 뒤, 필요하면 렌더링을 수행하고 다시 루프를 돈다. 여기서 콜스택은 동기 실행을, Web API(브라우저 제공: DOM 이벤트, 타이머, fetch 등)는 비동기 작업을 백그라운드에서 처리한 뒤 콜백을 각 큐로 넣어주는 역할을 한다. 매크로태스크에는 setTimeout/setInterval, 사용자·네트워크 이벤트, 초기 스크립트 실행이 있고, 마이크로태스크에는 Promise 반응(.then/catch/finally), queueMicrotask(), MutationObserver 콜백이 있다. 핵심 규칙은 '태스크 하나 → 마이크로태스크 전부 → (필요 시)렌더링'의 반복이며, 마이크로태스크가 항상 다음 매크로태스크보다 먼저, 그리고 렌더링보다 먼저 실행된다는 점이다.

내부 구성

콜스택 (Call Stack)
현재 동기 코드가 실행되는 LIFO 스택. 이벤트 루프는 이 스택이 비어야만 다음 콜백을 올린다
Web API (브라우저 제공)
타이머·DOM 이벤트·fetch·XHR 등을 콜스택 밖(별도 스레드/OS)에서 처리하고, 완료 시 콜백을 해당 큐로 넣는 환경 API
태스크 큐 / 매크로태스크 큐 (Task Queue)
setTimeout·이벤트·초기 스크립트 등의 콜백이 대기하는 큐. 이벤트 루프는 한 회전에 여기서 딱 하나만 꺼낸다
마이크로태스크 큐 (Microtask Queue)
Promise 반응·queueMicrotask·MutationObserver 콜백이 대기하는 큐. 매 태스크 후 콜스택이 비면 전부 소진될 때까지 실행
이벤트 루프 (Event Loop)
콜스택·큐·렌더링을 조율하는 반복 루프. '태스크→마이크로태스크→렌더링' 순서를 강제하는 핵심 스케줄러
렌더링 파이프라인 단계 (Style/Layout/Paint/Composite)
DOM/CSS 변경을 화면에 반영. 마이크로태스크 소진 후 다음 태스크 전에 필요 시 실행되며 보통 디스플레이 주사율(≈60fps)에 맞춰짐
requestAnimationFrame 콜백 (rAF)
렌더링 직전에 실행되도록 예약되는 콜백. 애니메이션 갱신을 프레임 페인트와 동기화해 부드럽게 함
MutationObserver 콜백
DOM 변이(mutation)를 관측해 마이크로태스크로 콜백 실행. 여러 변경을 배치로 모아 통지
Node.js libuv 페이즈 (참고)
브라우저와 달리 Node는 libuv로 timers→pending callbacks→poll→check(setImmediate)→close callbacks 단계를 순환하며, 각 단계 사이에 nextTick 큐·마이크로태스크 큐를 비운다

핵심 포인트

  • 이벤트 루프 1회전: 매크로태스크 1개 실행 → 마이크로태스크 큐 전부 비움 → (필요 시)렌더링 → 반복
  • 마이크로태스크는 매크로태스크보다 우선순위가 높고, 콜스택이 빌 때마다 '전부' 소진된다(중간에 추가돼도 같은 회전에서 실행)
  • 매크로태스크 소스: setTimeout/setInterval, DOM·네트워크 이벤트, 초기 스크립트, MessageChannel 등
  • 마이크로태스크 소스: Promise(.then/catch/finally), queueMicrotask, MutationObserver, async/await의 await 이후
  • Web API(브라우저 제공: 타이머·DOM·fetch·geolocation 등)는 콜스택 밖에서 비동기 작업을 처리하고 콜백을 큐에 넣는다
  • 렌더링(스타일·레이아웃·페인트)은 마이크로태스크가 다 끝난 뒤, 다음 매크로태스크 전에 일어나며 rAF 콜백은 그 렌더링 직전에 실행된다
  • 무한히 마이크로태스크를 추가하면 렌더링·태스크가 굶어(starvation) UI가 멈출 수 있다

심화

면접 단골은 'setTimeout(fn,0)과 Promise.then, queueMicrotask의 실행 순서'와 'async/await가 내부적으로 무엇으로 변환되는가'다. 동기 코드가 전부 끝나 콜스택이 비면 마이크로태스크가 먼저 전부 실행되고 그다음 setTimeout(매크로태스크)이 실행된다. await는 그 지점에서 함수를 멈추고 나머지(continuation)를 마이크로태스크로 예약하는 문법 설탕이라, await 뒤 코드는 항상 마이크로태스크 타이밍에 재개된다. 그래서 'Promise가 setTimeout(0)보다 빠르다'는 결과가 나온다. 또한 마이크로태스크 안에서 계속 새 마이크로태스크를 추가하면 큐가 비지 않아 렌더링과 다음 태스크가 영원히 밀리는 마이크로태스크 기아(starvation)가 생길 수 있다는 점을 짚으면 이해도가 드러난다. 브라우저와 Node.js의 차이도 깊은 포인트다. 브라우저의 이벤트 루프는 HTML 명세가 정의하며 렌더링 기회(rAF·style·layout·paint)가 루프에 통합돼 있다. 반면 Node.js는 libuv 기반으로 timers → pending callbacks → poll → check(setImmediate) → close callbacks의 여러 페이즈를 순환하고, 각 페이즈 콜백 하나가 끝날 때마다 process.nextTick 큐를 먼저, 그다음 마이크로태스크(Promise) 큐를 비운다 — 그래서 Node에서는 nextTick이 queueMicrotask보다 먼저 실행된다. 실무 함의로는, DOM 변경 후 즉시 레이아웃 값을 읽으면 강제 동기 레이아웃(reflow)이 일어나 성능이 나빠지므로 읽기/쓰기를 rAF로 배치(batch)하는 것이 좋고, 무거운 계산은 이벤트 루프를 막지 않도록 Web Worker나 청크 분할로 넘겨야 한다는 점을 연결하면 실전 감각까지 보여줄 수 있다.

쉽게 말하면 창구(콜스택)가 비면 VIP 대기줄(마이크로태스크)을 먼저 다 처리하고, 일반 대기줄(매크로태스크)에서 한 명 부르는 것.

면접 예상 질문

#이벤트 루프#마이크로태스크#매크로태스크#Promise#setTimeout