1.2 JS 엔진과 런타임
JavaScript 엔진이란?
JavaScript 엔진은 JavaScript 코드를 읽고, 분석하고, 실행하는 프로그램입니다. 브라우저마다 자체 JavaScript 엔진을 내장하고 있습니다.
| 엔진 | 브라우저/환경 | 개발사 |
|---|---|---|
| V8 | Chrome, Edge, Node.js, Deno | |
| SpiderMonkey | Firefox | Mozilla |
| JavaScriptCore (Nitro) | Safari, WebKit | Apple |
| Hermes | React Native | Meta |
JS 엔진 동작 원리
JavaScript 코드가 실행되는 과정을 단계별로 살펴봅시다.
1단계: 소스 코드 파싱
엔진은 텍스트로 된 JavaScript 코드를 읽어 **토큰(token)**으로 분리합니다.
// 이 코드가
const x = 10 + 20;
// 다음과 같은 토큰으로 분리됨
// [const] [x] [=] [10] [+] [20] [;]
2단계: AST(추상 구문 트리) 생성
토큰들을 트리 구조의 **AST(Abstract Syntax Tree)**로 변환합니다.
// const x = 10 + 20; 의 AST (간략화)
{
type: "VariableDeclaration",
kind: "const",
declarations: [{
type: "VariableDeclarator",
id: { type: "Identifier", name: "x" },
init: {
type: "BinaryExpression",
operator: "+",
left: { type: "Literal", value: 10 },
right: { type: "Literal", value: 20 }
}
}]
}
3단계: 바이트코드 생성 (인터프리터)
AST를 **바이트코드(bytecode)**로 변환합니다. 바이트코드는 기계어보다 고수준이지만 소스 코드보다 빠르게 실행됩니다.
V8 엔진의 경우 Ignition 인터프리터가 이 역할을 담당합니다.
4단계: JIT 컴파일
자주 실행되는 코드(Hot Code)를 감지하면 JIT(Just-In-Time) 컴파일러가 기계어로 컴파일하여 성능을 향상시킵니다.
V8에서는 TurboFan 최적화 컴파일러가 이 역할을 합니다.
소스 코드
↓ 파싱
AST
↓ 컴파일
바이트코드 ←→ 인터프리터(Ignition)로 실행
↓ Hot Code 감지
최적화된 기계어 ← TurboFan이 JIT 컴파일
V8 엔진 심층 분석
V8은 Google이 개발한 고성능 JavaScript 엔진으로, Chrome 브라우저와 Node.js에서 사용됩니다.
V8의 메모리 구조
┌─────────────────────────────────┐
│ V8 힙 메모리 │
├─────────────────────────────────┤
│ New Space (Young Generation) │ ← 새로 생성된 객체
│ (1~8MB) │
├─────────────────────────────────┤
│ Old Space (Old Generation) │ ← 오래된 객체
│ (기본 최대 1.5GB) │
├─────────────────────────────────┤
│ Code Space │ ← 컴파일된 코드
├─────────────────────────────────┤
│ Large Object Space │ ← 큰 객체 (>512KB)
└─────────────────────────────────┘
가비지 컬렉션
V8은 Mark-and-Sweep 알고리즘으로 더 이상 참조되지 않는 객체를 자동으로 메모리에서 해제합니다.
function createData() {
// 함수 실행 후 아래 객체들은 참조 없어짐 → GC 대상
const bigArray = new Array(1000000).fill(0);
const obj = { data: bigArray };
return obj.data[0]; // 숫자 0만 반환
} // bigArray, obj는 GC가 메모리 해제
let result = createData(); // 0
// result = 0이고, bigArray/obj는 이미 해제됨
브라우저 런타임
JavaScript 엔진 단독으로는 DOM 조작이나 HTTP 요청을 할 수 없습니다. 브라우저가 제공하는 Web API와 함께 동작합니다.
브라우저 런타임 구성 요소
┌─────────────────────────────────────────────────┐
│ 브라우저 │
│ ┌─────────────────┐ ┌────────────────────────┐ │
│ │ JavaScript │ │ Web APIs │ │
│ │ Engine │ │ - DOM │ │
│ │ (V8/Spider │ │ - Fetch API │ │
│ │ Monkey 등) │ │ - setTimeout │ │
│ │ │ │ - Canvas API │ │
│ │ ┌───────────┐ │ │ - Web Storage │ │
│ │ │ Call Stack│ │ │ - WebSocket │ │
│ │ └───────────┘ │ └────────────────────────┘ │
│ └─────────────────┘ │
│ ┌─────────────────────────────────────────────┐ │
│ │ 이벤트 루프 + 콜백 큐 │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
브라우저에서만 사용 가능한 전역 객체
// window 객체 (브라우저의 전역 객체)
console.log(window.innerWidth); // 브라우저 창 너비
console.log(window.location.href); // 현재 URL
// document 객체 (DOM 접근)
const btn = document.querySelector('#myBtn');
btn.addEventListener('click', () => alert('클릭!'));
// navigator 객체 (브라우저/기기 정보)
console.log(navigator.userAgent);
console.log(navigator.language); // 'ko-KR'
// history 객체 (브라우저 히스토리)
history.back();
history.pushState({}, '', '/new-url');
이벤트 루프 (간략 소개)
JavaScript는 싱글 스레드이지만 비동기 처리가 가능합니다. 이벤트 루프가 Call Stack과 Callback Queue를 관리합니다.
console.log('1. 시작');
setTimeout(() => {
console.log('3. setTimeout 콜백 (1초 후)');
}, 1000);
console.log('2. 끝');
// 출력 순서:
// 1. 시작
// 2. 끝
// 3. setTimeout 콜백 (1초 후)
이벤트 루프 상세 내용은 Ch6에서 깊이 다룹니다.
Node.js 런타임
Node.js는 브라우저 밖에서 JavaScript를 실행할 수 있게 해주는 런타임 환경입니다. Ryan Dahl이 2009년 V8 엔진 위에 구축했습니다.
Node.js 구성 요소
┌─────────────────────────────────────────┐
│ Node.js │
│ ┌───────────────┐ ┌─────────────────┐ │
│ │ V8 엔진 │ │ Node.js APIs │ │
│ │ │ │ - fs (파일) │ │
│ │ JavaScript │ │ - http │ │
│ │ 실행 │ │ - path │ │
│ └───────────────┘ │ - os │ │
│ │ - crypto │ │
│ ┌───────────────┐ └─────────────────┘ │
│ │ libuv │ │
│ │ (이벤트 루프 │ │
│ │ + I/O) │ │
│ └───────────────┘ │
└─────────────────────────────────────────┘
libuv란?
libuv는 Node.js의 핵심 라이브러리로, C언어로 작성된 비동기 I/O 라이브러리입니다.
- 이벤트 루프 구현
- 파일 시스템 I/O
- 네트워크 I/O
- 스레드 풀 (CPU 집약적 작업)
Node.js에서만 사용 가능한 기능
// 파일 시스템 접근 (브라우저에서는 불가)
import { readFileSync, writeFileSync } from 'fs';
const content = readFileSync('./data.txt', 'utf-8');
console.log(content);
writeFileSync('./output.txt', 'Hello Node.js!');
// HTTP 서버 (브라우저에서는 불가)
import { createServer } from 'http';
const server = createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World!');
});
server.listen(3000, () => console.log('서버 실행: http://localhost:3000'));
// process 객체 (Node.js 전역)
console.log(process.version); // Node.js 버전
console.log(process.platform); // 'win32', 'linux', 'darwin'
console.log(process.env.PATH); // 환경 변수
브라우저 vs Node.js 비교
| 항목 | 브라우저 | Node.js |
|---|---|---|
| 전역 객체 | window | global, globalThis |
| 모듈 시스템 | ESM (import/export) | ESM + CJS (require) |
| DOM | O | X |
| 파일 시스템 | X | O |
| HTTP 서버 | X | O |
| 네이티브 모듈 | Web APIs | Node.js APIs |
| 샌드박스 | O (보안 제한) | X (제한 없음) |
globalThis로 환경 감지
// 브라우저와 Node.js 모두에서 동작하는 전역 접근
if (typeof window !== 'undefined') {
console.log('브라우저 환경');
console.log(window.location.href);
} else {
console.log('Node.js 환경');
console.log(process.version);
}
// 더 나은 방법: globalThis 사용
console.log(globalThis); // 브라우저: window, Node.js: global
Deno와 Bun: 새로운 JavaScript 런타임
Deno
Ryan Dahl이 Node.js의 설계 실수를 반성하며 만든 새로운 런타임 (2020년 출시).
// Deno - TypeScript 네이티브 지원, URL 기반 임포트
import { serve } from "https://deno.land/std@0.200.0/http/server.ts";
serve((req) => new Response("Hello Deno!"), { port: 8000 });
특징:
- TypeScript 기본 지원
- 보안 샌드박스 (명시적 권한 필요)
- Web API 표준 준수
- npm 호환 (Deno 1.28+)
Bun
Zig 언어로 작성된 초고속 JavaScript 런타임 (2022년 출시).
# 속도 비교 (간단한 HTTP 서버 기준)
# Node.js: ~10만 req/sec
# Deno: ~12만 req/sec
# Bun: ~20만+ req/sec
특징:
- Node.js 호환
- 내장 번들러, 테스트 러너
- 매우 빠른 실행 속도
고수 팁
V8 최적화를 위한 코딩 패턴
V8 엔진은 Hidden Class와 Inline Cache 기법으로 객체 접근을 최적화합니다. 이를 위해:
// 좋은 예: 항상 같은 순서로 프로퍼티 초기화
class Point {
constructor(x, y) {
this.x = x; // 항상 x 먼저
this.y = y; // 항상 y 다음
}
}
// 나쁜 예: 동적으로 프로퍼티 추가 (Hidden Class 변경)
const p1 = {};
p1.x = 1;
p1.y = 2; // Hidden Class 변경 → 최적화 해제
// 숫자 타입 유지 (V8은 Smi, Double, Object 등 내부 표현 사용)
const arr = [1, 2, 3]; // Smi 배열 (빠름)
arr.push('text'); // Object 배열로 변경 (느려짐)
arr.push(1.5); // Double 배열로 변경
devtools로 엔진 동작 확인
Chrome DevTools의 Performance 탭으로 V8의 JIT 컴파일, GC 동작, 스크립트 실행 시간 등을 시각적으로 확인할 수 있습니다.