JavaScript 는 동적 타입 언어인데도 V8 / SpiderMonkey 같은 모던 엔진은 거의 native 속도로 실행한다. 어떻게? 그리고 var 가 hoisting 되는 이유, return 다음 줄에 객체 시작{ 박으면 깨지는 ASI 함정, for (...) 가 micro-task / macro-task 보다 먼저 실행되는 event loop — 이 모든 게 parser → AST → optimizer → bytecode → JIT 파이프라인의 결과. 이 가이드는 JavaScript 가 어떻게 파싱·실행되는지 정리한다.
전체 파이프라인
source bytes
↓
1. Lexer — character stream → token stream
↓
2. Parser — token → AST (abstract syntax tree)
↓
3. Bytecode — AST → V8 의 경우 Ignition bytecode
↓
4. Interpreter — bytecode 실행 (start fast)
↓
5. Profiler — hot function 감지
↓
6. JIT — TurboFan / Maglev 로 machine code 컴파일
↓
machine code 실행V8 (Chrome / Node), SpiderMonkey (Firefox), JavaScriptCore (Safari) — 세 엔진 모두 비슷한 구조. 단, 단계 이름·최적화 디테일 다름.
1. Lexer — source → token
source:
const x = 42 + foo();
tokens:
KEYWORD const
IDENT x
PUNCT =
NUMBER 42
PUNCT +
IDENT foo
PUNCT (
PUNCT )
PUNCT ;공백·주석 무시. 한 character 씩 state machine 진행. JavaScript 의 lexer 가 정규 expression 보다 어려운 이유 — 같은 글자가 context 따라 다른 token:
/abc/ ← regex 또는 나누기?
context: 변수 뒤 (a = /abc/) → 나누기
statement 시작 또는 ( 뒤 → regex2. Parser — token → AST
source:
function add(a, b) {
return a + b;
}
AST 일부:
FunctionDeclaration
├── name: "add"
├── params: [a, b]
└── body: BlockStatement
└── ReturnStatement
└── BinaryExpression
├── op: "+"
├── left: Identifier(a)
└── right: Identifier(b)Lookup — astexplorer.net에서 임의 JavaScript 의 AST 즉시 표시. Babel / ESLint / TypeScript / Prettier 가 모두 같은 AST 기반. JavaScript 포매터 도.
ASI — Automatic Semicolon Insertion 의 함정
JavaScript 가 ; 생략 허용. 그러나 parser 가 추측하는 규칙이 함정:
// 의도 — 객체 반환
function f() {
return
{
name: "Alice"
};
}
// 실제 동작 — ASI 가 return 뒤에 ; 박음
function f() {
return; // ← undefined 반환!
{ name: "Alice" }; // ← unreachable
}규칙 — newline 이 있으면 ASI 가 ; 박음 (단, return, throw, break,continue, ++, -- 뒤).
해결 — 객체 반환 시 같은 줄에서 { 시작 또는 명시적 ; 사용.
3. Hoisting — parser 단계의 결과
// 코드 순서
console.log(x); // undefined (에러 X)
console.log(y); // ReferenceError
var x = 1;
let y = 2;parser 가 scope 분석 시 변수 선언을 미리 발견 → hoisted. 그러나:
var— 선언 +undefined초기화 hoistlet/const— 선언만 hoist, 초기화는 실제 라인까지. 그 사이 = temporal dead zone- function declaration — 전체 hoist (선언 + body)
- function expression —
var와 동일 (undefined 초기화)
4. Bytecode — Ignition
V8 의 Ignition 은 register-based VM. AST 직접 실행 (interpretation) 은 너무 느려서 bytecode 로 컴파일:
source:
function add(a, b) {
return a + b;
}
ignition bytecode (간략):
Ldar a1 // a 를 accumulator 로
Add a2 // accumulator += b
Return // accumulator 반환bytecode 는 source 보다 작고 빠름. 그러나 native code 보다 느림 — hot function 만 JIT compile.
5. JIT — TurboFan / Maglev
V8 의 JIT 계층 (2024 기준):
- Ignition — 모든 코드를 bytecode 로. start 빠름.
- Sparkplug (2021) — bytecode → simple machine code. 빠른 baseline JIT.
- Maglev (2023) — mid-tier optimizing JIT. inline cache 활용.
- TurboFan — top-tier optimizing JIT. 최강 최적화, compile 시간 가장 길음. hot loop 만.
Profiler 가 hot function 감지 → 더 높은 tier 로 promotion. deoptimization (가정 깨지면 lower tier 로 down) 도 자주.
Hidden Class — 동적 객체를 빠르게
JavaScript 객체는 동적 (필드 추가 가능):
const a = {x: 1};
a.y = 2; // 추가
a.z = 3; // 추가Naive 구현 — 매번 dict lookup. 느림. V8 의 hidden class 트릭:
a = {x: 1} → Class C0 [x: offset 0]
a.y = 2 → Class C1 [x: offset 0, y: offset 4]
a.z = 3 → Class C2 [x: offset 0, y: offset 4, z: offset 8]같은 모양의 객체는 같은 hidden class. obj.x access 가 dict lookup 이 아닌 fixed offset → C++ struct 수준 속도.
Performance 함정
// Bad — 순서 다른 객체 → 다른 hidden class
function makeUser(name, age) {
const u = {};
if (name) u.name = name;
if (age) u.age = age;
return u;
}
makeUser("Alice", 30) → {name, age} → class A
makeUser(null, 30) → {age} → class B
makeUser("Alice") → {name} → class C
// 3 가지 hidden class → polymorphic site → JIT 비최적화
// Good — 항상 같은 모양
function makeUser(name, age) {
return {name: name || null, age: age || null};
}
// 항상 class X → monomorphic site → JIT 최적화Inline Cache — property access 최적화
function getName(obj) {
return obj.name;
}
// 매 호출마다:
// 1. obj 의 hidden class lookup
// 2. class 안 "name" 의 offset lookup
// 3. memory access
// JIT 가 첫 호출 후 "이 site 는 항상 class X" 학습 → inline cache 저장
// 다음 호출 — class 확인만, offset 은 cache → 1 step같은 함수에 다른 hidden class 객체 전달 = polymorphic / megamorphic → cache miss → 느림. 일관된 객체 모양 권장.
Event Loop — 비동기의 핵심
while (true) {
if (callStack.empty) {
if (microtaskQueue.notEmpty) {
runMicrotask(); // Promise.then, queueMicrotask
} else if (macrotaskQueue.notEmpty) {
runMacrotask(); // setTimeout, setInterval, I/O
} else {
sleep();
}
}
render(); // 매 frame
}Microtask vs macrotask 우선도. 같은 tick 에서 microtask 가 모두 처리된 후 macrotask 1 개 → render.
console.log("1");
setTimeout(() => console.log("2"), 0); // macrotask
Promise.resolve().then(() => console.log("3")); // microtask
console.log("4");
출력: 1, 4, 3, 2
│ │ │ │
│ │ │ └─ macrotask
│ │ └──── microtask (현재 tick 끝)
│ └─────── sync
└────────── syncClosure — scope chain 의 결과
function counter() {
let n = 0;
return () => ++n;
}
const inc = counter();
inc(); // 1
inc(); // 2inner function 이 outer 의 n 을 capture. parser 가 scope chain 만들 때 inner 가 outer 변수 참조하면 outer 의 stack frame 을 heap 으로 promote (escape analysis).
memory 누수 위험 — outer 가 큰 객체 capture 시 inner 가 살아있는 동안 GC X.
Bundler·Minifier 와 parser
- Babel — 옛 JS → 새 JS transform. parser → AST 변형 → re-emit
- esbuild / swc — Go/Rust 로 작성된 fast parser/ bundler. JS native parser 보다 10-100 배 빠름
- Terser — minifier. parser → AST → name mangling / dead code elimination → re-emit
- Prettier / formatter — parser → AST → 들여쓰기 rule 로 re-emit
JavaScript 포매터 도 같은 패턴 — parser → AST → prettify. JS Object → JSON 는 JS 객체 리터럴 (unquoted key, trailing comma) → 표준 JSON 변환. parser 가 relaxed JS 문법 받아 표준 JSON 출력.
흔한 함정
1. ASI 의 invisible bug
앞서 본 return + 새 줄. ++ / -- 도 동일:
a
++b // a 가 ++b 의 결과로 평가? X — a; ++b; 로 ASI2. polymorphic hidden class
앞서 본 객체 모양 다양화 → JIT 비최적화.
3. delete obj.x
delete obj.x;
// hidden class 변경 + dictionary mode 로 전환 → 모든 access 느려짐
// 대신
obj.x = undefined;
// hidden class 그대로4. Array 의 hole
const arr = [1, 2, 3];
arr[10] = 100;
// 길이 11, 인덱스 3-9 는 hole (sparse)
// V8 가 dictionary mode 로 전환 → 느려짐
// 대신
const arr = new Array(11).fill(undefined);
arr[10] = 100;
// dense, 빠름5. eval / with — JIT 비활성화
eval() / with 가 scope 동적 변경 → parser 가 정적 분석 못 함 → 해당 function 의 JIT 포기. 모던 코드 에서는 사용 X.
참고 자료
- V8 Ignition·TurboFan blog — v8.dev
- ECMAScript spec (ASI) — TC39
- AST Explorer — astexplorer.net
- V8 hidden class explanation — v8.dev
요약
- source → lexer → parser → AST → bytecode → interpreter → (hot 시) JIT → machine code.
- ASI 가 newline + return/throw/break/continue 뒤 자동
;박음. 객체 반환 시 같은 줄에서{. - Hoisting — var (선언 + undefined) / let·const (TDZ) / function (full) / function expression (var 와 동일).
- V8 의 hidden class — 같은 모양 객체 = 같은 class → monomorphic site → JIT 최적화. polymorphic 회피.
- Event loop — sync → microtask → macrotask → render. Promise.then 이 setTimeout 보다 먼저.
- Closure = outer 의 변수를 inner 가 capture → outer frame 이 heap 으로 promote.
- esbuild / Babel / Prettier 모두 같은 parser → AST → emit 패턴.
- 실험 — JavaScript 포매터 / JS Object → JSON / astexplorer.net (외부) 에서 AST 직접 보기.