javascript · high priority
JavaScript 이벤트 루프
Call Stack · Task · Microtask · Render — 싱글 스레드가 비동기처럼 보이는 이유
학습 개요
탄생 배경
쉬운 설명
복잡한 개념을 실생활 비유로 설명합니다.
“은행 창구 한 개 + 번호표 두 종류”
창구(Call Stack)는 하나라 한 번에 한 손님만 응대합니다. 일반 번호표(Task)는 한 명 끝날 때마다 한 명씩 불러오고, VIP 번호표(Microtask)는 손님 한 명 끝난 직후 VIP 가 남아있는 동안 계속 불러옵니다. 창문 청소(렌더링) 는 VIP 손님이 모두 빠진 뒤에만 합니다 — 그래서 VIP 가 끝없이 몰리면 창문이 더러운 채로 유지됩니다.
핵심 개념
자바스크립트 엔진(V8/JSC/SpiderMonkey) 은 실행 중인 함수 호출을 **Call Stack** 에 LIFO 로 쌓고, 값은 **Heap** 에 보관합니다. 엔진 자체는 비동기 개념을 모릅니다 — setTimeout, fetch, 이벤트는 엔진이 아닌 **호스트 환경(브라우저/Node)** 이 제공하는 API 입니다. 이벤트 루프는 엔진과 호스트 사이의 **스케줄러** 역할을 합니다.
"엔진"과 "런타임" 의 구분
V8 같은 **엔진** 은 ECMAScript 사양(문법, Job Queue, GC) 만 구현합니다. setTimeout, fetch, addEventListener, 이벤트 루프의 구체적 큐 구조는 모두 **호스트 환경** 책임입니다. 그래서 브라우저와 Node.js 의 이벤트 루프 구현이 세부적으로 다릅니다 (Node 는 libuv 의 phase 기반).
실무 적용
어떤 상황에서 사용하는가
상태를 setState 로 바꾼 직후 DOM 높이를 측정해 레이아웃을 조정하려는데 이전 값이 나온다.
어떻게 적용하는가
React 는 state 업데이트 후 렌더링이 커밋되는 시점이 현재 microtask 범위가 아니라 다음 렌더 프레임 전후이므로, `useLayoutEffect` (paint 이전) 또는 `useEffect` + `requestAnimationFrame` 으로 측정 시점을 미뤄야 정확한 값을 얻는다. 이벤트 루프 상 "DOM 에 반영되는 순간" 이 언제인지를 먼저 그려봐야 디버깅이 빨라진다.
흔한 실수와 안티패턴
- `await` 뒤 코드가 바로 실행될 거라고 착각 — 실제로는 microtask 로 미뤄짐
- `setTimeout(fn, 0)` 이 "즉시" 실행될 거라고 믿음 — HTML 명세상 중첩 타이머는 최소 4ms clamp
- microtask 안에서 무거운 재귀 호출 → 프레임 drop 과 hang 유발
- Node 에서 `process.nextTick` 재귀 사용 → I/O 이벤트 starvation
- Promise 체이닝 길이가 성능에 영향 없다고 생각 — 과도한 체이닝은 같은 턴 내 microtask 폭주로 이어질 수 있음
흔한 오해
"이벤트 루프는 자바스크립트 엔진의 기능이다."
교정엔진이 아니라 호스트 환경(브라우저/Node) 이 정의·구현한다.
왜 중요V8 같은 엔진은 ECMAScript 의 Job Queue 만 정의하고, 구체적 Task Queue 구조와 렌더 파이프라인은 HTML/libuv 명세에 있다.
"setTimeout(fn, 0) 은 즉시 실행된다."
교정다음 task 로 미뤄지며, 중첩된 타이머는 최소 4ms clamp 가 걸릴 수 있다.
왜 중요HTML spec 이 배터리 보호와 지나친 타이머 남용 방지를 위해 clamp 를 규정하기 때문.
"await 는 실행을 진짜로 멈춘다."
교정함수 본문이 state machine 으로 변환되어 await 뒤부터 microtask 로 재개되는 것이지, 스레드가 blocking 되는 게 아니다.
왜 중요async 함수는 컴파일 타임에 제너레이터 유사 구조로 변환되고, await 는 Promise.then 으로 desugar 된다.
면접 질문
답변 방향 힌트
Call Stack, Task Queue, Microtask Queue 를 종이에 그리면서 각 시점에 어떤 콜백이 어디로 들어가는지 따라가세요.
반드시 언급할 키워드
- 동기 코드: 1, 4 가 먼저
- Promise.then 은 microtask — task(setTimeout) 보다 먼저 flush
- setTimeout 은 0ms 여도 다음 task 로 미뤄짐
- 최종 출력: 1, 4, 3, 2
- microtask 가 task 보다 우선한다는 점을 명시적으로 언급
예상 꼬리 질문
- `setTimeout(fn, 0)` 이 실제로는 즉시 실행되지 않는 이유는?
- Promise.then 을 여러 번 체이닝한 경우 microtask 몇 개가 생기나요?
자기 점검
스크롤 올리지 말고 답해보세요. Task 와 Microtask 의 차이와, 한 턴에 각각 몇 개가 실행되는지?
기대 키워드
자주 하는 오해
Microtask 를 "Task 보다 가벼운 버전" 정도로만 이해하면 실행 개수 차이를 놓치기 쉽습니다. Task 는 한 턴당 1개, Microtask 는 큐가 빌 때까지 전부 실행됩니다.
async function 안의 `await Promise.resolve(1)` 뒤 코드는 언제 실행되나요? 다음 번 어떤 단계인가요?
기대 키워드
자주 하는 오해
"await 한 줄이면 다음 task 까지 미뤄진다" 로 오해하는 경우가 많습니다. 실제로는 같은 이벤트 루프 턴의 microtask flush 에서 재개되므로 setTimeout(fn,0) 보다 먼저 실행됩니다.
`setTimeout(fn, 0)` 이 실제로 0ms 뒤에 실행되지 않는 이유는?
기대 키워드
자주 하는 오해
"0 이면 즉시" 라고 생각하지만, HTML 명세가 5단계 이상 중첩된 timer 에 대해 최소 4ms clamp 를 규정하고, 무엇보다 현재 task + 모든 microtask 가 먼저 끝나야 실행됩니다.
학습 자료
- The event loop — HTML Living Standard브라우저 이벤트 루프의 정식 사양. task/microtask queue, rendering steps 의 공식 정의.DocWHATWG
- In depth: Microtasks and the JavaScript runtime environmentMicrotask 와 이벤트 루프의 관계를 실제 예시와 함께 상세 설명. queueMicrotask 사용법 포함.DocMDN Web Docs
- The Node.js Event Looplibuv 기반 6 phase 모델, process.nextTick 과 setImmediate 의 차이, I/O starvation 주의사항.DocNode.js 공식 문서
- What the heck is the event loop anyway?이벤트 루프를 시각적으로 가장 잘 설명한 고전 강연. 이 분야의 "첫 수업" 으로 평가.VideoPhilip Roberts · JSConf EU · 2014