목차

이 아티클을 읽게 된 계기는, 자바스크립트를 실행 가능한 코드로 변환하기 위해선 V8 같은 엔진이 필요하고 파싱, 최적화, 기계어 변환 과정을 거친다는 과거의 기억에 대한 궁금증을 해소하고 싶었기 때문이다. 따라서 이번 아티클을 통해 엔진 메커니즘을 깊이 이해해 추후에 성능 개선과 디버깅에 활용하고자 한다.
이 글에선 아티클의 핵심 내용과 이해 과정에서 마주한 익숙하지만 설명하기 어렵거나, 생소한 용어들에 대해 추가로 정리하였다.
(이 글은 노션에서 처음 작성되었으며, 노션의 원문 스타일이 블로그에 완전히 적용되지 않아, 일부 가독성이 떨어질 수 있는 점 양해 부탁드립니다.)
자바스크립트는 엔진에 의해 처리된다.
자바스크립트가 실행되기 위해선 엔진이 필요하며, 실행에 필요한 모든 것들은 엔진에 의해 처리된다. 이를 담당하는 엔진 종류 중 하나가 바로 구글에서 개발한 자바스크립트 및 웹어셈블리 엔진인 C++로 작성된 V8이다.
V8은 다양한 환경에서 사용될 수 있다.
- 크롬 또는 유사한 환경
- Node.js
- 브라우저 이외에 독립적 실행
- C++ 애플리케이션 속 임베드
최신 웹 브라우저에서 실행할 수 있는 새로운 유형의 코드로, C/C++, Rust 등의 소스 언어를 효과적으로 컴파일하도록 고안되었다.
자바스크립트를 대체하기 위해 탄생한 것은 아니며, 나란히 돌아가면서 서로의 부족한 점을 보완해 두 언어의 강점을 동시에 취하기 위해 설계되었다. (자바스크립트만으로는 고성능 연산과 다양한 언어 지원에 한계가 있다.)
왜 V8일까?
프로그래밍 언어의 추상화 레벨을 나타내며,
아래로 내려갈수록 더 많은 책임이 따른다.
(+ 시스템에 대한 더 많은 권한, 제어를 가진다.)
- C/C++ → 어셈블리어 ⇒ 컴파일러 필요
- 어셈블리어 → 기계어 ⇒ 어셈블러 필요
- 자바스크립트 → 실행 가능한 코드 ⇒ 자바스크립트 엔진
스파이더몽키 vs V8
스파이더몽키(파이어폭스 엔진)
자바스크립트 → 바이트 코드 → 기계어
V8
자바스크립트 → 기계어
Node.js에서는 어떻게 사용될까?
V8 소스코드를 살펴보면 크롬의 document
객체, require()
등의 기능을 찾을 수 없다.
그 이유는 Node.js와 크롬에서 이 기능들을 C++로 구현하고,
V8을 사용해 자바스크립트 함수로 바인딩하기 때문이다!!
왜 이런 방식을 택했을까?
자바스크립트는 고수준 언어라 접근이 불가능하기 때문이다.
대신 C/C++을 통해 저수준 리소스에 직접 접근해 원하는 작업을 수행하는 것이다.
따라서 위에서 언급한 기능은 Node.js 소스 코드에서 찾아볼 수 있다.
-
실제 Node.js의 소스코드 (
require
,resolve
등)function require(path) { try { // 모듈을 불러올 때 requireDepth(깊이) 카운터를 증가시킴 // => 중첩된 require 호출(순환 참조 등) 상황을 추적하는 용도 exports.requireDepth += 1; // 실제 모듈 로딩은 mod.require(path)를 통해 수행 // mod는 현재 모듈 객체(Module 인스턴스)를 가리킴 return mod.require(path); } finally { // require가 끝나면 depth 카운터를 감소시켜 정리 exports.requireDepth -= 1; } } --- function resolve(request, options) { // 첫 번째 인자(request)가 문자열인지 검증 validateString(request, 'request'); // Module._resolveFilename을 호출해 실제 파일 경로를 계산 // - request: 'fs' 같은 내장 모듈 이름, './foo' 같은 상대 경로 등 // - mod: 현재 모듈 객체 // - false: isMain 여부 (여기선 false) // - options: 사용자 전달 옵션 (경로 기준 디렉토리 등) return Module._resolveFilename(request, mod, false, options); } require.resolve = resolve; // require.resolve()로 경로 확인 가능하도록 연결
-
추가자료 - Node.js에서 설명하는 V8 엔진
여기서 눈여겨 봐야할 점은 이러한 방식(V8이 크롬과 Node.js에서 쓰이는 방식)은 다른 사람들도 자신의 사례에 맞게 적용과 사용이 가능하다. 다시 말해, C++로 기능 → 자바스크립트 함수로 바인딩 → 노출하는 방식이 가능하다. (그렇기에 자바스크립트가 로봇 공학 분야 등 다양한 분야에서 사용된다.)
V8은 어떻게 동작할까?
V8은 코드를 두 단계로 컴파일한다.
-
코드 → 기계어 (빠르게 컴파일 but 최적화 X)
- ⬆️ 현재 단계로도 자바스크립트 실행 충분
- 동시에 최적화 코드가 컴파일 (느림 but 훨씬 최적화) → 완료 시 다음 단계를 진행
-
자바스크립트 → 최적화 컴파일 코드로 전환 (또 두 단계를 거침)
-
이그니션 (빠른 저수준 레지스터 기반 인터프리터)
⇒ 추상 구문 트리(AST) → 바이트 코드 생성 (이그니션만으론 한계 존재… → 이때 터보팬 사용)
-
터보팬 (최적화 컴파일러)
⇒ 어떤 함수가 자주 사용 → 훨씬 빠른 최적화
-
JIT 컴파일레이션 (새로운 접근방식, 인터프레테이션 + 컴파일레이션의 장점을 결합)
-
단계별로 정리하자면
V8의 동작과정 요약
-
V8이 자바스크립트 코드 이해를 위해 소스 코드 파싱 + 추상 구문 트리(AST)로 변환 ⇒ V8이 더 쉽게 처리 가능한 형태 (이 과정에서 스코프 함께 생성됨)
-
AST + 스코프 기반으로 → 인터프리터 → 바이트 코드 생성
- 이 시점에서 엔진은 코드 실행 + 타입 피드백(어떤 타입의 값들이 들어왔는지) 수집 (즉, 실행 단계에서 코드에 대한 타입 피드백을 제공)
-
(실행 속도를 높이기 위해) 바이트 코드 + 피드백 데이터 → 최적화 컴파일러(터보팬) 전달될 수도 있음
-
최적화 컴파일러 → 고도로 최적화된 기계어 생성
-
최적화 vs 비최적화 기계어 예시(V8 공식문서)
공식문서를 이해시킨 뒤 GPT에게 예시를 요청함 (틀린 예시일 수 있음)// 범용(비최적화) 기계어 스타일 ; a와 b 타입 검사 if (typeof a !== 'number' || typeof b !== 'number') { goto generic_add_handler ; 문자열이면 concat, 객체면 toString 등 } ; 숫자일 경우만 처리 result = a + b return result --- // 최적화된 기계어 스타일 (TurboFan 결과) ; 타입 피드백: a, b는 항상 number ; 따라서 타입 체크 생략 result = a + b return result
-
-
이 과정은 병렬로 처리, 자주 사용되는 바이트 코드 → 핫 코드로 표시 → 더 효율적인 기계어로 변환
-
바이트 코드가 아닌 기계어를 직접 사용하지 않는 이유는?
- 기계어 → 많은 양의 메모리가 필요
- 속도: 기계어 > 바이트 코드 ⇒ X (항상 빠른건 아님)
- 기계어: 실행 속도 ⬆️, 컴파일 속도 ⬇️
- 바이트 코드: 실행 속도 ⬇️, 컴파일 속도 ⬆️
-
-
-
만약 어느 시점에서 최적화 컴파일러(터보팬)의 가정 중 하나라도 잘못된다면 → 최적화 취소 → 다시 인터프리터로