8/12/2023

JavaScript Engine & Runtime

JavaScript Engine

JavaScript 엔진은JavaScript 코드를 실행하는 컴퓨터 프로그램이다. 각 브라우저마다 고유한 JavaScript 엔진이 있지만 가장 잘 알려진 엔진은 구글의 V8 JavaScript 엔진이다. V8 JavaScript 엔진은 구글 크롬과 NodeJS를 구동시키는 JavaScript 엔진이다. 모든 JavaScript 엔진은 항상 호출 스택과 힙을 포함하고 있다. 호출 스택은 실행 컨텍스트를 사용하여 실제로 코드가 실행되는 곳이고 힙은 애플리케이션에 필요한 모든 객체를 저장하는 비구조적인(unstructured) 메모리 풀이다.

Compilation vs. Interpretation vs. Just-in-time(JIT) compilation

컴파일에서는 전체 소스 코드가 한 번에 기계 코드로 변환되고 기계 코드는 어떤 컴퓨터에서든 실행할 수 있는 이식 가능한 바이너리 파일로 작성된다. 즉, 먼저 기계 코드가 생성되고 그런 다음 CPU 내에서 실행된다. 실행은 컴파일 이후에도 일어날 수 있다.

인터프리테이션에서는 소스 코드를 줄 단위로 실행하는 인터프리터가 있다. 즉, 코드가 읽혀지고 동시에 실행된다. 소스 코드는 여전히 기계 코드로 변환되어야 하지만 실행 직전에 변환되며 사전에 변환되지 않는다.

JavaScript는 예전에는 순수히 인터프리테이션 언어였지만 인터프리테이션 언어의 문제는 컴파일 언어보다 훨씬 느리다는 것이다.예전 JavaScript의 경우 이러한 점이 문제가 되지 않았지만 현대 JavaScript와 현재 사용하는 완전한 웹 애플리케이션에서는 낮은 성능은 더 이상 허용되지 않는다. 현대 JavaScript 엔진은 단순한 인터프리테이션 대신 컴파일과 인터프리테이션 사이의 혼합인 즉시(Just-In-Time, JIT) 컴파일을 사용한다. 이 방식은 소스 코드 전체를 한 번에 기계 코드로 컴파일한 다음 즉시 실행한다. 컴파일과 같은 두 단계가 있지만 실행할 수 있는 이식 가능한 바이너리 파일이 없으며 실행은 컴파일 직후에 즉시 발생한다. 이는 코드를 한 줄씩 실행하는 것보다 훨씬 빠르기 때문에 JavaScript에게 이상적이다.

JIT Compilation of JavaScript

JavaScript 코드가 엔진으로 들어오면 첫 번째 단계는 코드를 파싱하는 것이다. 파싱 과정에서 코드는 추상 구문 트리(Abstract Syntax Tree, AST)라는 자료구조로 파싱된다. 이 과정은 코드의 각 줄을 const나 function 예약어와 같이 언어에 의미 있는 조각으로 나누고 모든 조각을 구조화된 방식으로 트리에 저장한다. 이 단계에서 구문 오류가 있는지 확인하며 결과적으로 생성된 트리는 나중에 기계 코드를 생성하는 데 사용된다.

다음 단계는 파싱으로 생성된 AST를 가져와 기계 코드로 컴파일한다. 기계 코드는 바로 실행되며 실행은 JavaScript 엔진의 호출 스택에서 발생한다.

코드가 실행 중이라도 현대의 JavaScript 엔진은 여기서 멈추지 않으며 최적화 전략을 사용한다. 초기에 매우 최적화되지 않은 버전의 기계 코드를 생성해서 가능한 빨리 실행을 시작할 수 있다. 그런 다음 코드는 백그라운드에서 이미 실행 중인 프로그램 실행 중에 최적화되고 다시 컴파일된다. 각 최적화 후에 비최적화 코드는 실행을 멈추지 않으면서 새로운 최적화된 코드로 교체된다. 이러한 과정이 현대의 엔진(e.g., V8 JavaScript 엔진)을 빠르게 만든다.

파싱, 컴파일 및 최적화 작업은 엔진 내의 특수 스레드에서 수행되며 이 스레드는 코드에서 접근할 수 없다. 기본적으로 호출 스택에서 실행 중인 메인 스레드와 완전히 분리되어 있다.


JavaScript Runtime


JavaScript 런타임을 JavaScript를 사용하기 위해 필요한 모든 것을 담고있는 컨테이너로 상상할 수 있다. 먼저 브라우저의 경우를 알아본다. 어떤 JavaScript 런타임이든 핵심은 항상 JavaScript 엔진이다. 엔진이 없으면 런타임도 없고, JavaScript도 없다. 하지만 엔진만으로는 충분하지 않는데 제대로 작동하려면 웹 API에도 접근해야 한다. 웹 API는 엔진에 제공되는 기능이지만 실제로는 JavaScript 언어 자체의 일부가 아니다. JavaScript는 전역 window 객체를 통해 이러한 API에 접근한다. 하지만 여전히 웹 API가 런타임의 일부이다. 다음으로 전형적인 JavaScript 런타임은 이른바 콜백 큐도 포함한다. 콜백 큐는 실행 준비가 된 모든 콜백 함수를 담고있는 자료구조이다. 이벤트 핸들러 함수는 콜백 함수인데 이벤트 후 발생하는 첫 번째 단계는 콜백 함수가 콜백 큐에 넣어지는 것이다. 그런 다음 호출 스택이 비어 있을 때 콜백 함수가 호출 스택으로 전달되어 실행되며 이 과정은 이벤트 루프에 의해 발생한다. 기본적으로 이벤트 루프는 콜백 큐에서 콜백 함수를 가져와 호출 스택에 넣어 실행할 수 있게 해주며 이벤트 루프는 JavaScript의 비동기 논-블록킹(non-blocking) 동시성(concurrency) 모델 구현에 필수적이다.

또 다른 런타임인 NodeJS의 경우 브라우저와 크게 다르지 않지만 브라우저가 아니므로 웹 API를 가질 수 없다. 대신, 여러 개의 C++ 바인딩과 스레드 풀이 있다.

update: 2023-08-14

댓글 없음:

댓글 쓰기