frontend/javascript

🟨 2-25. 자바스크립트 엔진 내부 구조 — V8 엔진이 코드를 처리하는 진짜 방식

mirabo01 2025. 11. 7. 08:56

1. V8 엔진이란?

V8은 Google ChromeNode.js에 탑재된 자바스크립트 엔진이다.
C++로 작성되었고,
자바스크립트 코드를 기계어(Machine Code) 로 직접 변환하여 실행한다.

🔍 다른 브라우저 엔진

  • Chrome → V8
  • Firefox → SpiderMonkey
  • Safari → JavaScriptCore (Nitro)
  • Edge(Chromium) → V8

즉, 우리가 console.log('Hello'); 를 실행할 때
그건 사실상 “C++ 코드로 변환된 실행 파일이 CPU 위에서 작동하는 것”이다.


2. V8의 주요 구성 요소

V8 엔진은 크게 아래와 같이 구성되어 있다 👇

┌─────────────────────────────────────────┐
│           JavaScript Source Code        │
└───────────────┬─────────────────────────┘
                ▼
        ┌────────────┐
        │ Parser (파서)│ → AST(Abstract Syntax Tree)
        └─────┬──────┘
              ▼
      ┌───────────────┐
      │ Ignition (인터프리터) │ → Bytecode 생성 및 실행
      └─────┬─────────┘
            ▼
     ┌────────────────┐
     │ TurboFan (JIT 컴파일러) │ → 자주 실행되는 코드 최적화
     └────────────────┘

3. 코드 실행의 전체 흐름

(1) Parsing (파싱)

먼저 자바스크립트 소스 코드를 읽고 구문을 분석한다.
이 단계에서 생성되는 것이 바로 AST (Abstract Syntax Tree) — 추상 구문 트리다.

function add(a, b) {
  return a + b;
}

→ AST 예시 (간략화)

FunctionDeclaration
 ├── name: "add"
 ├── params: [a, b]
 └── body: BinaryExpression (+)

이 트리는 자바스크립트 코드의 “논리적 구조”를 담고 있다.


(2) Bytecode 생성 (Ignition 인터프리터)

V8은 AST를 바이트코드(Bytecode) 로 변환한다.
이건 CPU가 직접 이해하지는 못하지만,
엔진이 빠르게 해석할 수 있는 중간 형태의 코드다.

예시

  • LdaConstant, Add, Return 같은 명령어로 구성
  • Python의 .pyc와 비슷한 개념

바이트코드는 인터프리터가 즉시 실행 가능하다.


(3) JIT 컴파일 (TurboFan 컴파일러)

Ignition이 코드를 실행하다 보면,
“자주 반복 실행되는 코드(Hot Function)”를 감지한다.

그러면 TurboFan이 그 코드를 기계어로 컴파일(JIT, Just-In-Time) 한다.

즉, JS 코드는 실행 중에 점점 더 빨라진다.

처음 실행 → 인터프리터로 느리게 실행  
반복 실행 → TurboFan이 최적화 → 네이티브 코드로 변환  

✅ 이게 바로 V8이 다른 엔진보다 빠른 이유다.


4. 최적화와 디옵티마이제이션

TurboFan은 최적화 시 타입 추론(Type Inference) 을 이용한다.

예를 들어 👇

function sum(a, b) {
  return a + b;
}

sum(1, 2);   // 숫자 → 최적화 대상
sum("a", "b"); // 문자열 → 비최적화 대상

V8은 처음에는 “a, b는 숫자다”라고 추론해 최적화하지만,
두 번째 호출에서 문자열이 들어오면
→ 추론이 깨짐
디옵티마이즈(De-Optimize) 과정으로 되돌린다.

✅ 그래서 JS에서 일관된 타입 사용이 중요하다.
이게 실무에서 “코드 일관성”을 유지하라는 이유 중 하나다.


5. V8의 메모리 구조

V8은 가상 머신이기 때문에,
실행 중 데이터를 저장할 힙(Heap)스택(Stack) 메모리 영역을 갖고 있다.

구분 설명

Stack 함수 호출, 지역 변수 저장
Heap 객체, 배열, 함수 등 동적 데이터 저장
function example() {
  const x = 10;        // Stack
  const obj = { y: 20 }; // Heap
}

obj는 Stack이 아니라 Heap에 저장되고,
Stack에는 그 Heap의 “참조(reference)”만 올라간다.


6. Garbage Collection (가비지 컬렉션)

V8은 Mark-and-Sweep 알고리즘으로 메모리를 자동 관리한다.

1️⃣ Root(전역 객체, 스코프 등)에서 접근 가능한 객체 탐색 (Mark)
2️⃣ 접근 불가능한 객체는 정리 (Sweep)

예시 👇

let obj = { name: "A" };
obj = null;

→ obj가 더 이상 참조되지 않음
→ GC가 감지하여 메모리에서 제거

⚠️ 하지만 “순환 참조(circular reference)”는 조심해야 한다.

const a = {};
const b = {};
a.ref = b;
b.ref = a;

✅ 둘 다 Root에서 접근 불가능해야 GC 가능
➡ 클로저, 이벤트 리스너에서 메모리 누수가 발생하는 이유다.


7. Hidden Class와 Inline Cache

V8은 객체 접근을 빠르게 하기 위해
Hidden Class (숨겨진 클래스)Inline Cache (IC) 를 사용한다.

Hidden Class

JS에는 클래스가 없지만,
V8은 내부적으로 객체 구조를 “클래스처럼” 정리한다.

function Point(x, y) {
  this.x = x;
  this.y = y;
}
const p1 = new Point(1, 2);
const p2 = new Point(3, 4);

→ 두 객체는 동일한 구조를 가지므로 같은 Hidden Class를 공유.
→ 접근 속도가 비약적으로 빨라진다.

하지만 👇

const p3 = new Point(5);
p3.z = 10;

→ Hidden Class가 깨짐 → 최적화 해제(Deopt) 발생

✅ 해결: 모든 프로퍼티를 초기화 시점에 선언하자.


Inline Cache

V8은 동일한 프로퍼티 접근이 반복되면,
그 메모리 주소를 캐싱하여 재사용한다.

function print(obj) {
  console.log(obj.name);
}

const user = { name: "Kim" };
print(user);
print(user);
print(user);

→ 첫 실행 시 “obj.name의 위치”를 기억
→ 다음 호출에서는 바로 접근

덕분에 반복 호출은 점점 더 빨라진다.


8. 실행 과정 요약

[JS Source] 
   ↓
[Parser] → AST 생성
   ↓
[Ignition] → Bytecode 변환 및 실행
   ↓
[TurboFan] → JIT 컴파일 (최적화)
   ↓
[CPU 실행] → 최적화된 네이티브 코드

✅ GC가 주기적으로 Heap을 청소
✅ Hidden Class, Inline Cache로 접근 속도 향상
✅ Hot Path는 TurboFan이 JIT 최적화


9. 실무 최적화 요약

항목 권장 패턴 이유

변수 타입 일관성 한 변수에 다양한 타입 넣지 않기 Deopt 방지
객체 구조 일정 유지 프로퍼티 추가/삭제 지양 Hidden Class 유지
반복문 내 함수 정의 금지 매번 클로저 생성 방지 메모리 절약
DOM 접근 최소화 JS ↔ 브라우저 통신 비용 절감  
GC 유도 방지 불필요한 참조 제거 메모리 누수 방지

10. 마무리

이제 단순히 “JS 코드가 동작한다”가 아니라
V8 엔진이 그 코드를 어떻게 최적화하고 실행하는지까지 이해했을 거야.

이걸 알면 단순한 코드 수정이 아니라,
“엔진이 좋아하는 코드 스타일”을 선택할 수 있게 된다.

“빠른 코드는 운이 아니라,
엔진의 동작 방식을 이해한 개발자의 선택이다.”