자바스크립트는 스크립트 언어이자 엔진을 통해 처리되는 인터프리트 언어입니다. 다만, 컴파일 과정을 갖고 있습니다.
자바스크립트 엔진은 일반적인 쉘 스크립트가 한 라인씩 바로 실행되는 인터프리트 언어와는 조금 다른 실행 구조를 갖고있습니다. 먼저, 실행할 전체 함수를 실행 직전에 간단히 변수 및 함수 선언들만 스캔하는 (A) JIT 컴파일 과정을 거쳐, 그 후 (B) 수행 과정의 사이클로 실행됩니다. 여기서 (1) JIT 컴파일 과정은 실제 우리들이 흔히 알고있는 C++, Java 와 같은 컴파일 언어에서 중간코드를 만드는 AOT(Ahead-of-Time) 컴파일 과정과는 다릅니다. 자바스크립트를 인터프리트 언어라고 알고있었는데 좀 놀랍죠. 이렇게 자바스크립트 엔진에 단순히 컴파일 과정이 있다는 사실만으로 자바스크립트를 컴파일 언어로 언급하기도 합니다만 엄연히 기존 컴파일 언어의 정의와 다르고, 자바스크립트 엔진은 함수 실행 시점에 컴파일을 진행하므로 인터프리트 언어입니다.
인터프리터 언어 : 소스코드를 한 줄 한 줄 읽어가며 명령을 바로 처리하는 프로그램(언어). 번역과 실행이 동시에 이루어진다.
JIT 컴파일(just-in-time compilation) 또는 동적 번역(dynamic translation) : 프로그램을 실제 실행하는 시점에 기계어로 번역하는 컴파일 기법이다.
AOT(Ahead-of-time) : 소스 코드를 미리 컴파일하는 방식 / 설치 시점에 기계어로 번역
자바스크립트 런타임은 크게 2 개의 구성요소로 나눠질 수 있고, 개별적으로는 5 개로 나누어 볼 수 있습니다.
자바스크립트 엔진 = (1) Heap, (2) Stack(Call stack)
(3) Web APIs, (4) Callback queue, (5) Event loop
자바스크립트 엔진은 (1) Heap 그리고 (2) Stack 만을 의미하며 싱글 스레드로 모든 코드를 수행합니다.. 자바스크립트의 비동기를 학습할때 배우는 (3) Web APIs, (4) Callback queue, (5) Event loop들은 정확히는 자바스크립트 엔진의 구성요소가 아닙니다. 자바스크립트 엔진이 싱글 스레드로 모든 코드를 수행한다면 동기적 실행밖에 안될텐데 어떻게 비동기를 지원한다는 것일까요? 비동기 지원을 위해 바로 자바스크립트 런타임에서 (3), (4), (5) 세 요소를 추가한것입니다.
자바스크립트 엔진의 (2) Stack 은 일반 프로그램 언어들의 Stack 과는 다른데요. 타 프로그램 언어들은 함수 실행에 따라 Call stack 에 각 로컬 함수들의 변수 등의 Context 정보들을 다 같이 쌓습니다. 로컬 함수에만 국한된 정보들을 갖는다는 이유로 Context 를 Scope 라고도 부릅니다. 반면, 자바스크립트 엔진도 Call stack 에 함수 호출 순서를 적재합니다만, 변수 및 함수 선언과 할당 정보는 Heap 에 따로 저장히여 Call Stack 에는 본 Heap 에 대한 포인터만 갖고 있습니다. 구체적으로 정리하면 아래와 같습니다.
자바스크립트 엔진
(1) Heap: 각 함수 별 선언 및 할당되는 모든 변수 및 함수를 적재하는 메모리 영역
(2) Stack(Call Stack): 함수 실행 순서에 맞게 위 Heap 에 대한 포인터 적재 및 실행
비동기 지원
(3) Web APIs: 기본 자바스크립트에 없는 DOM, ajax, setTimeout 등의 라이브러리 함수들. 브라우저나 OS 등에서 C++ 처럼 다양한 언어로 구현되어 제공
(4) Callback Queue: 위 Web APIs 에서 발생한 콜백 함수들이 차곡차곡 여기에 적재
(5) Event Loop: 위 Callback Queue에 적재된 함수를 Stack 로 하나씩 옮겨서 실행되도록 하는 스레드
자바스크립트 엔진은 (A) JIT 컴파일 과정과 (B) 수행 과정 이렇게 두 개로 나뉩니다.
매 함수 실행 시 (자바스크립트 첫 실행 함수는 main() 입니다.) ASTs 생성 및 바이트코드로 변경하고 JIT 컴파일 기법(바이트코드 캐싱을 통해 불필요한 컴파일 시간을 줄이는것)을 위해 프로파일러로 함수 호출 횟수를 저장/추적합니다. 우리가 기억하면 될 것은 본 과정에서 변수의 ‘선언’(선언과 할당 중) 그리고 함수의 ‘선언’을 Heap 에 적재한다는것입니다.
자바스크립트 변수의 ‘선언’은 var a 입니다. (a = 5 는 ‘할당(Assignment)’입니다.)
자바스크립트 함수의 ‘선언’은 function a() 입니다.
Compilation Phase에선 변수 및 함수의 ‘선언(Declaration)’만 추출하여 Heap 에 적재합니다.변수와 함수의 선언을 자바스크립트 실행 이전에 컴파일로 저장하여 실제 실행 시 변수와 함수 선언 여부를 검색합니다.
변수의 ‘할당(Assignment)’과 실제 함수를 호출 및 실행합니다.
자바스크립트 변수의 ‘할당’은 a = 1 입니다.
a = 1 할당 시 이전 컴파일 과정에서 선언된 변수 a 가 있는지 확인합니다.
만약 존재하지 않는다면 a 변수 ‘선언’과 동시에 ‘할당’하여 적재합니다.
자바스크립트 함수의 ‘호출 및 실행’은 a() 입니다.
a() 실행 시 첫번째로, 이전 컴파일 과정에서 선언된 함수 a() 가 있는지 확인합니다.
a() 실행 시 두번째로, Heap 에는 새 함수를 위한 Local Execution Scope 영역을 생성하고, Call Stack 에는 생성된 Heap 에 대한 포인터를 갖는 함수 a() 정보를 적재합니다.
a() 실행 시 마지막으로, 컴파일을 수행하여 본 함수 내 변수 및 함수를 위 Local Execution Scope 영역에 적재합니다.
Execution Phase에선 변수의 ‘할당(Assignment)’값들을 Heap 에 적재하고 함수는 호출 및 실행합니다.
매 함수 호출때마다 스택에 함수 내 변수 및 함수를 같이 적재하는 스택 베이스 언어과 달리 자바스크립트는 스택에는 함수 호출 순서와 실제 변수 및 함수 정보들은 Heap 에 대한 포인터를 갖습니다. Heap 에 함수 a() 를 위한 Local Execution Scope 는 a() 함수가 호출되기 이전에 Heap 에 존재했던 Global Scope (window)에 대한 포인터를 갖고있어서, 엔진 내에서 아래와 같은 처리가 가능합니다.
a() 함수 내에서 a = 1 변수 할당 시 먼저 Local Execution Scope 에 a 변수의 선언을 찾고존재하지 않는다면 이전 Global Scope 로 돌아가 검색할 수 있습니다.
a() 함수 실행이 끝나게 되면 Call Stack 을 통해 현재 Heap 영역을 Global Scope 로 다시 되돌립니다.
*참고자료
Crucian Carp
Javascript 개요