
자바스크립트는 인터프리터 언어다.
그런데 이번에 자바스크립트 엔진에 대해 스터디를 하면서 자바스크립트가 컴파일을 한다는 사실을 알아버렸다.
이녀석은 왜 컴파일을 할까
아니 그런데 왜 컴파일을 하는데 인터프리터 언어라고 할 수 있을까
이제 혼자 고민에 빠졌다

일반적으로 우리가 알고 있듯,
그래서 초기 스캔이 오래 걸리지만, 실행 시간 자체는 인터프리터보다 빠르다는 장점이 있다.
반대로, 컴파일러는 Object Code를 만들고, 다시 이 코드를 묶어 하나의 실행 파일로 만드는 linking 이라는 작업을 진행하기 때문에 인터프리터보다 많은 메모리를 사용한다.
그래서 실행시간이 컴파일러보다 더 걸린다. 또한 한 문장씩 번역될 때 오류를 만날 수 있기 때문에 프로그램을 실행해야 오류를 발견할 수 있다.
하지만 컴파일 과정에서 만들어지는 파일이 없기 때문에 메모리 효율이 좋다는 장점이 있다.
JavaScript는 1995년에 넷스케이프에서 처음 개발되었고, 초기 설계 목적은 웹 브라우저 내에서 동적인 사용자 인터랙션과 기능을 바로 실행할 수 있도록 하는 것이었습니다.
초기 자바스크립트의 역할을 생각해보면, 사용자의 브라우저 바로 실행되어야 했다. 때문에 인터프리터 방식이 적합했다.
인터프리터 언어는 변환 없이 텍스트를 바로 실행하는 방식이 아니라, "컴파일 단계 없이(혹은 최소한의)" 바로 실행하는 방식을 말한다.
그래서 우리의 console.log()는 자바스크립트 엔진이 바로 이해할 수는 없다. 어쨌든 파싱 과정이 필요하다.

자바스크립트는 기계에게 전달되기 전에 바이트 코드로 변환이 되어야 한다.
이 바이트 코드가 가상머신에 의해 기계어로 변환되고 이를 기계가 해석하여 실행하는 것이다.

JS 엔진에는 여러 가지 구성요소들이 있다.

JS 엔진 중에 파서(Parser)가 읽어서 소스 코드를 토큰화하고, AST(추상 구문 트리)로 만든다.
그 다음 AST를 Ignition 인터프리터용 바이트코드로 컴파일한다. 이 바이트코드는 Ignition 인터프리터가 바로 실행하거나, JIT 컴파일러(TurboFan)가 최적화된 머신 코드로 변환하기 위한 중간 단계이다.
JS엔진 내부에 가상 머신이 있는데, 이 가상 머신이 바이트 코드를 받는다.
실행 방식은 크게 두 가지가 있다.
1) 인터프리터 방식
: 가상 머신은 바이트코드를 한 줄씩 해석(인터프리팅)하며 실행한다. 이 경우 바이트코드를 직접 기계어로 변환하지 않고, 해석해서 바로 실행하는 방식이다.
2) JIT 컴파일 방식
: 가상 머신 내부에 있는 JIT 컴파일러가 바이트코드를 기계어로 변환한다.
실행 시점에 인터프리터가 바이트코드를 한 줄씩 해석하면서 실행하지만, 자주 실행되는 코드는 JIT 컴파일러가 기계어 코드로 변환하여 메모리 캐시에 저장한다.
따라서 같은 함수가 여러 번 호출되더라도 매번 기계어로 번역할 필요 없이, 저장된 기계어 코드를 재사용하여 실행 속도를 크게 향상시킨다.
짧게 말해, 런타인 환경에서 컴파일을 하는 역할인 것이다.
캐싱으로 미리 번역을 해두니, 당연히 효율적이겠지.. 만,
자바스크립트는 반드시 JIT 모드가 더 빠르다고 할 수는 없다.
변수의 타입이 실행 중에 변할 수 있고, 프로토타입 기반 방식을 사용하는 등 매우 동적인 특성을 가지기 때문에, JIT는 많은 예외 케이스를 고려해야 한다.
이런 예외 케이스 대응을 하면, slow case로 점프를 하게 되는데, 이런 helper function들은 인터프리터 모드로 수행할 때와 동일한 코드를 사용하게 된다. JIT 컴파일러로 native code를 수행한다 해도 많은 부분이 인터프리터를 사용할때와 차이가 없게 되는 것이다.
slow case란?
native code로 생성하기 어려운(native code로 표현하면 양이 많아지는) 동작들을 native code로 뽑아내는 대신 미리 엔진 내부에 C로 구현된 helper function을 호출하여 동작을 수행하는 경우를 의미
JavaScript는 주로 웹페이지의 layout을 조작하거나 사용자 입력에 반응하는 방식의 코드가 많아, 자주 반복되어서 수행되는 구간인 hotspot이 상대적으로 매우 적다.
이러한 경우 native code를 수행하는 시간에 비해, native code를 만드는 시간, 즉 컴파일 오버헤드가 상대적으로 커지게 되는 문제가 있다. 코드가 골고루 실행되기 때문에 컴파일해야하는 코드의 양이 많아지는 것이다.
Adaptive JIT Compilation
실행 초기에는 인터프리터 방식으로 실행하고 자주 호출되는 메소드나 반복되는 코드를 검출하여 이러한 코드만 컴파일하는 방식을 말한다.
JIT 컴파일러의 단점을 보완하기 위한 방식으로, 코드가 사용되었을때 즉시 컴파일하는 것이 아니라 여러번 호출 된 후 지연시켜 컴파일을 수행하는데 이를 Lazy Compilation이라 한다.
인터프리터 언어라는 정의는 일반적으로 “코드를 한 줄씩 해석해서 바로 실행하는 언어”를 뜻하지만, 현대적인 인터프리터 언어들은 런타임에 컴파일(JIT 포함)을 함께 수행하는 경우가 많아 엄밀히 말하면 ‘인터프리터’와 ‘컴파일러’의 경계가 모호해졌다.
그렇기는 하다만, 컴파일 언어는 보통 ‘실행 전에(compile time) 전체 코드를 미리 컴파일해서 실행 파일을 만드는 언어’를 의미한다.
반면 지금까지 우리가 알아봤듯, 자바스크립트는 실행 중에 코드를 해석하거나 컴파일한다. 따라서 런타임 환경에서 실행되는 동적 언어, 즉 인터프리터 언어 계열로 분류하는 것이 적절하다고 생각한다.
![]()
고민이 해결되었다.