Js는 인터프리터 언어일까, 컴파일 언어일까?
V8엔진이 JS 소스코드를 실행하는 과정을 통해 알아보는 자바스크립트의 정체, 그리고 컴파일러, 트랜스파일러와의 비교
JavaScript (JS) is a lightweight interpreted (or just-in-time compiled) programming language with first-class functions. - MDN
I think it's clear that in spirit, if not in practice, JS is a compiled language. And again, the reason that matters is, since JS is compiled, we are informed of static errors (such as malformed syntax) before our code is executed. That is a substantively different interaction model than we get with traditional "scripting" programs, and arguably more helpful! - You don't know JS
책 You Don't Know JS Yet에서, 저자는 오늘날에는 프로그램의 배포 방식이 더 이상 중요하지 않다고 하는 주장은 무시해야 한다고 말한다. 인터프리터 언어인지에 따라 프로그램의 배포 방식이 달라지고, JS가 인터프리터 언어인지, 컴파일 언어인지 명확히 이해하고 있어야 JS가 오류를 어떻게 처리하는지를 알 수 있기 때문이다. 그리고 완전한 사실은 아니지만, 자신은 JS를 컴파일 언어라고 생각한다고 한다. 도대체 그래서 JS는 인터프리터 언어라는 건지, 컴파일 언어라는 건지 머릿속이 복잡해져 버렸다. 이 오랜 논쟁에서 가장 최근의 중론이 무엇이고 왜 그런지, 그리고 YDKJSY 저자는 왜 JS를 컴파일 언어라고 생각한다고 주장하는지 정리해보기 위해 글을 쓰게 되었다. 처음에는 '그래서 JS는 무슨 언어인지' V8엔진의 동작과정을 통해 이해해보고, 두번째로 타입스크립트의 컴파일 과정과의 비교, 그리고 마지막으로 JS 소스코드 실행 전 트랜스파일러의 동작과정과 비교해보는 순서로 글을 구성하였다.
1. JS의 인터프리터 vs 컴파일러 논쟁과 결론
1.1 JS는 인터프리터 언어라고 주장하는 이유
- 초창기 실행 방식 : 초창기 JS 엔진들은 인터프리터 방식을 사용했으며, 이 때문에 JS는 인터프리터 언어로 분류되었다.
- 컴파일 시점 : 모던 브라우저들의 JS 엔진 동작과정에서는 '컴파일' 단계를 거치나, 이는 정적 컴파일러와 다르게 '런타임'에 수행되므로 JS는 인터프리터 언어라는 주장.
Netscape의 Navigator나, Microsoft의 JScript 등 초창기 자바스크립트 엔진들은 전통적인 인터프리터 방식으로 런타임에 JS 소스 코드를 한 줄씩 실행했기 때문에 JS는 처음에 인터프리터 언어로 분류되었다. 그러나 최근의 브라우저에서 사용되는 대부분의 엔진들은 컴파일 방식의 장점과 인터프리팅 방식의 장점을 혼합한 JIT(Just-In-Time) 컴파일러로 JS 소스 코드를 실행한다. JIT 컴파일러는 런타임 중에 최적화가 필요한 코드를 수집해 컴파일 과정을 거치게 된다. (자세한 동작방식은 아래 1.3에서 설명) 하지만 '컴파일' 과정을 거쳐서 JS 소스 코드가 실행된다 하더라도, 이는 정적 컴파일러처럼 런타임 이전에 컴파일이 수행 되는 것이 아니라 런타임에 수행되는 것이므로, 여전히 JS는 인터프리터 언어라고 분류되기도 한다. 일반적으로 컴파일 언어는 프로그램 실행 이전에 파싱->컴파일->실행 파일 생성 단계를 거치고 프로그램 실행 시 코드를 실행하는데, JS 소스 코드의 실행 과정과 이는 거리가 멀어보이기도 한다.
1.2 JS는 컴파일 언어라고 주장하는 이유
- JIT 컴파일러 : 최근의 JS 엔진들은 JS 소스 코드를 파싱과 컴파일을 거쳐 코드를 실행한다.
- 컴파일러 최적화 : JIT 컴파일 과정에서 실행 시간 최적화가 이루어지므로, 자바스크립트는 실행 전에 "컴파일" 단계를 거치는 현대적인 컴파일 언어의 일부 특성을 가진다.
근래의 JS 엔진들은 텍스트 형태의 JS 소스 코드를 파싱하여 추상 구문 트리(AST)로 변환하여, JIT 컴파일러를 통해 AST를 ByteCode로 변환한다. 여기서 ByteCode란 기계어를 추상화한 코드로, 고수준 언어와 저수준 언어 사이의 중간 언어(IR) 형태로 JS 가상 머신이 한결 편하게 코드를 실행할 수 있는 형태이다. 그리고 ByteCode를 실행하면서 코드의 실행 정보를 프로파일링하여 최적화가 필요한 코드를 수집해 기계어로 컴파일한다. 자세한 동작과정은 아래에서 크롬 브라우저와 Node.JS의 JS 런타임(실행 환경)인 V8엔진의 동작방식을 통해 살펴보자.
1.3 V8 엔진의 동작방식(v9.1 기준)
아래의 그림은 V8엔진의 전체 동작 과정을 나타낸 도표이다. 전반적인 흐름을 우선 간략히 요약한 후 각각의 단계를 이어서 세부적으로 설명하려고 한다.

- V8엔진은 JS 소스 코드를 가져와 Parser에게 전달한다.
- Parser는 소스 코드를 추상 구문 트리(AST)로 변환한다.
- Parser는 Lexical Analysis 과정을 통해 코드를 토큰으로 분해한다.
- 이어서 Parser는 Syntax Analysis 과정을 진행한다. (이 때 문법 에러가 발견될 경우 에러 메세지 출력)
- Parser는 위 두 과정을 거쳐 소스 코드를 추상 구문 트리(AST)로 만들어낸다.
- Parser는 AST를 Ignition으로 전달한다.
- Ignition은 전달받은 AST를 기반으로 ByteCode를 생성한다. (이 때 실행 컨텍스트가 생성됨)
- Ignition은 ByteCode를 실행하고, 이 때 실제 JS 코드가 실행된다.
- Ignition은 코드를 실행하면서 프로파일링 및 피드백 데이터를 수집하며, 수집된 정보들을 바탕으로 반복 사용 여부, 코드의 양, 그 외 여러 조건에 따라 TurboFan 컴파일러가 컴파일할지, SparkPlug 컴파일러가 컴파일 할 지 결정된다.
1.3.1 Parser
V8엔진이 JS 소스 코드를 가져와서 Parser에게 전달하면, Parser는 Lexical Analysis(어휘 분석)과정과 Syntax Analysis(구문 분석)과정을 거쳐 AST를 만들어내고, AST를 Ignition에 전달한다.
Lexical Analysis(어휘 분석)

Lexical Analysis 과정에서는 코드를 토큰으로 분해한다. 여기서 토큰이란 문법적인 의미를 가지며, 문법적으로 더 이상 나눌 수 없는 코드의 기본 요소(키워드, 식별자, 세미콜론, 마침표 등)를 말한다. 코드를 토큰화하는 예시는 여기에서 확인해볼 수 있다.
Syntax Analysis (구문 분석)

Parser는 앞서 만든 토큰의 구문 분석(Syntax Analysis) 과정을 진행하게 된다. 이 때 만약 문법 에러가 있다면 에러 메세지를 출력하게 된다. 위에서 예시로 든 에러 메세지처럼 SyntaxError와 Unexpected token이 에러 메세지에 출력된다면 이제는 내 코드가 V8 엔진의 Parser가 Syntax Analysis 과정을 수행하는 중에 오류가 발생한 것임을 알 수 있다.
AST 생성

구문 분석에서 에러가 없다면, Parser는 위 사진과 같은 형태의 추상 구문 트리(AST)를 만들고 이를 Ignition에 전달한다.
1.3.2 Ignition
V8 엔진의 Ignition은 빠른 속도로 ByteCode를 생성하는 인터프리터이다. Ignition은 컴파일러처럼 모든 소스 코드를 한번에 컴파일하는 대신, 코드를 한 줄씩 실행될 때 마다 ByteCode로 변환하고 ByteCode를 해석한다.
Ignition이란 이름은 ByteCode 생성기와 ByteCode 해석기(Interpreter) 모두를 나타낸다. ByteCode 생성기는 Parser가 생성한 AST를 가져와서 ByteCode를 생성하고, ByteCode 해석기는 생성된 ByteCode를 가져와 ByteCode 핸들러 세트로 전송하여 해석하고 실행한다. (참고)
AST -> ByteCode 변환, 실행

Ignition은 Parser로부터 전달받은 AST를 고수준 언어와 저수준 언어 사이의 중간 언어 형태인 ByteCode로 변환하고 실행시킨다. 이 때 ByteCode가 실행되기 바로 전 단계에서 실행 컨텍스트가 생성되어 호이스팅, this 바인딩, 외부 렉시컬 환경에 대한 참조의 결정 등이 이뤄지게 된다.
프로파일링 & 피드백 데이터 수집
Ignition은 ByteCode를 실행하고, 코드를 실행하면서 프로파일링 및 피드백 데이터를 수집한다. (수집된 데이터는 TurboFan 컴파일러에서 최적화 기계어 코드를 생성하는 데 사용됨) 수집된 정보들을 바탕으로 반복 사용 여부, 코드의 양, 그 외 여러 조건에 따라 TurboFan 컴파일러가 컴파일할지, SparkPlug 컴파일러가 컴파일 할 지 결정된다.
1.3.3 SparkPlug - 비최적화 컴파일러
파이프라인에서 Ignition과 TurboFan 컴파일러 사이에 존재하는 SparkPlug 컴파일러는 Ignition이 생성한 ByteCode를 기반으로 기계어 코드를 만들어낸다. 따라서 AST 분석에서 수행되는 변수 확인, 화살표 함수 확인, Desugaring(문법적 설탕을 제거한다는 뜻으로, ES6+ 문법을 이전 문법으로 변환, 전개연산자나 구조분해할당의 변환 등의 작업)등 작업들을 수행할 필요가 없다. AST 분석에서 수행되는 여러 과정들을 생략하므로 SparkPlug는 좀 더 빨리 컴파일을 수행할 수 있다. 파이프라인 상 SparkPlug 다음에 TurboFan 컴파일러가 존재하기 때문에 SparkPlug는 과도한 최적화를 수행할 필요가 없다.
1.3.4 TurboFan - 최적화 컴파일러
TurboFan 컴파일러 또한 AST 대신 Ignition이 생성한 ByteCode를 기반으로 하며, Ignition이 ByteCode를 실행하며 수집한 프로파일링과 피드백 데이터를 이용하여 히든 클래스, 인라인 캐싱 등 여러 최적화 기법을 적용한 기계어 코드로 변환한다. 또한 최적화가 더 이상 필요하지 않다고 판단되는 코드는 역최적화(Deoptimization)하여 다시 원래의 코드로 되돌리기도 한다.
1.4 결론
인터프리터 시스템의 일부로 컴파일러가 포함되어있고, 컴파일 언어처럼 파싱과 컴파일 단계를 거치며, JS 코드를 파싱하여 AST를 생성하는 과정에서 구문 오류와 같은 정적 오류를 잡아내고, ByteCode를 기계어로 컴파일하는 등 컴파일 언어의 실행과정과도 유사한 점이 많아진 현대의 JS는 더 이상 '인터프리터'나 '컴파일러' 둘 중 하나로 딱 잘라 표현하긴 어렵다.
개인적인 의견으로는 서문에서 인용했던 MDN에서 정의한 설명인 "JavaScript is a lightweight interpreted (or just-in-time compiled) programming language with first-class functions."가 가장 적절한 설명이라고 생각된다.
2. JS 컴파일 과정과 TS 컴파일 과정의 비교
- 참고
- 우아한 타입스크립트 with 리액트 - 6장. 타입스크립트 컴파일
- 컴파일 타임 : 소스 코드가 컴파일러에 의해 기계어 코드로 변환되어 실행 가능한 프로그램이 되는 단계
- 런타임 : 프로그램이 메모리에 적재되어 실행되는 단계
컴파일이란 JS 같은 고수준 언어를 기계어와 같은 저수준 언어로 바꾸어 주는 과정을 말한다. 앞서 V8엔진의 동작과정에서 살펴보았듯이, JS 소스 코드는 AST로 파싱되어 중간 언어인 ByteCode로 변환되고 ByteCode에서 기계어로 변환된다. 하지만, TS는 고수준 언어(TS)가 또 다른 고수준 언어(JS)로 변환되므로, 이 과정을 컴파일이 아니라 트랜스파일이라고 부르기도 한다. 또한 소스코드를 다른 소스코드로 변환하는 것이기 때문에, 타입스크립트 컴파일러(TSC)를 '소스 대 소스 컴파일러'라고 지칭하기도 한다.
2.1 타입스크립트 컴파일러(TSC)의 동작과정
타입스크립트는 5단계를 거쳐 타입 검사와 자바스크립트 소스 변환을 진행한다. 이 과정은 컴파일 타임에 진행된다.
- Scanner : .ts 파일을 찾아 Lexical Analysis(어휘 분석)를 거쳐 토큰화(토큰 단위로 소스코드를 나눔)한다.
- Parser : 토큰을 기반으로 AST를 생성한다.
- Binder : AST에서 선언된 타입의 노드 정보를 저장하는 심볼 데이터 구조를 생성한다.
- Checker : 파서가 생성한 AST와 바인더가 생성한 심볼을 기반으로 타입 검사를 수행한다.
- Emitter : 타입 검사 결과 에러가 없다면 자바스크립트 소스 파일(
.js)로 변환한다.
이후 과정은 런타임에 진행되며 위에서 다룬 V8엔진의 동작과정이 이 때 진행된다.
3. 타입스크립트 컴파일러와 트랜스파일러와의 비교
트랜스파일러 중 하나인 바벨을 예로 들자면, TSC와 바벨 모두 소스 코드를 특정 과거 버젼의 자바스크립트 코드로 변환해준다는 점은 둘 모두 동일하다. 하지만 바벨은 타입 검사를 하지 않는 것이 TSC의 컴파일(트랜스파일) 과정과의 차이점이다.