자바스크립트 엔진(V8)의 작동 원리

728x90

V8과 자바스크립트 엔진 개요

 

1. 자바스크립트는 어떻게 실행되는가?

자바스크립트는 사람이 작성한 텍스트 코드를 브라우저Node.js 내부의 자바스크립트 엔진(V8 등)이 해석하고 실행한다.

자바스크립트 실행 과정은 크게 파싱 -> 컴파일 -> 실행 순으로 진행되며, 최적화도 동시에 수행된다.


2. 엔진이 무엇이고 V8은 왜 유명한가?

자바스크립트 엔진은 JS 코드를 실행하는 핵심 시스템이고, V8은 성능과 범용성에서 가장 뛰어난 엔진이다.

  • 엔진 : 자바스크립트 코드를 파싱, 컴파일, 실행하는 시스템
  • V8 : 구글이 만든 JS 엔진으로, 빠른 속도, JIT 컴파일, Node.js에서 사용되면서 JS 생태계를 주도

2.1. 자바스크립트 엔진의 정의

자바스크립트 엔진은 JS 소스 코드를 읽고(파싱) -> 기계어로 바꾸고(컴파일) ->실행하는 소프트웨어이다

 

브라우저(Chrome)나 런타임(Node.js) 내부에 포함되어 있으며, 사용자가 작성한 코드가 실제로 작동할 수 있게 만들어주는 실행기(interpreter + compiler)이다.

2.2 V8 엔진이 유명한 이유

특징 설명
빠른 속도 JIT 컴파일러와 다양한 최적화 기술로 빠른 실행 속도 확보
Node.js의 엔진 브라우저 외에도 서버(Node.js)에서 JS를 실행할 수 있게 한 기발
WebAssembly 지원 JS 외에도 다른 언어로 작성된 코드를 실행할 수 있다.
크로스 플랫폼 Window, Linux, macOS 등 어디서든 작동 가능
오픈 소스 누구나 분석 및 커스텀마이징 가능, 생태계 확장에 기여

 

2.3. V8 엔진이 탑재된 대표 플랫폼

  • Google Chrome
  • Node.js
  • Electron (데스크탑 앱 프레임워크)
  • Deno
  • Cloudflare Workers / Vercel Edge Functions 등 서버리스 플랫폼

3. 인터프리터 vs 컴파일러 비교

3.1. 요약

특징 인터프리터(Interpreter) 컴파일러 (Compiler)
실행 방식 한 줄씩 바로 해석하며 실행 전체 코드를 한 번에 기계어로 번역 후 실행
실행 속도 초기 실행은 빠름, 전체는 느릴 수 있음 초기 느리지만 실행 중 속도 빠름
디버깅 에러가 발생한 줄에서 바로 확인 가능 컴파일 타임에 전체 에러 확인 가능
메모리 사용 적음 상대적으로 많음
예시 언어 JS, Python C, C++, Rust

 

 

3.2. JIT 컴파일

JIT : Just-In-Time Compiler

JIT 컴파일은 실행 중(Interpreting 도중)에 자주 실행되는 부분을 기계어로 변환하여 실행 속도를 높이는 하이브리드 방식이다.

 
3.2.1. 동작 흐름
  1. 인터프리터로 전체코드 실행
  2. 실행 중 프로파일링 ( 자주 쓰이는 코드 확인)
  3. 자주 쓰이는 코드 (핫 코드) 를 JIT 컴파일러가 기계어로 변환
  4. 이후 이 기계어 코드로 빠르게 실행
 
3.2.2. JIT와 다른 컴파일 방식들
방식 설명 예시
AOT (Ahead-Of-Time) 실행 전에 전체 소스코드를 미리 기계어로 변환 C, C++, Rust, Swift
JIT (Just-In-Time) 실행 중에 필요한 부분만 기계어로 컴파일 Java, JS(V8), Kotlin/Native 일부
인터프리팅 기계어 변환 없이, 바로 한 줄씩 실행 JS, Python, Ruby

 


 

V8 내부 구조

V8은 파서 -> 인터프리터 (Ignition) -> JIT (TurboFan)로 구성되어, 실행 중 최적화를 반복하여 성능을 끌어올리는 구조이다.

 

1. V8 흐름 

소스코드
-> Parser (파서)
-> AST (추상 구문 트리)
-> Interpreter (Ignition)
-> Bytecode 생성 및 실행
-> Hot Code 감지
-> JIT Compiler (TurboFan)
-> 최적화된 기계어 코드
-> CPU에서 실행


2. 상세 설명

2.1. Parser (파서)

  • 자바스크립트 소스를 읽어서 AST(abstract syntax tree)로 변환
  • 문법 오류 체크도 이 단계에서 처리됨

2.2. Ignition (인터프리터)

  • AST를 바이트코드(bytecode)로 변환하고 즉시실행
  • 빠른 시작이 가능함
  • 코드 실행 중 어떤 함수가 자주 호출되는지 프로파일링을 시작함

2.3. Profiler (프로파일러)

  • 어떤 함수/루프가 자주 실행되는지 기록 -> Hot Code 식별
  • 성능을 향상할 수 있는 대상만 JIT 컴파일러로 넘김

2.4. TurboFan (JIT 컴파일러)

  • Hot Code를 받아서 기계어로 컴파일
  • 이 코드는 이후 재사용됨 -> 실행 속도 급격히 향상
  • 다양한 최적화 수행
    • Inline Caching : 객체 프로퍼티 접근을 캐싱
    • Escape Analysis : 객체가 함수 밖에서 사용되지 않으면 스택에 할당
    • Dead Code Elimination : 실행되지 않는 코드는 제거
    • Function Inlining : 자주 호출되는 함수 내부로 코드 삽입
    • Constant Folding : 계산 가능한 상수는 미리 처리

2.5. Garbage Collector ( GC )

  • 필요 없는 메모리를 자동으로 회수함
  • V8은 Generational GC 방식 사용:
    • New Space (짧게 살아남는 객체)
    • Old Space (오래 살아있는 객체)
부가 구성요소
  • IC (Inline Cache) : 객체 접근을 캐싱하여 반복 성능 향상
  • Hidden Class / Map : 객체 구조를 정형화시켜 빠르게 접근 가능하게 함
  • Zone memory allocator : 단기 메모리 관리를 위한 전용 영역

3. Ignition / TurboFan

  • Ignition : 바이트 코드를 생성하고 실행하는 인터프리터
  • TurboFan : 실행 중인 핫코드를 기계어로 최적화 컴파일하는 JIT 컴파일러

3.1. Ignition : 바이트코드를 실행하는 인터프리터

항목 설명
역할 자바스크립트 소스코드를 바이트코드로 변환하고 실행함
특징 빠른 시작(Startup Time), 저메모리 소비
도입 배경 기존에는 AST -> 기계어로 전환하던 방식에서 더 유연한 실행을 위해서 등장
장점 빠르게 실행 시작 가능하며, JIT 컴파일 대상 탐지 가능 (프로파일링 포함)
function add(a, b){
    return a + b
}

add(1, 2)
  • 위 함수가 처음 실행될 때, Ignition이 바이트코드를 만들어 바로 실행한다.
  • 반복 실행되면 이 함수는 'Hot Code'로 감지되고, TruboFan에게 넘겨진다.

3.2. TurboFan : 기계어로 변환하는 JIT 컴파일러

항목 설명
역할 자주 실행되는 코드(핫코드)를 JIT 컴파일하여 네이티브 코드로 변환
특징 SSA 기반 중간 표현(IR)을 사용하여 다양한 최적화 수행
장점 실행 속도를 극적으로 향상, 캐시된 기계어 코드 재사용

3.3. SSA 기반 중간 표현

SSA란?
  • Static Single Assignment - 정적 단일 할당
  • "모든 변수는 딱 한 번만 할당된다."
  • 어떤 값이 어디서 생성되고, 어디로 흐르는지를 명확하게 추적할 수 있게 만드는 구조
예시
// 기존코드 

let x = 1;
x = x + 2;
x = x * 3;

// SSA로 변환하면
x1 = 1 // 최초 할당
x2 = x1 + 2 // x1의 결과로 x2 생성
x3 = x2 * 3 // x2의 결과로 x3 생성

변수명이 바뀌면서 모든 값이 어떤 연산 결과인지 명확하게 추적할 수 있게 된다.

중요한 이유?
  • x3는 x2가 있어야만 계산이 되고
  • x2는 x1이 있어야만 계산이 된다.

그래프 형태로 데이터 흐름을 추적할 수 있게 된다. -> 컴파일러 입장에서 최적화하기 좋은 조건

만약!

"x2가 필요 없는 값이면 x3도 필요 없다" -> 불필요한 연산 통째로 제거 가능

최적화 기법
최적화 기법 설명 예시
Dead Code Elimination 결과에 영향을 주지 않는 연산 제거 let a = 5; → a 안 쓰면 제거
Constant Folding 고정된 값을 미리 계산 2 + 3 → 5로 변환
Value Numbering 같은 연산은 하나만 남김 a + b, a + b → 한 번만 수행
Copy Propagation 중간 변수 제거 let x = y; let z = x; → z = y;
Loop-Invariant Code Motion 루프 밖으로 고정 연산 이동 for (...) { const x = 3 + 4; } → 루프 밖으로 이동

즉, 값이 “언제”, “어디서”, “무엇으로” 바뀌는지를 정확히 알 수 있기 때문에, 공격적인 최적화가 가능

3.4. Ignition과 TurboFan의 협업

단계 역할
1단계 Ignition이 바이트코드 생성 및 초기 실행
2단계 실행 중 반복된 함수/루프를 HotCode로 식별
3단계 TurboFan이 해당 코드 JIT 컴파일-> 네이티브 기계어 생성
4단계 이후부터는 기계어 코드가 재사용되어 성능 급상승

 


 

4. 결론

TurboFan의 핵심 : '실행하면서 배우고, 자주 쓰는 코드는 더 빠르게 실행되도록 만든다.'

  • V8은 코드 실행 중에도 계속해서 학습하고, 분석하고, 최적화한다.
  • 이를 통해 한 번 실행된 코드라도 점점 더 빠르게 동작하게 만드는 것이 이 구조의 핵심이다.
  • V8은 JS 엔진이라기보단 하나의 런타임 머신에 가깝다.

V8 엔진이 좋아하는 코드 스타일과 싫어하는 패턴

JIT-friendly VS JIT-unfriendly

 

V8은 타입이 일관되고 구조가 고정된 코드를 좋아하며, 동적으로 구조가 바뀌는 객체나 예외적 흐름을 싫어한다.

1. V8이 좋아하는 코드 스타일 (JIT-Friendly)

패턴 설명
일관된 객체 구조 프로퍼티 순서와 수가 고정되어야 Hidden Class를 재사용할 수 있음
일관된 변수 타입 타입이 계속 바뀌지 않아야 인라인 캐싱이 잘 작동
짧고 명확한 함수 인라인 최적화 및 빠른 실행 가능
배열은 같은 타입만 사용 배열 내부에 숫자, 문자열, 객체가 섞이면 최적화 깨짐
클래스 사용 권장 클래스는 Hidden Class를 잘 활용하기에 성능상 이점 존재
반복문에서 상수는 루프 밖으로 이동 for 내부에서 불필요한 연산 제거 가능
try-catch는 제한적으로만 사용 예외 흐름은 최적화의 적 → 별도 핸들링 로직으로 분리 추천

2. V8이 싫어하는 코드 스타일 (JIT Unfriendly)

패턴 설명
동적 프로퍼티 추가/삭제 객체 구조(Hidden Class)가 계속 바뀌면 JIT 포기
타입이 자주 바뀌는 변수 let x = 1; x = '문자열'; → 인라인 캐싱 불가
배열에 타입 섞기 [1, 2, "3", {a: 1}] → 일반 객체 배열로 강등
delete 키워드 사용 Hidden Class 깨짐, JIT 무력화
객체에 프로퍼티 순서가 자주 바뀜 내부적으로 새 Hidden Class를 생성해야 함
with, eval 사용 스코프 예측이 어려워 최적화 불가 영역으로 간주됨
클로저 안에서 객체 참조가 바뀌는 패턴 캐시 무효화 발생 → 느려짐

3. 예시 코드

// JIT-friendly
function createUser(name, age){
    return {name, age}
}

// JIT-unfriendly
function createUser(name, age){
    const obj = {}
    if(name) obj.name = name;
    if(age) obj.age = age;
    return obj;
}

4. Hidden Class

V8의 핵심 최적화 전략 중 하나, JS처럼 동적인 언어에서도 정적 언어처럼 빠르게 객체에 접근할 수 있도록 만들어주는 구조

Hidden Class는 V8이 객체 구조를 정적으로 가정하고 최적화하기 위해 내부적으로 생성하는 '클래스 설계도'이다.

4.1 왜 필요한가?

JS는 객체 구조가 동적으로 바뀔 수 있는 언어이다.

const obj = {}
obj.name = "Baek"
obj.age = 32
  • obj는 객체지만, 실제로는 런타임에서만 구조가 결정됨
  • 일반적으로는 프로퍼티 접근 시 obj['name']처럼 처리해야 함 -> 느림

그래서 V8은 객체가 만들어지는 순서와 구조를 분석해서 '이 구조는 이런 프로퍼티를 갖는다'라는 설계도(클래스)
를 내부적으로 생성한다 -> Hidden Class

4.2. Hidden Class 예시

const user = {}      // HiddenClass A 생성 
user.name = "Baek";  // HiddenClass B 생성 (name 있음)
user.age = 32        // HiddenClass C 생성 (name, age 있음)
  • user 객체의 구조가 변경될 때마다 V8은 새로운 Hidden Class를 생성한다.
  • 객체마다 해당 클래스에 대한 포인터를 저장해 둔다.
  • 프로퍼티 접근 시 이 클래스를 참조해서 빠르게 오프셋을 계산한다.

즉, user.name 은 내부적으로 'C클래스에서 name은 0번째 위치'처럼 인덱스로 바로 접근한다.

 

4.2.1. 의문
동적으로 객체를 만들 때 프로퍼티를 하나씩 추가하면,
예를 들어 빈 객체에서 시작해서 3개의 속성을 추가하면 Hidden Class가 3개 생긴다고 들었습니다.

그러면 ABC처럼 계속 생성된다는 건데,
이 Hidden Class들이 계속 메모리에 남는 거라면 너무 비효율적인 거 아닌가요?

어딘가에 저장되고 있다는 건, 결국 리소스 낭비 아닌가요?

그리고 한 번에 객체를 만들 때, 예를 들어
const obj = { name: "", age: "" }
처럼 선언하면 이건 Hidden Class가 생기는 게 아니라 그냥 객체일 뿐인가요?

Hidden Class는 단기적인 메모리 비용을 감수해서 장기적인 실행 속도 최적화를 이끌어내는 구조이며, 오히려 성능 향상에 훨씬 이득이다.

  1. Hidden Class는 항상 생성된다 (정적/동적 상관없이)
    • 객체를 만들 때 V8은 항상 Hidden Class를 생성한다.
    • 즉, { name: "", age: "" }처럼 한 번에 선언하더라도 내부적으로는 Hidden Class가 만들어진다.
    • 이 경우에도 Hidden Class는 생성된다. 다만 초기 상태에서 완성된 구조이기 때문에 추가 전이 없이 Hidden Class 1개만 생성되어 최적화 관점에서 가장 유리한 케이스가 된다.
  2. Hidden Class가 계속 생성되는 것은 맞다.
    • 각각의 상태 (A, B, C)에 대해 Hidden Class를 만들고, 내부적으로 클래스 전이 트리로 연결해 둔다.
  3. 그럼 메모리 낭비 아냐? → 아니다.
    • Hidden Class는 재사용됨. 같은 구조를 가진 객체가 또 생기면 기존 Hidden Class를 그대로 쓴다.
    • 또한 V8은 이 전이 정보를 효율적인 트리 구조로 관리하고, 메모리 낭비를 줄이기 위해 불필요한 클래스는 GC로 제거할 수 있다.
    • 무엇보다, 이 구조 덕분에 obj.name 같은 속성 접근을 일반 객체처럼 느리게 하지 않고, 내부적으로는 정적 오프셋 기반으로 obj[0]처럼 빠르게 접근할 수 있음. → 이게 V8의 JIT 최적화 성능의 핵심
  4. Hidden Class는 객체의 속성 추가 순서가 일치하면, V8은 기존에 만든 Hidden Class 전이 경로를 그대로 재사용한다
const obj = {};
obj.name = 'Kim';     // Hidden Class A → B
obj.age = 30;         // Hidden Class B → C

const obj2 = {};
obj2.name = 'Kimss';  // ❗ A → B (재사용됨)
obj2.age = 31;        // ❗ B → C (재사용됨)

V8 입장에서 보면 obj2도 똑같은 속성 추가 순서로 객체를 만들었기 때문에 Hidden Class A → B → C 전이 경로를 그대로 다시 따라감

포인트 설명
최초 객체 프로퍼티 추가 순서 따라 Hidden Class A → B → C 생성
다음 객체 동일한 순서로 추가되면 기존 전이 트리를 그대로 재사용
V8 입장 “이 구조는 이미 봤다!” → 새 Hidden Class 생성 ❌
장점 메모리 낭비 없이, 빠른 객체 구조 캐시 + JIT 최적화 가능

 

4.3. Hidden Class가 깨지는 경우

구조가 일관되지 않으면 V8은 Hidden Class를 재활용할 수 없고, 매번 새로 만들어야 해서 최적화가 깨진다.

순서 변경, delete, eval, 동적 프로퍼티 등

const user1 = {};
user1.name = 'A';
user1.age = 20;

const user2 = {};
user2.age = 30;
user2.name = 'B';  // 순서가 다르기 때문에 다른 Hidden Class 생성됨

 

4.4 Hidden Class를 잘 활용하기 위해서

  • 프로퍼티는 항상 동일한 순서로 추가하기
    • 속성 추가 순서와 구조가 같다면 Hidden Class는 무조건 재사용
    • 그래서 “객체를 만들 때 항상 같은 순서로, 같은 구조로” 만드는 습관이 V8 JIT 최적화에서 매우 중요한 포인트
  • 객체 구조는 가급적 초기화 시점에 완성하기
  • delete는 사용 자제 → undefined 할당 추천
  • 클래스를 사용하면 Hidden Class를 일관되게 유지하기 쉬움
반응형