JavaScript V8 & Event Loop
JavaScript V8 및 Event Loop의 정의, 범위, 선행 지식, 학습 주제, 참고 근거를 정리한 CS&E 학습 노드입니다.
Article
M
Me
hyunyoun's Blog
programming-languages-compilersprogramming-languagescompilersruntime-systemsmemory-managementjava-script-v8event-looplanguages-compilers10 min read
1. Overview
JavaScript V8과 이벤트 루프(JavaScript V8 & Event Loop)는 브라우저 스크립트 언어를 고성능 서버(Node.js)와 데스크톱 앱(Electron)의 구동 엔진으로 확장시킨 구글 V8 엔진의 실행 구조와 단일 스레드 비동기 I/O의 핵심인 **이벤트 루프(Event Loop)**를 다룹니다.
학습자는 V8의 인터프리터(Ignition)와 최적화 컴파일러(TurboFan), 그리고 동적 타입 객체를 C++ 구조체처럼 빠르게 접근하게 해주는 **히든 클래스(Hidden Class)**와 인라인 캐싱(Inline Caching) 기법을 살펴봅니다. 이어서 JavaScript 런타임의 핵심 동시성 모델인 콜 스택(Call Stack), 태스크 큐(Task Queue, Macrotask/Microtask), 이벤트 루프 루틴을 정리하며, "싱글 스레드임에도 멈추지 않는" Node.js 백엔드의 비동기 논블로킹 I/O 구조를 이해합니다.
2. Scope & Boundaries
In-Scope
- V8 엔진 아키텍처 (V8 Engine): Ignition 인터프리터, TurboFan JIT 컴파일러, JIT Tiering, 탈최적화(Deoptimization).
- 객체 모델 최적화 (Object Optimization): 히든 클래스(Hidden Class / Shape / Map), 인라인 캐싱(Inline Caching, IC), Fast Properties vs Dictionary Mode.
- 이벤트 루프 (Event Loop): 콜 스택, Web APIs (브라우저) / libuv (Node.js), 매크로태스크 큐(Macrotask: setTimeout), 마이크로태스크 큐(Microtask: Promise, process.nextTick).
- 비동기 I/O (Async I/O): 논블로킹 I/O 모델, Callback Hell, Promise 체이닝, async/await의 동작 원리.
Out-of-Scope
- DOM 렌더링 파이프라인: 브라우저 렌더링 엔진(Blink) 레이아웃/페인팅 → Browser Architecture 영역.
- Node.js 모듈 시스템/NPM: CommonJS vs ES Modules, 패키지 관리 → 프레임워크 생태계 영역.
Boundaries
- 비동기성 vs 병렬성: JavaScript 엔진(콜 스택)은 철저히 **싱글 스레드(Single-threaded)**입니다.
setTimeout이나 파일 읽기가 병렬로 처리되는 것은 JS 엔진이 아니라 호스트 환경(브라우저 Web API 또는 Node.js libuv 워커 스레드 풀)이 대신 수행하기 때문입니다. JS 코드는 병렬로 실행되지 않으며(Web Worker 제외), I/O 대기 시간에 다른 JS 코드를 쪼개서 실행하는 **비동기 동시성(Asynchronous Concurrency)**입니다.
3. Counterexample
- 콜 스택 블로킹으로 인한 전체 서버 정지 (Blocking the Event Loop): Node.js 서버에서
while (true) {}무한 루프나 10초 걸리는 동기적JSON.parse(대형파일)을 실행. 콜 스택이 비워지지 않으므로 이벤트 루프가 멈춰, 수신되는 모든 새 HTTP 요청이 타임아웃됩니다. 싱글 스레드 환경에서는 CPU 집약적 작업 하나가 전체 시스템을 멈추게(Block) 하므로 10ms 이상 걸리는 동기 작업은 워커 스레드(Worker Thread)로 분리해야 합니다. - 히든 클래스 전환으로 인한 JIT 최적화 저하 (Hidden Class Megamorphism):
function Point(x, y) { this.x = x; this.y = y; }. 객체a = new Point(1, 2)생성 후a.z = 3동적 속성 추가.b = new Point(3, 4)생성 후b.w = 5추가. 생성자에서 속성을 초기화하지 않고 런타임에 임의로 추가하면 V8이 히든 클래스를 계속 분기(Split)하여 인라인 캐싱이 메가모픽(Megamorphic, 최적화 포기 딕셔너리 모드) 상태가 되고, 프로퍼티 접근 속도가 수십 배 느려집니다.
4. Prerequisites
- JIT 컴파일 기초 (Basic): 가상 머신의 인터프리터와 JIT 컴파일러 기본 동작. (05-02-03 Virtual Machines & JIT)
- 운영체제 I/O (Basic): 동기(Synchronous) vs 비동기(Asynchronous) 논블로킹 모델. (03-03-03 File System & I/O)
5. Learning Map
6. Learning Topics
Basic
Core Topic 01: 엔진의 점화와 터보팬, V8 아키텍처와 JIT (V8 Architecture & JIT)
- Why to Learn: Node.js 서버 성능 문제 진단이나 고성능 클라이언트 사이드 게임 제작 시, 어떤 JS 코드가 JIT 최적화(TurboFan)를 잘 받고 어떤 코드가 인터프리터(Ignition) 수준으로 강등되는지를 이해하기 위함입니다.
- What to Learn:
- Concepts: Ignition(바이트코드 인터프리터), TurboFan(최적화 컴파일러), JIT Tiering, Hot Function, Speculative Optimization, Deoptimization.
- Skills: V8 엔진 플래그
--print-opt-code,--trace-deopt분석.
- How to Learn:
- 1단계: 소스 코드 파싱 → AST → Ignition이 바이트코드로 변환 후 실행 시작(빠른 시작 시간). 자주 호출되는 함수(HotSpot) 발견 시 타입 정보를 수집하여 TurboFan으로 전송함을 살펴봅니다.
- 2단계: TurboFan은 "이 변수는 계속 숫자였다"고 투기적(Speculative) 최적화하여 네이티브 기계어 생성. 만약 문자열이 들어오면 추정 실패(Bailout) 발생 후 Ignition 인터프리터로 강등(Deoptimization)되는 과정을 살펴봅니다.
- Implement: JavaScript Node.js CLI 테스트.
function add(a, b) { return a+b; }루프 10만 번 호출 (TurboFan 진입). 이후add("string", 1)호출하여 Deoptimization 유발.node --trace-deopt test.js명령어로 터미널에 탈최적화 로그("soft deopt" 등)가 찍히는 과정 증명.
Recommended
Core Topic 02: 동적 객체를 정적 구조체처럼 빠르게, 히든 클래스와 IC (Hidden Classes & Inline Caching)
- Why to Learn: 파이썬 딕셔너리처럼 동작하는 JS 동적 객체(해시 테이블)의 느린 속성을 어떻게 V8이 C++의 컴파일 타임 구조체 오프셋(Offset)처럼 O(1) 포인터 연산으로 빠르게 처리하는지 이해하기 위함입니다.
- What to Learn:
- Concepts: 히든 클래스(Hidden Class / Shape / Transition Tree), 인라인 캐시(Inline Cache, IC), Fast Properties(히든 클래스 모드) vs Dictionary Mode(해시맵 강등).
- Skills: JIT 친화적(Monomorphic) 객체 생성 패턴 준수(생성자 내 속성 일괄 정의, 정의 순서 일치).
- How to Learn:
- 1단계: 히든 클래스 전이:
obj = {}(Class C0).obj.x = 1추가 시x가 메모리 오프셋 0에 있다는 히든 클래스 C1 생성 & obj의 클래스 포인터 C1 갱신.obj.y = 2추가 시 C2 생성 & 갱신.obj2 = {x:1, y:2}생성 시 같은 순서면 C2를 공유하는 구조적 메모리 트리를 살펴봅니다. - 2단계: 인라인 캐시(IC):
function getX(obj) { return obj.x; }. 첫 호출 시obj가 C2 히든 클래스임을 캐싱. 다음 호출 시 C2 클래스 검사(비교 1번) 후 오프셋 0에서 즉시 값을 읽는 O(1) 캐싱 흐름을 살펴봅니다. 객체 구조 변경(속성 삭제delete obj.x)은 이 최적화를 깨뜨립니다(Dictionary Mode).
- 1단계: 히든 클래스 전이:
- Implement: Node.js 퍼포먼스 벤치마크. 동일한 속성을 가진 객체 100만 개 리스트 순회 vs 속성 추가 순서가 랜덤하여 10개의 다른 히든 클래스를 가진 객체 리스트 순회 성능 비교. Monomorphic IC(빠름) vs Polymorphic IC(느림) 시간 차이(수십 배) 콘솔 출력.
Practical
Core Topic 03: 멈추지 않는 싱글 스레드의 심장, 콜 스택과 이벤트 루프 (Event Loop & Call Stack)
- Why to Learn:
setTimeout(() => console.log('A'), 0); console.log('B');가 왜 B, A 순서로 출력되는지, 그리고 Promise는 왜 setTimeout보다 먼저 실행되는지를 이해하여 비동기 코드 버그를 줄이기 위함입니다. - What to Learn:
- Concepts: 콜 스택(Call Stack), 브라우저 Web APIs / Node C++ API, 콜백 큐(Callback Queue), 매크로태스크(Macrotask:
setTimeout,I/O), 마이크로태스크(Microtask:Promise.then,process.nextTick), 이벤트 루프(Tick). - Skills: 이벤트 루프 페이즈 분석, 실행 순서 예측.
- Concepts: 콜 스택(Call Stack), 브라우저 Web APIs / Node C++ API, 콜백 큐(Callback Queue), 매크로태스크(Macrotask:
- How to Learn:
- 1단계: 콜 스택(현재 실행 코드)이 빌 때까지 이벤트 루프는 큐를 대기합니다. 스택이 비면 1순위: Microtask 큐(
Promise) 전체 비우기. 2순위: Macrotask 큐(setTimeout) 하나 꺼내 스택으로 이동시키는 틱(Tick) 구조를 살펴봅니다. - 2단계: 실행 순서 퀴즈:
sync 1->Promise 2->setTimeout 3->sync 4. 동기 코드(1, 4) 완료 후 콜 스택 비워짐 → Microtask(Promise 2) 실행 → Macrotask(setTimeout 3) 실행. 브라우저/Node.js 런타임의 컨텍스트 전환 흐름을 살펴봅니다.
- 1단계: 콜 스택(현재 실행 코드)이 빌 때까지 이벤트 루프는 큐를 대기합니다. 스택이 비면 1순위: Microtask 큐(
- Implement: 복잡한 실행 순서 예측 스크립트 작성. 중첩된
Promise.resolve().then(...),process.nextTick(...),setTimeout(...),setImmediate(...)10단 콤보 코드. 예상 출력 리스트를 배열로 주석 작성 후, 실제 실행하여 Microtask > Macrotask 우선순위 및 Node.js 이벤트 루프 페이즈 동작 증명.
Advanced
Core Topic 04: 호스트 환경으로의 I/O 아웃소싱, 비동기 I/O와 Promise (Async I/O & Promises)
- Why to Learn: Node.js가 C10K 문제(1만 개 동시 소켓 연결)를 적은 메모리와 단일 스레드로 처리하는 배경(libuv 스레드 풀 및 epoll/kqueue)과 그 위에서 콜백 헬을 완화한 Promise/async 아키텍처를 이해하기 위해서입니다.
- What to Learn:
- Concepts: Node.js 아키텍처 (V8 + libuv), 비동기 논블로킹 I/O(Asynchronous Non-blocking I/O), Worker Thread Pool(libuv 기본 4개 스레드), Callback Hell, Promise 체이닝,
async/await. - Skills:
async/await동기적 예외 처리(try/catch), 비동기 병렬화(Promise.all). - Tools: Node.js 내장
fs/promises,crypto모듈.
- Concepts: Node.js 아키텍처 (V8 + libuv), 비동기 논블로킹 I/O(Asynchronous Non-blocking I/O), Worker Thread Pool(libuv 기본 4개 스레드), Callback Hell, Promise 체이닝,
- How to Learn:
- 1단계: I/O 아웃소싱: JS 코드
fs.readFile()호출. Node.js는 이를 C++ 바인딩을 통해 libuv 워커 스레드(또는 OS 비동기 I/O 커널)로 넘기고 JS 콜 스택을 즉시 비웁니다. 파일 읽기 완료 후 libuv가 콜백 큐에 완료 이벤트를 넣는 아키텍처를 살펴봅니다. - 2단계: 중첩 콜백(피라미드)을
Promise.then으로 평탄화(Flat)하고, 이를async/await문법 설탕(Syntactic Sugar)으로 동기 코드처럼 읽히게(하지만 실제로는 제어권을 이벤트 루프에 반환하는) 만드는 코루틴(Coroutine) 흐름을 살펴봅니다.
- 1단계: I/O 아웃소싱: JS 코드
- Implement: Node.js libuv 워커 풀 블로킹 시뮬레이션.
crypto.pbkdf2(CPU 연산 C 바인딩) 비동기 호출을 6번 동시 실행.UV_THREADPOOL_SIZE=4(기본값) 환경에서 4개는 병렬 실행되나 2개는 워커 스레드 대기로 지연됨을 시간 측정으로 증명. 백그라운드 스레드 풀 크기 조정 효과 데모.
7. Terminology
8. References
Primary
- [P1] CS2023 - Programming Languages (PL) - Event-Driven Programming
- [P5] SFIA - Software Development (PROG) - Asynchronous Logic
Secondary
- [You Don't Know JS Yet: Async & Performance] Kyle Simpson - The Event Loop and Promises
- [High Performance Browser Networking] Ilya Grigorik - Browser Event Loop & Non-blocking I/O
Industry
- [Node.js Documentation] - The Node.js Event Loop, Timers, and
process.nextTick() - [V8 Dev Blog] - Ignition and TurboFan Architecture, Hidden Classes
9. Final Checklist
Primary
- 자바스크립트가 단일 스레드(Single-Thread)임에도 불구하고 HTTP 요청을 기다리지 않고(Non-Blocking) 다음 코드를 실행할 수 있는 이유를 이벤트 루프 관점에서 설명할 수 있는가?
-
Call Stack,Web APIs(또는 C++ APIs),Task Queue,Event Loop네 가지 구성 요소가 비동기 콜백(예:setTimeout)을 처리하는 순환 사이클을 묘사할 수 있는가?
Secondary
-
setTimeout(cb, 0)콜백(Macrotask)과Promise.then(cb)콜백(Microtask)이 경합할 때, 이벤트 루프가 왜 항상Promise를 먼저 스택에 올리는지 증명할 수 있는가? - V8 엔진의 히든 클래스(Hidden Class)와 인라인 캐싱(Inline Caching) 원리를 이해하고, 객체의 속성(Property)을 생성자 밖에서 동적으로 계속 추가하면 왜 성능이 크게 떨어지는지 논증할 수 있는가?
Industry
- 무거운 JSON 파싱(Sync 연산)을 메인 스레드에서 돌리면 이벤트 루프가 블로킹되어 전체 서버(Node.js)나 브라우저 화면이 정지(Freeze)되는 병목을 아키텍처 관점으로 설명할 수 있는가?
- V8의 JIT 컴파일러(TurboFan)가 변수 타입을 추측하여(Type Speculation) 고속 기계어로 컴파일했다가, 다른 타입의 인자가 들어오면 디옵티마이즈(Deoptimization)하며 속도가 급격히 떨어지는 현상을 방어(TypeScript 도입 등)할 수 있는가?