JavaSript Engine
브라우저 구조를 이해하기 위해서는 JavaScript 엔진이 무엇인지, 어떻게 동작하는지를 파악하는 것이 시작이다. JavaScript 엔진은 시스템에서 JavaScript 코드를 실행하기 위한 핵심 요소다. 옛날에는 단순한 인터프리터였지만, 현대의 JavaScript 엔진은 성능 최적화를 위해 여러 기능이 생긴 복잡한 프로그램이 되어버렸다. 대표적으로 최적화 컴파일러와 JIT(Just-In-Time) 컴파일이 이에 해당한다.
현재 주로 사용되는 JavaScript 엔진
- V8: Google에서 개발한 오픈소스 고성능 JavaScript & WebAssembly 엔진.
Chrome - SpiderMonkey: Mozilla 재단에서 개발한 JavaScript & WebAssembly 엔진.
Firefox - Chakra: Microsoft가 IE와 초기 Edge를 위해 개발한 엔진
- JavaScriptCore: Apple이 개발한 Webkit 기반 엔진.
Safari
복잡한 엔진들이 필요한 이유
JavaSript는 원래 가볍고, 인터프리트되며, 객체지향적인 스크립트 언어로 설계됐다. 인터프리트 언어는 코드를 한 줄씩 읽고 바로 결과를 반환하기에 별도의 컴파일 과정 없이 브라우저에서 실행이 가능하다. 하지만 이런 동작 과정은 성능 측면에서 현저히 불리하다.
이때 등장한게 바로 JIT 컴파일이다. JavaScript 코드를 먼저 Bytecode라는 중간 형태로 변환한 뒤, 이걸 JIT 컴파일러가 최적화해서 성능을 더 끌어올린다. 덕분에 JavaScript는 인터프리터 언어이면서도 높은 성능을 챙길 수 있다.
각 엔진마다 내부에서 사용하는 컴파일러와 최적화 기법은 다를 수 있지만, 근본적인 구조는 EcmaScript 사양을 기반으로 구현된다. 이 사양은 브라우저 간 동일한 JavaScript 코드가 일관되게 동작하도록 정의한 표준이다.
JavaScript 코드 실제 동작 과정
JavaScript 코드가 실행되는 과정을 좀 더 직관적으로 이해하기 위해, 아래에 JavaScript 엔진의 컴파일 파이프라인, 즉 전반적인 처리 흐름을 간단히 다이어그램 형태로 정리했다.

다소 복잡해 보이지만, 하나하나 뜯어보면 생각보다 단순하기 때문에 걱정할 필요 없다.
1. Parser
JavaScript 코드를 실행하면, 제일 먼저 해당 코드가 JavaScript 엔진에 전달되고 Parsing이 시작된다. 이 단계에서는 코드가 다음과 같은 과정을 거친다
토큰화
코드는 먼저 식별자, 숫자, 문자열, 연산자 등과 같은 “토큰”으로 분류된다.
Example:
var num = 42는var,num,=,42로 세분화되고 각 ‘토큰’ 또는 항목은 해당 유형으로 태그가 지정되므로 이 경우 키워드,식별자,연산자,번호가 된다.
Abstract Syntax Tree (AST)
코드가 토큰으로 파싱되면 파서는 해당 토큰을 AST로 변환한다. 이 부분을 “Syntax Analysis”이라고 하며, 코드에 문법가 없는지 확인하는 작업을 진행한다.
위의 코드 예시를 사용하면 해당 AST는 아래처럼 표시된다.
{
"type": "VariableDeclaration",
"start": 0,
"end": 13,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 12,
"id": {
"type": "Identifier",
"start": 4,
"end": 7,
"name": "num"
},
"init": {
"type": "Literal",
"start": 10,
"end": 12,
"value": 42,
"raw": "42"
}
}
],
"kind": "var"
}2. Interpreter
AST가 생성되면 인터프리터로 전달되어 AST를 실행하고 바이트코드를 생성한다. 바이트코드가 생성되면 바이트코드가 실행되고 AST가 삭제된다.
- V8용 바이트코드 목록은 여기에서 확인할 수 있다.
var num = 42;의 바이트코드는 다음과 같다:
LdaConstant [0]
Star1
Mov <closure>, r2
CallRuntime [DeclareGlobals], r1-r2
LdaSmi [42]
StaGlobal [1], [0]
LdaUndefined
Return3. Compiler
컴파일러는 optimize해야 할 코드를 모니터링하고 감시하는 “Profiler”라는 것을 사용하여 미리 작업한다. “hot function”라고 알려진 것이 있는 경우 컴파일러는 해당 함수를 가져와 실행할 최적화된 머신 코드를 생성한다. 그렇지 않으면 최적화되었던 ‘hot function’이 더 이상 사용되지 않는다고 판단되면 다시 바이트코드로 ‘deoptimize’한다.
V8 Engine
구글의 V8 자바스크립트 엔진에서도 전체적인 컴파일 흐름은 앞서 설명한 일반적인 자바스크립트 엔진과 비슷한 구조를 따르고 있다.
하지만 V8에는 다른 엔진들과 차별화되는 요소가 있는데, 바로 “비최적화(Non-Optimizing)” 컴파일러가 있다는 점이다.
이 컴파일러는 2021년에 새롭게 도입되었고, 성능과 속도 간의 균형을 맞추는 역할을 한다.
V8의 각 구성 요소는 각각 이름이 있으며, 다음과 같은 역할을 수행한다:
V8 엔진의 경우 전체적인 컴파일 흐름은 일반적인 엔진과 비슷하다. 다른 부분은 2021년에 추가된 “Non-Optimizing” 컴파일러다. 해당 컴파일러는 성능과 속도의 밸런스를 잡는 역할을 한다.
V8 엔진의 구성 요소는 아래와 같다.
- Ignition: V8의 레지스터 기반 인터프리터로, 자바스크립트 코드를 바이트코드로 변환하는 역할을 한다.
- SparkPlug: V8의 비최적화 컴파일러로, Ignition이 생성한 바이트코드를 받아 그대로 순회하면서 각각의 바이트코드에 대해 바로 머신 코드를 생성한다.
- TurboFan: V8의 최적화 컴파일러로, 바이트코드를 정교하게 분석한 뒤 더 복잡하고 정교한 최적화 기법을 적용해 고성능의 머신 코드를 생성한다. 여기서 JIT(Just-In-Time) 컴파일이 본격적으로 작동하며, 실행 중에 수집된 정보를 활용해 코드 성능을 극대화한다.
V8의 전체 컴파일 흐름을 아래와 같다.

Conclusion
글을 다 보아도 컴파일러, 최적화 같은 개념이 잘 이해되지 않아도 괜찮다. 이번 글의 목적은 전체 흐름을 완벽히 이해하는 것이 아닌, 엔진이 어떻게 동작하는지에 대한 큰 그림을 잡는 것이기 때문이다.
V8의 컴파일 흐름과 각 구성 요소에 대한 자세한 설명은 다음에 따로 글을 작성할 예정이다.