- 발행일
이벤트 루프, V8 엔진, 그리고 스택 & 큐 — 그리고 이 지식이 실전에서 쓰인 순간들
이벤트 루프, V8 엔진, 그리고 스택 & 큐
면접 대비 문답 형태로 정리했던 글입니다. 문답은 그대로 두고, 이 지식이 실무에서 실제로 쓰인 순간들을 뒤에 붙였습니다 — 그게 이 글을 다시 꺼낸 이유입니다.
Q1. V8 엔진의 주요 역할은?
자바스크립트 코드를 파싱해 AST를 만들고, 인터프리터(Ignition)가 바이트코드로 실행하며, 자주 실행되는 코드는 JIT 컴파일러(Turbofan)가 최적화된 네이티브 코드로 변환합니다. 가비지 컬렉션으로 메모리를 관리하고, 콜스택으로 실행 컨텍스트를 추적합니다.
Q2. 이벤트 루프의 역할과 구조는?
이벤트 루프는 싱글 스레드인 자바스크립트가 비동기 작업을 처리하는 메커니즘입니다.
- 콜스택(Call Stack) — 함수 실행 컨텍스트가 쌓이는 곳. 동기 코드가 실행되는 자리.
- 태스크 큐(Task Queue) —
setTimeout·setInterval·DOM 이벤트 등 비동기 콜백의 대기열. - 마이크로태스크 큐(Microtask Queue) —
Promise.then,async/await재개,MutationObserver. 태스크보다 우선순위가 높습니다.
루프의 규칙: 콜스택이 빌 때마다 → 마이크로태스크 큐를 전부 비우고 → 태스크 큐에서 하나 꺼내 실행.
Q3. 순서를 검증하는 최소 예제
console.log('Start')
setTimeout(() => console.log('Task Queue'), 0)
Promise.resolve()
.then(() => console.log('Microtask 1'))
.then(() => console.log('Microtask 2'))
console.log('End')
// Start → End → Microtask 1 → Microtask 2 → Task Queue
포인트 둘 — setTimeout(..., 0)도 동기 코드가 끝나기 전엔 절대 실행되지 않는다는 것, 그리고 마이크로태스크는 체인으로 이어져도 태스크보다 전부 먼저 소화된다는 것.
Q4. 왜 이 구조인가?
자바스크립트는 싱글 스레드라 무거운 작업이 콜스택을 점유하면 UI가 멈춥니다. 비동기 작업의 콜백을 큐에 두고 "스택이 빌 때" 실행하는 구조 덕분에, 스레드 하나로도 UI가 멈추지 않으면서 비동기 처리가 됩니다. 뒤집어 말하면 — 콜스택을 오래 점유하는 동기 코드는 이 구조 전체를 무력화합니다. 큐에 쌓인 게 아무리 급해도 스택이 비기 전엔 못 나오니까요.
이 지식이 실전에서 쓰인 순간들
당시엔 면접용 지식이라고 생각했는데, 이후 실무에서 이 글의 내용으로 정확히 풀린 문제들이 있었습니다.
1. 렌더 직후 getBBox()가 엉뚱한 값을 주던 문제
D3로 그래프를 그린 직후에 크기를 재서 중앙 정렬하려는데, getBBox()가 0이나 어긋난 값을 줬습니다. 원인은 이벤트 루프 그 자체 — DOM 조작 코드는 실행됐지만 브라우저의 레이아웃·페인트는 현재 태스크가 끝난 뒤에 일어나거든요. 같은 동기 흐름 안에서 측정하면 아직 계산 전의 값을 읽습니다. 해결은 측정을 다음 프레임으로 미루는 것(requestAnimationFrame 또는 setTimeout(0))이었고, D3 시각화 글에서 실제로 쓴 방법입니다. "왜 한 틱 기다리면 되는가"의 답이 정확히 이 글의 Q2였어요.
2. 구독 콜백의 stale closure
무한 스크롤과 웹소켓 구독에서 겪은 버그 — 콜백이 등록 시점의 상태를 캡처한 채 나중에(큐에서 꺼내질 때) 실행되니, 그 사이 바뀐 상태를 못 봅니다. "콜백은 등록되는 시점과 실행되는 시점이 다르다"는 이벤트 루프의 기본 성질이 클로저와 결합해 만드는 함정이고, 무한 스크롤 구현 글에서 실제 사례를 다뤘습니다.
3. 원래 이 글에 붙어 있던 DOM 이벤트 정리에 대해
원본에는 target vs currentTarget, 캡처링/버블링, 이벤트 위임 예제가 통째로 붙어 있었는데, 제목과 다른 주제(이벤트 루프가 아니라 DOM 이벤트 전파)라 이번 개고에서 걷어냈습니다. 요지만 남기면 — target은 실제로 클릭된 요소, currentTarget은 핸들러가 달린 요소이고, 이 차이를 이용하는 게 이벤트 위임(#list에 하나만 달고 event.target.tagName으로 분기)입니다. 정리 노트가 길어질 때 서로 다른 주제가 한 파일에 눌어붙는 전형적인 사례였어요.
정리
- 이벤트 루프의 규칙은 세 줄이면 끝나지만("스택 비우기 → 마이크로태스크 전부 → 태스크 하나"), 그 세 줄이 렌더 타이밍 버그와 stale closure의 원인 설명까지 해줍니다.
- "면접용 지식"과 "실무 지식"의 구분은 배울 때는 몰랐고, 버그를 만나고서야 같은 것이었음을 알았습니다.