자바스크립트는 인터프리터언어이다. 그런데 컴파일러를 얹은.
컴파일러는 특정 프로그래밍 언어로 쓰여 있는 문서를 다른 프로그래밍 언어로 옮기는 언어 번역 프로그램을 의미한다. 가령 자바스크립트라는 고급 프로그래밍 언어를 기계어라는 다른 프로그래밍 언어로 번역하는 역할을 컴파일러가 수행한다.
기계어를 다른 언어로 번역할 필요 없이 프로그래밍 언어의 소스 코드를 바로 실행하는 컴퓨터 프로그램 또는 환경을 말한다. 코드 한줄한줄씩 바로 실행해나가는 방식으로 진행된다. 이처럼 바로 실행이 가능하기에 변경사항을 빠르게 테스트해보기 용이하다는 장점이 있다.
기본적으로 자바스크립트는 인터프리터 언어에 해당한다. 다른 대표적인 컴파일러 언어인 C언어 혹은 C++의 경우에는 컴파일 과정(+어셈블러 +링커) 을 통해 실행파일을 생성해주어야 비로소 프로그램 실행이 가능해진다.
반면 파이썬이나 자바스크립트 같은 언어의 경우에는 위의 사진처럼 코드 한줄씩만 입력하더라도 바로바로 실행하며 결과를 확인하는 것이 가능하기에 인터프리터 언어에 해당하게 된다. 이는 자바스크립트가 만들어질 당시 웹문서 구조를 동적으로 나타내기 위해 제작된 언어이기에 가벼운 인터프리터 구조가 적합했기 때문이다.
그렇다고 자바스크립트가 따로 별도의 과정없이 바로 컴퓨터가 실행가능한 것은 아니다. 자바스크립트가 인터프리터에 전해지기 일련의 과정을 거쳐야 한다.
자바스크립트 뿐만 아니라 모든 고급언어들은 컴퓨터에서 구동되기 위해서 기본적으로 기계가 이해가능한 기계어로 변환되어질 필요가 있다.
위에서 보다시피 자바스크립트는 기계에게 전달되기전에 바이트 코드로 변환되고 이를 받아 가상머신에 의해 기계어로 변환된다. 이러한 일련의 변환 과정은 아래와 같이 진행된다.
1) 바이트 코드로의 변환
자바스크립트 엔진에 의해 바이트코드로 변환된다.
2) 기계어로 변환
CPU마다 기계어를 다르게 해석하기에 가상 머신은 최적화된 기계어를 제작해낸다. 이 가상머신 덕분에 개발자는 따로 CPU별로 최적화된 기계어를 만들어낼 필요는 없다.
3) CPU 코드 실행
기계어를 실행하여 데이터 저장 및 연산 작업을 진행한다.
이제 JS가 자바스크립트 엔진에 의해 어떻게 바이트 코드로 변환되는지 알아본다. 이는 엔진 내 인터프리터가 진행한다. 인터프리터에게 전달되기 전에도 일련의 과정이 필요한데 이는 이번 6일차에서 배웠던 Tokenizer, Parser를 거쳐 AST가 되는 과정이다.
이를 단순화해서 보면 다음과 같다.
Tokenizing
: 주어진 소스코드를 의미있는 단위로 나누는 과정이다. 이렇게 나누어진 것을 Token이라고도 한다. (보통 의미를 부여하는 lexer의 역할도 여기에 포함된다.)
Parser
: Tokenizer
로부터 생성된 토큰들의 배열을 바탕으로 이를 자바스크립트 문법에 알맞은 방식으로 AST(Abstract Syntax Tree)
로 변화 시킨다.
이렇게 생성된 AST
는 인터프리터를 거쳐 기계가 알아볼 수 있는 바이트 코드롤 변환되게 되는 것이다.
이러한 자바스크립트는 인터프리터 언어로서 기능을 해왔지만, 점차 웹에서도 다양한 요구사항들이 추가되면서 더 많은 기능들을 갖추어야 했고 이는 자바스크립트가 점차 성능상 무거워지는 계기가 되었다. 한편, 2009년 당시 구글은 웹에서 이용가능한 지도인 구글맵스를 개발하려고 있었는데 지도 어플리케이션은 사용자 상호작용이 많이 필요한 만큼 성능상 개선이 필요했고 이를 개선하고자 내놓은 것이 바로 Chrome V8
엔진이다. 이를 통해 자바스크립트 언어에서도 컴파일을 진행하게 된 계기가 되었다.
이에 대해 알아보기전에 왜 컴파일 언어의 성능이 더 효율적인지 이해해볼 필요가 있다. 컴파일 언어와 인터프리터 언의 가장 큰 차이점은 바로 실행전 미리 기계어로 바꾸어 놓는다는 점이다. 인터프리터처럼 고급언어를 기계어로 번역하는 것이 아니라 미리 변경해놓기에 빠른 것인데 이는 아래 예시를 보면 훨씬 이해하기 수월해진다.
function sum () {
let result = 0
for (let i = 1 ; i <= 10 ; i++){
result += i;
}
return result;
}
sum() // for loop.. -> 55
sum() // for loop.. -> 55
sum() // for loop.. -> 55
// compile 결과
sum() = 55
sum() = 55
sum() = 55
이러한 코드가 있다고 할때 컴파일 과정을 거친 sum
함수의 결과값은 미리 기계어로 번역되기에 실제 실행할 필요없이 미리 정해져있다. 반면 인터프리터는 한줄한줄 실행해 나가는 방식이기에 sum
를 만나게 되면 일일이 실행하여 for
문을 거쳐야 비로소 결과가 도출된다.
이를 통해 컴파일 언어가 인터프리터 언어보다 좋은 성능을 보이는지 확인해보았다.
다시 돌아와 V8 엔진에 의해서 어떻게 자바스크립트도 컴파일과정을 거치는지 알아볼 차례이다. V8 엔진은 기존의 Parser를 거쳐 AST로 변환된 내용을 인터프리터에게 전달하는 과정에 덧붙여 자바스크립트 변환과정을 진행한다.
위 그림에서 자바스크립트가 Parser, AST, Interpreter를 거쳐 ByteCode로 변모하는 것은 V8 엔진이 등장하기 전까지의 JS의 모습이다. 이에 추가적으로 Profiler
라는게 등장한다. 이 Profiler
는 인터프리터를 관찰하며 실행되는 코드를 계속해서 모니터링 한다. 모니터링하는 과정에 코드내에 반복 실행되는 것이 있다면 이를 컴파일러에게 넘겨 실시간으로 컴파일 하도록 한다. 이를 통해 최적화된 바이트 코드를 생성해낸다. 이전의 코드 예시에서 sum
함수처럼 반복해서 진행되는 여지가 있는 코드가 컴파일의 대상이 되겠다.
이처럼 필요할때 마다 컴파일 하는 컴파일러를 JIT(Just-In-Time)
컴파일러라고 부른다. 또한 필요할 경우 Decompile
과정을 진행하는데 이는 컴파일러가 판단할때 컴파일하는게 좋다고 판단했던 것이 잘못되었음을 알고 되돌리는 과정이다. 이는 컴파일 하는 비용을 줄이기 위함이라고 한다.
이제 이 질문에 대답할 수 있는 차례가 왔다. 정답은 살펴본것 처럼 둘다에 해당한다. 기본적으로는 Interpreter
언어로서의 성질을 가지지만, 성능상의 최적화를 위해 Compiler
언어의 특성도 같이 가진다.
자바스크립트 코드 실행 동작 원리: 엔진, 가상머신, 인터프리터, AST 기초
헷갈리는 개념이었는데 이 글보고 잘 정리가 되었어요. 감사합니다!