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