Muscardinus
What is JavaScript Engine? 본문
우리는 웹개발을 하면서 HTML, CSS, 그리고 자바스크립트를 작성한다.
하지만 우리가 작성한 파일들을 컴퓨터에서 어떻게 알고 웹페이지상으로 보이게 하는지 고민한 적은 잘 없을 것이다.
이번 글은 그 원리를 설명하기 위해서 작성하게 되었다.
JavaScript Engine
자바스크립트 엔진은 JS 코드를 컴퓨터가 이해하고 실행할 수 있는 언어(Machine Language)로 변형하는 작업을 해주는 Engine이다. 그 종류는 매우 많다. 그리고 그 많은 종류의 엔진들은 ECMA Script에 표준을 두어서 개발을 한다. 그 중 가장 대표적으로 사용하는 엔진이 Chrome Broswer에서 제공하는 V8 Engine이다.
※ 해당 글은 V8 Engine을 기준으로 작성하겠습니다.
List Of JavaScript Engines
en.wikipedia.org/wiki/List_of_ECMAScript_engines
그렇다면 JavaScript Engine은 어떠한 원리에 의해서 작동하는 것일까?
위 그림을 보면 JavaScript Engine은 크게 4가지의 과정을 거쳐서 컴퓨터로 전달된다.
Parser - AST - Interpreter - Compiler
1. Parser
소스코드를 분석하여 에러의 유무를 검사하고 코드를 Parsing 하여 token이라는 작은 단위들로 쪼갠다.
말로는 이해가 되기 힘들 것이다.
하지만 우리도 하나의 Parsing을 하는 JavaScript Engine을 만들 수 있다.
function jsengine(code) {
return code.split(/\s+/);
}
jsengine("var a = 5")
위 코드를 실행하면 아래와 같은 결과가 나올 것이다.
[ 'var', 'a', '=', '5' ]
이렇게 Parsing을 하는 것이 Parser의 역할이다.
2. AST (Abstract Syntax Tree)
AST는 자료구조의 한 종류로 Parser에서 분해된 token들을 기반으로 나무 구조를 만든다.
AST에서 생성된 나무 구조는 Interpreter와 Compiler를 거치면서 컴퓨터가 이해할 수 있는 ByteCode와 Optimized Code로 변환된다.
참고:
ko.wikipedia.org/wiki/%EC%B6%94%EC%83%81_%EA%B5%AC%EB%AC%B8_%ED%8A%B8%EB%A6%AC
3. Interpreter
코드를 한 줄 한 줄 읽어내려가며 한 줄씩 ByteCode로 변환한다.
4. Compiler
Interpreter와는 다르게 한 줄 한 줄 번역하지 않고 파일 전체를 읽은 뒤, 코드의 의미를 해석하고 파일 전체를 Machine Language로 Compile 해서 변환한다.
우리가 흔히 알고 있는 Babel와 TypeScript도 이러한 원리로 진행된다. 한 개의 언어를 또 다른 언어로 변환한다!
여기까지 글을 읽으면서 3번과 4번의 과정에 있어서 의구심이 들 것이다. Interpreter와 Compiler는 하는 행동이 다른데 왜 두 가지를 같이 하는 것일까?
이러한 의구심이 들었다면 독자는 지적 호기심이 매우 높다고 할 수 있다!
Interpreter의 경우 JS 파일을 한 줄 한 줄 해석하면서 중간 단계의 ByteCode로 변환하고 바로 실행한다.
반면 Compiler는 JS 코드를 입력받으면 파일 전체를 읽은 뒤, 이를 Compile하여 Machine Code로 변환하고 CPU로 입력되어 실행된다.
InterPreter VS Compiler
1. InterPreter
장점
- 코드 전체가 Compile 된 Compilation이 완성되는 것을 기다릴 필요 없이, 한 줄 한 줄 변환하기 때문에 실행속도가 빠르다.
단점
- JS코드가 복잡해질수록 점점 속도가 느려진다. 예를 들어, 같은 코드를 여러 차례 반복하는 반복문의 경우, 같은 결과를 반복하는 것임에도 불구하고 코드를 한 줄 한 줄 읽는 방식 때문에 극단적으로 10억번을 반복해야하는 경우 10억번을 반복해서 코드를 변형해야한다.
2. Compiler
장점
- 작업을 단순화 시킨다. 예를 들어, 특정 함수를 10억번 반복해야하는 경우, Compile 과정에서 함수를 반복하는 것이 아니라 함수의 결과물을 반복하도록 Compile한다. 이처럼 불필요한 동작을 제거하는 Optimization 과정을 거친다.
단점
- 코드를 바로 실행하지 않고, 코드 실행 전 전체를 Compile하는 과정이 필요하기 때문에 초기에 속도가 느리다.
function addNumFive(num) {
return num + 5;
}
for (let i = 0; i < 100000000000; i++) {
addNumFive(2); // ---> addNumFive(2)를 7로 인식
}
사실 과거에는 Interpreter만으로도 작성한 JS Engine이 존재하였다. 하지만, 한 줄 한 줄 읽는 것은 어떻게 보면 비효율적이다. 그렇다고 전체를 읽고, 의미를 해석하고 자주 사용하는 코드를 인지하고 최적화 시키는 것도 어떠한 상황에서는 비효율적이다. 그러기에 현대의 JS Engine은 두 가지를 Combine 시켜서 작동하고 있다. 그리고 이것을 JIT(Just In Time) Compiler라고 한다.
JIT Compiler 구동 방식
1. AST를 통해 변환된 코드는 최초에 Interpreter에게 전달된다.
2. Interpreter가 코드를 실시간으로 변환하면서 Browser에게 특정 작업을 지시하는 동안, Profiler가 입력 받은 코드에서 최적화할 수 있는 부분을 찾아서 기록한다.
3. 최적화가 가능한 부분을 찾으면 Compiler에 전달되고, Compiler는 Interpreter에 의해 실시간으로 Website가 구동되는 동안 필요한 부분을 기계어로 변환하여 Optimization을 돕는다.
4. 최적화된 코드를 수행할 차례가 오면, ByteCode에서 부분적으로 Compiler가 변환한 최적화된 코드를 그 자리에 대체하여 실행한다.
위 방식을 통하여 최적화가 진행된다. 하지만 이것에도 한계가 있기에 우리는 JS Engine의 구동 원리를 기반으로, 더 빠르고 효율적인 최적화된 코드를 작성해야한다.
'FrontEnd > JavaScript Basics' 카테고리의 다른 글
Call Stack And Memory Heap (0) | 2020.11.30 |
---|---|
Writing Optimized Code (0) | 2020.11.30 |
this keyword - 마지막 (0) | 2020.10.19 |
this keyword - 3탄 (0) | 2020.10.15 |
this keyword - 2탄 (2) | 2020.10.13 |