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. 동작 흐름
- 인터프리터로 전체코드 실행
- 실행 중 프로파일링 ( 자주 쓰이는 코드 확인)
- 자주 쓰이는 코드 (
핫 코드
) 를 JIT 컴파일러가 기계어로 변환 - 이후 이 기계어 코드로 빠르게 실행
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개 생긴다고 들었습니다.
그러면 A → B → C처럼 계속 생성된다는 건데,
이 Hidden Class들이 계속 메모리에 남는 거라면 너무 비효율적인 거 아닌가요?
어딘가에 저장되고 있다는 건, 결국 리소스 낭비 아닌가요?
그리고 한 번에 객체를 만들 때, 예를 들어
const obj = { name: "", age: "" }
처럼 선언하면 이건 Hidden Class가 생기는 게 아니라 그냥 객체일 뿐인가요?
Hidden Class는 단기적인 메모리 비용을 감수해서 장기적인 실행 속도 최적화를 이끌어내는 구조이며, 오히려 성능 향상에 훨씬 이득이다.
- Hidden Class는 항상 생성된다 (정적/동적 상관없이)
- 객체를 만들 때 V8은 항상 Hidden Class를 생성한다.
- 즉, { name: "", age: "" }처럼 한 번에 선언하더라도 내부적으로는 Hidden Class가 만들어진다.
- 이 경우에도 Hidden Class는 생성된다. 다만 초기 상태에서 완성된 구조이기 때문에 추가 전이 없이 Hidden Class 1개만 생성되어 최적화 관점에서 가장 유리한 케이스가 된다.
- Hidden Class가 계속 생성되는 것은 맞다.
- 각각의 상태 (A, B, C)에 대해 Hidden Class를 만들고, 내부적으로 클래스 전이 트리로 연결해 둔다.
- 그럼 메모리 낭비 아냐? → 아니다.
- Hidden Class는 재사용됨. 같은 구조를 가진 객체가 또 생기면 기존 Hidden Class를 그대로 쓴다.
- 또한 V8은 이 전이 정보를 효율적인 트리 구조로 관리하고, 메모리 낭비를 줄이기 위해 불필요한 클래스는 GC로 제거할 수 있다.
- 무엇보다, 이 구조 덕분에 obj.name 같은 속성 접근을 일반 객체처럼 느리게 하지 않고, 내부적으로는 정적 오프셋 기반으로 obj[0]처럼 빠르게 접근할 수 있음. → 이게 V8의 JIT 최적화 성능의 핵심
- 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를 일관되게 유지하기 쉬움
'시작 > TIL(Today I Learned)' 카테고리의 다른 글
웹 성능 최적화 - 애니메이션 최적화 (1) | 2025.04.02 |
---|---|
React Testing Tutorial(9) - findBy (0) | 2024.09.18 |
React Testing Tutorial (8) - getAllBy..., textMatch (0) | 2024.09.11 |
React Testing Tutorial(7) - getByDisplayValue, getByAltText, getByTitle, getByTestId (1) | 2024.09.08 |
React Testing Tutorial(6) - getByLabelText(), getByPlaceholderText(), getByText() (0) | 2024.09.01 |