콘텐츠로 바로가기

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

Sequence Core Cluster Objective & Description Evidence (BoK)
1 V8 Architecture & JIT Ignition(인터프리터)과 TurboFan(JIT 컴파일러)의 협업 파이프라인과 탈최적화 메커니즘을 익힙니다. P1
2 Hidden Classes & Inline Caching 동적 객체를 C 구조체 메모리 레이아웃으로 매핑하여 속성 접근을 O(1)로 가속하는 V8 핵심 기법을 살펴봅니다. P5
3 Event Loop & Call Stack 단일 콜 스택, Microtask 큐, Macrotask 큐의 실행 우선순위와 이벤트 루프의 틱(Tick) 흐름을 살펴봅니다. Industry
4 Async I/O & Promises 비동기 작업이 libuv 워커 풀로 위임되고 콜백/Promise로 귀환하는 논블로킹 I/O 모델을 다룹니다. Industry

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" 등)가 찍히는 과정 증명.

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).
  • 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: 이벤트 루프 페이즈 분석, 실행 순서 예측.
  • 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 런타임의 컨텍스트 전환 흐름을 살펴봅니다.
  • 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 모듈.
  • 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) 흐름을 살펴봅니다.
  • Implement: Node.js libuv 워커 풀 블로킹 시뮬레이션. crypto.pbkdf2 (CPU 연산 C 바인딩) 비동기 호출을 6번 동시 실행. UV_THREADPOOL_SIZE=4(기본값) 환경에서 4개는 병렬 실행되나 2개는 워커 스레드 대기로 지연됨을 시간 측정으로 증명. 백그라운드 스레드 풀 크기 조정 효과 데모.

7. Terminology

Term (EN / ko, abbr) 1문장 정의 단계(기본/권장/실무/심화) 역할/맥락 관련 개념 유사/대비/함께 사용 오해 포인트 Evidence(Primary/Secondary/Industry) Flags(core)
Event Loop 콜 스택(Call Stack)이 비어있는지 확인하고, 태스크 큐(Task Queue)에 대기 중인 콜백 함수를 스택으로 밀어 넣어 실행하는 자바스크립트 논블로킹 동시성의 핵심 장치입니다. 기본 비동기 실행 제어 Call Stack / Task Queue Thread Pool JS 엔진 내부가 아닌 브라우저나 Node.js의 런타임 환경에서 제공됨 P1:CS2023 core
V8 Engine 구글이 개발한 오픈소스 자바스크립트 엔진으로, 바이트코드 인터프리터(Ignition)와 최적화 JIT 컴파일러(TurboFan)를 결합하여 JS를 기계어로 빠르게 번역합니다. 권장 JS 실행 엔진 JIT Compiler SpiderMonkey (Firefox) 단순히 코드를 읽어주는 것을 넘어 런타임에 기계어 최적화를 수행함 P5:SFIA core
Hidden Class 자바스크립트는 객체의 속성이 동적으로 변하기 때문에, V8 엔진이 내부적으로 C++의 클래스처럼 객체 구조를 고정시켜(캐싱) 속성 접근 속도를 높이는 최적화 기법입니다. 실무 메모리 및 성능 최적화 Inline Caching Dynamic Typing 개발자가 코드로 직접 다루는 개념이 아니라 엔진 내부의 C++ C-Struct 매핑 기술임 Industry core
Microtask Queue Promise.then() 콜백이 담기는 특별한 큐로, 일반 비동기 콜백(Task Queue)보다 우선순위가 높아 이벤트 루프가 먼저 비워내는 대기열입니다. 심화 태스크 스케줄링 Macrotask / Promise setTimeout (Macrotask) 마이크로태스크가 계속 쌓이면 일반 렌더링 태스크가 실행 기회를 잃을 수 있음(Starvation) Industry core

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 도입 등)할 수 있는가?