V8 엔진의 동작 방식.
masonfe-hyunsu
🧵 1. 개요.

우리가 사용하는 JavaScript는 소스를 읽고, 해석하고, 실행한 다음 실행 빈도가 높은 코드를 최적화하는 과정을 통해 실행된다.
이 과정을 맡는 층을 JS 엔진이라고 부른다. Chrome과 Node가 쓰는 엔진이 V8이다.
이 포스팅에서는 V8이 그 과정을 어떻게 나누고, Heap에 무엇을 두는지 정리해 본다.
요약하면, JavaScript 실행은 소스 해석과 실행, 최적화를 JS 엔진이 맡고, Chrome과 Node가 쓰는 그 엔진이 V8이다.
🧩 2. V8이란.
V8의 일은 한 줄로 말하면 소스 텍스트를 실제로 돌리는 것이다.
파일을 읽고 문법을 해석한다
{ }, 배열, 함수, 클로저 같은 값을 메모리에 만든다실행 빈도가 높은 코드를 더 빠른 형태로 바꾼다
더 이상 안 쓰는 객체를 수거한다(GC, Garbage Collection, 쓰지 않는 메모리를 회수하는 일)
여기서 Node.js와 V8을 같은 말로 쓰면 헷갈린다. Node는 런타임이고, V8은 그 안의 JS 실행기이다.
V8:
const a = {}같은 코드가 의미하는 바를 실행한다Node:
fs,http,npm run build처럼 파일과 프로세스, 바깥 세계를 붙인다

그래서 node app.js를 치면 겉은 Node가 프로세스를 띄우고, 그 안에서 JS를 해석하는 쪽은 V8이다.
Chrome도 페이지 스크립트를 V8로 돌린다. 환경은 달라도 JS를 돌리는 규칙과 메모리 모델은 같은 엔진이다.
배포 중 빌드가 JavaScript heap out of memory로 죽을 때도, 로그가 가리키는 대상은 Node 전체가 아니라 V8 Heap 한도인 경우가 많다. 옵션만 외우기보다 엔진이 메모리를 어떻게 쓰는지 보는 이유가 여기 있다.
⚙️ 3. 실행 파이프라인.
V8은 소스를 받자마자 전부 네이티브 머신 코드로 컴파일하지 않는다. 크게 나누자면 Ignition(인터프리터)과 TurboFan(최적화 컴파일러)이다.

파싱.
먼저 소스를 AST(Abstract Syntax Tree, 코드 구조를 트리로 펼친 것)로 만든다. function add(a, b) { return a + b }를 글자 나열이 아니라 "함수, 인자 둘, 더하기" 같은 구조로 바꿔 두는 단계다. 문법 오류는 대개 여기서 걸린다.
Ignition.
AST를 bytecode(엔진이 바로 실행하는 중간 코드)로 바꾸고, 일단 여기서 실행한다.
바이트코드는 소스보다 기계에 가깝고, 머신 코드보다는 가볍다. 시작이 빠르고, 한 번만 도는 코드까지 비싼 최적화를 하지 않아도 된다. Ignition은 실행하면서 "이 함수가 숫자만 받는다" 같은 타입 힌트도 같이 모은다.
TurboFan.
같은 함수의 실행 빈도가 충분히 높아지면 TurboFan이 그 힌트를 믿고 최적화 머신 코드로 올린다. 가정이 맞으면 훨씬 빠르다. add가 갑자기 문자열을 받으면 가정이 깨지고, 최적화된 코드를 버리고 바이트코드 쪽으로 되돌린다. 이 되돌림이 deoptimize다.
처음부터 전부 최적화하지 않는 이유는 메모리와 컴파일 비용 때문이다. 프론트 빌드처럼 짧은 수명에 파일이 많은 작업은, 최적화된 머신 코드보다 파싱 결과와 중간 객체가 Heap을 먼저 채우는 쪽이 더 자주 보인다.
🧠 4. Heap과 Old Space.
실행 중 만든 객체는 Heap에 올라간다. const user = { name: 'a' }의 { name: 'a' }가 사는 곳이 Heap이다.
함수 안의 let i = 0 같은 지역 값과 호출 순서는 Stack 쪽에 가깝다. heap out of memory 로그가 가리키는 쪽은 Stack이 아니라 Heap이다.
Heap은 세대로 나눈다. 이유는 객체 수명이 제각각이라서다.
New Space(Young): 방금 만든 객체가 모이는 곳. 대부분은 금세 안 쓰이게 되므로, 가벼운 GC가 자주 돌아 비운다
Old Space: 여러 번 살아남은 객체가 옮겨지는 곳. 한도가 크고,
--max-old-space-size가 건드리는 영역이기도 하다
Node.js 메모리 가이드도 Old Space 상한을 이 플래그로 조절하라고 안내한다.
한 가지 더. 서버에 RAM이 남아 있어도 Node 프로세스 안 V8이 쓸 수 있는 Heap에는 별도 상한이 있다. 시스템 메모리와 Heap 한도를 같다고 보면 원인 추적이 빗나간다.

시스템 RAM: 머신, 컨테이너 전체가 쓰는 물리/할당 메모리
V8 Heap 한도: 그 안 JS 객체에 쓸 수 있는 상한
시스템 RAM 부족과 V8 Heap 한도 초과는 다른 문제다.
📈 5. Heap이 커지는 지점.
엔진 입장에서 Heap이 불어나는 조건은 객체를 많이 만들고, 그 참조가 오래 남는 것이다.
프론트 빌드는 그 패턴이 잘 보인다. 모듈을 읽고 AST를 만들고 변환 결과를 한 프로세스 안에 붙들어 두면, 잠깐 쓰인 값이 New Space에서 안 사라지고 Old Space로 넘어간다.
번들, Source Map, minify는 큰 그래프와 문자열을 오래 붙잡는다
의존성, 플러그인, 동시 빌드가 늘면 같은 한도를 더 빨리 채운다
한 번에 다 의하지 말고, 최근에 뭐가 커졌는지부터 보면 된다.
🛠️ 6. 한도에 닿았을 때.
Heap이 한도에 닿으면 V8은 더 할당하지 못하고 프로세스를 죽인다. 로그의 Reached heap limit이 그 지점이다. 머신 전체가 다운된 것이 아니라, 이 Node 프로세스가 V8에 허용된 천장을 넘겼다는 뜻에 가깝다.
대응은 한도 확인 → Heap 조정 → (그래도 남으면) 원인 분석 순으로 본다.
node -e "console.log(require('v8').getHeapStatistics())"heap_size_limit으로 현재 한도를 본다. 컨테이너면 Memory Limit도 같이 본다확인 뒤에는
--max-old-space-size(Old Space 상한, MB 단위)를 올린다
NODE_OPTIONS="--max-old-space-size=4096" npm run build한도를 올릴 때는 머신, 컨테이너의 실제 가용 메모리보다 크게 잡지 않는 편이 안전하다. V8 천장만 키우면 이번엔 호스트 OOM으로 넘어갈 수 있다.

어제까지 되던 빌드가 오늘 죽었다면, Heap을 올리기 전에 최근 변경을 먼저 비교하는 편이 낫다.
Node 버전, 의존성, 빌드 옵션, 컨테이너 limit, 특정 단계의 급격한 메모리 증가만 맞춰도 원인이 꽤 좁혀진다.
✅ 7. 정리.
V8은 JavaScript를 실행하는 엔진이고, Node는 그 위에 바깥 세계를 붙인 런타임이다
소스는 파싱 → Ignition(bytecode) → 실행 빈도가 높은 코드만 TurboFan 순으로 흐른다
객체는 Heap, 오래 사는 값은 Old Space에 모인다
JavaScript heap out of memory= V8 Heap 한도 초과 (시스템 RAM 고갈과 동일시하지 않는다)단기:
heap_size_limit확인 →--max-old-space-size조정 → 가용 RAM 확인한도는 응급처치고, 갑자기 깨졌다면 왜 사용량이 늘었는지를 같이 남긴다
댓글