이 글은 "How does Javascript and JavaScript engine work in the browser and node?"를 번역한 글입니다. 원문의 의미를 살리기 위해 직역을 한 부분이 존재하므로, 이를 참고하시기 바랍니다.
이번 시간에는 자바스크립트 엔진과 그 구조에 대해서 알아볼 것이다. 또한 자바스크립트의 콜스택, 이벤트 루프, 태스크 큐, 그리고 자바스크립트가 제대로 동작하도록 하는 기타 다양한 요소들에 대해 배울 것이다.
자바스크립트 엔진의 구조를 살펴보고, 그것들이 어떻게 조화를 이루는지 확인하기 전에 먼저 자바스크립트의 기본 개념과 그 시작에 대한 약간의 역사를 이해해보자.
JavaScript in a nutshell
자바스크립트는 인터프리터 언어이다. 이것은 자바스크립트 소스 코드를 브라우저로 보내기 전에 컴파일할 필요가 없다는 것을 의미한다. 인터프리터는 원시 자바스크립트 코드를 가져와서 실행할 수 있다.
자바스크립트는 C나 C++와 달리 동적 타입 언어이다. 이는 var로 선언된 변수는 int, string, boolean과 같은 모든 유형의 자료형 또는 객체(object) 및 배열(array)과 같은 복잡한 자료형을 저장할 수 있다는 것을 의미한다.
타입 시스템의 부족은 자바스크립트를 느리게 만든다. 정적 타입 언어는 타입(type)과 크기(size)와 같은 데이터에 대한 정보를 가지고 있기 때문에 훨씬 효율적인 기계어를 만들 수 있다.
C나 C++와 같은 정적 타입 언어가 너를 아무 이유 없이 힘들게 한다는 생각이 들 때마다, 그 성능에 대해 생각해봐라.
History of JavaScript
자바스크립트가 속도 면에서 형편없다면, 왜 이러한 방식으로 설계되었는지 의문이 들 수 있다. 이를 위해서 우리는 자바스크립트의 역사를 이해해야 한다.
Web 초기에 웹 브라우저는 정적 페이지를 보여주기 위해 사용되었다. 일반적으로 이러한 페이지들은 상호작용이 없었다. 일부 상호작용을 추가하기 위해, Brendan Eich은 1995년 Netscape 브라우저에 새로운 언어를 도입하였다. 이 새로운 언어가 JavaScript(이전에는 LiveScript로 불림)였으며, 이는 설계하는데 10일이 걸렸다.
10일 만에 좋은 것이 나올 수는 없지만 10일 동안 노력한 결과물로써 자바스크립트는 경이로웠다. ActionScript, Silverlight, Flash와 같은 다른 언어 및 플러그인이 함께 등장했으나, 자바스크립트가 경쟁에서 승리하였다.
자바스크립트는 성능을 염두에 두고 설계되지 않았다. 단지 브라우저 내에서 작동하고, DOM과 작동하도록 API를 제공하기만 하면 되었다. 하지만 많은 브라우저들이 각자 나름의 방식으로 자바스크립트를 채택하려 했기 때문에 표준화가 필요했다.
Ecma International은 자바스크립트를 표준화하는 조직이며, Technical Committee 39(TC39)는 이러한 표준을 관리한다. 이 표준은 EcmaScript로 알려져있다.
The Anatomy of the JavaScript engine
EcmaScript 사양은 자바스크립트 프로그램이 모든 브라우저에서 동일하게 실행되도록 자바스크립트를 구현하는 방법은 알려주지만, 이러한 브라우저 내부에서 자바스크립트가 어떻게 실행되어야 하는지는 알려주지 않는다. 이것은 브라우저 공급 업체의 결정에 달려있다.
모든 브라우저는 자바스크립트 코드를 실행하는 자바스크립트 엔진을 제공한다. Netscape 브라우저는 SpiderMonkey 자바스크립트 엔진을 사용한다. 이 엔진은 최적화가 되어있지 않은 가장 기본적인 인터프리터이다. 이 엔진으로 자바스크립트 코드를 실행하는 것은 느리지만 동작하기는 한다.
위 그림에서 볼 수 있듯이, 처음 자바스크립트 엔진의 역할은 자바스크립트 소스 코드를 가져와서, 그것을 CPU가 이해할 수 있는 바이너리 명령(기계어)으로 컴파일하는 것이었다.
기초적인 자바스크립트 엔진은 자바스크립트 소스 코드를 바이트 코드(byte code)라고 불리는 중간 표현(Intermediate Representation, IR)으로 컴파일하고, 이 바이트 코드를 인터프리터에게 전달하는 baseline complier를 포함한다.
인터프리터는 이 바이트 코드를 가져와 기계의 하드웨어(CPU)에서 실행되는 기계어로 변환한다.
이것은 자바의 작동 방식과 동일하지만, 바이트 코드의 생성은 프로그래머에 의해 수행되며 바이트 코드는 소스 코드보다 더 보편적으로 공유된다.
baseline complier의 역할은 가능한 한 빠르게 코드를 컴파일하고 덜 최적화된 바이트 코드(또는 다른 경우 기계어)를 생성하는 것이다. 인터프리터에는 작업해야 하는 최적화되지 않은 바이트 코드가 있으므로, 응용 프로그램 속도는 느려지지만, 응용 프로그램의 부트스트랩 시간은 매우 짧아진다.
SpiderMonkey JavaScript는 고도로 최적화된 기계어를 만들어내는 복잡한 기계로 진화했으며, 현재 Firefox 브라우저에서 사용된다.
매우 동적인 상호작용 웹 애플리케이션의 경우, 이러한 자바스크립트 실행 모델은 사용자 경험이 매우 좋지 않다. 이 문제는 Google의 Chrome 브라우저에서 웹에 Google 지도를 표시할 때 발생했다. 웹에서 자바스크립트 성능을 높이려면 더 나은 접근 방식을 찾아야 했다.
초기의 Google 크롬은 V8 자바스크립트 엔진을 사용한다. 처음에는 자바스크립트 성능을 개선하기 위해 아래와 같이 자바스크립트 엔진 파이프라인에 두 가지 부분을 추가했다.
2010년 버전 V8 자바스크립트 엔진은 엔진을 위해 무거운 작업을 수행하는 두 가지 주요 장비가 있었다. full-codegen은 보다 빠른 애플리케이션 부트스트랩을 위해 최적화되지 않은 기계어를 최대한 빨리 내보내는 baseline complier이다.
애플리케이션이 실행 중일 때, crankshaft 컴파일러가 실행되어 소스 코드를 최적화하고, baseline complier에 의해 생성된 기계어 일부를 대체한다. 이 최적화는 더 나은 기계어가 생성될수록 애플리케이션 성능을 더 향상시킨다.
그러나 이 프로세스에는 많은 CPU 오버헤드와 메모리 소모가 수반된다. 따라서 V8은 다른 모델을 내놓아야 한다.
위 버전의 자바스크립트 엔진에는 인터프리터가 포함되어 있지 않다. 이는 JIT(Just-In-Time) 모델로, 코드가 즉시 기계 수준으로 컴파일되고 나중에 기계 코드로 최적화된다.
How JavaScript is optimized?
자바스크립트 코드를 최적화하기 위한 다양한 기준이 있다. 자바스크립트 코드가 인터프리터 또는 baseline complier에 전달되기 전에, 먼저 트리와 유사한 코드 구조인 AST(Abstract Syntax Tree)로 파싱되어야 한다.
자바스크립트 애플리케이션을 실행할 때, 애플리케이션 시작 시 모든 코드가 필요하지는 않다. 예를 들어 버튼 클릭과 같은 사용자 작업에서 호출되는 함수가 있는 경우 해당 코드를 나중에 파싱할 수 있다.
즉시 파싱해야 하는 항목을 식별하고 기계어를 생성하는 것이 더 빠른 애플리케이션 부트스트랩을 위한 최상의 전략이다.
때때로 자바스크립트 코드는 단순화할 수 있는 불필요하게 복잡한 로직을 포함한다. 예를 들어, 정수를 증가시키는 for 루프는 n번의 + 연산을 이용해 인라인될 수 있다. 이러한 프로세스를 Loop unrolling이라고 한다. function inlining을 사용하여 유사한 최적화를 수행할 수 있다.
자바스크립트의 타입 시스템 부족은 자바스크립트 엔진이 덜 최적화된 기계어 코드를 생성하게 만드는 원인이다. 따라서 이미 정의된 값을 기반으로 자바스크립트 엔진은 변수의 자료형을 추측하고 더 나은 기계어를 생성할 수 있다.
이 전체 프로세스는 Paul Ryan의 V8 엔진에 대한 블로그 게시물에 자세히 나와있다. 이러한 개념을 깊이 있게 이해하려면 해당 게시물을 반드시 확인해라.
한편 자바스크립트 엔진이 또 할 수 있는 일은 코드 실행에 대한 프로파일링 데이터를 수집하고 느리게 실행되는 코드를 찾는 것이다. 이 코드는 CPU를 태우기 때문에 "핫" 코드라고 한다. 이 코드는 더욱 최적화될 수 있으며 최적화된 기계어로 대체될 수 있다.
이러한 사항과 full-codegen 및 crankshaft로 인한 문제를 염두에 두고, V8 팀은 처음부터 새로운 버전의 V8 엔진을 만들었다. 이 새 버전의 자바스크립트 엔진은 2017년에 출시되었다.
위 그림에서 볼 수 있듯이, V8 팀은 새로운 인터프리터 파이프라인인 Ignition을 도입하였다. 이는 baseline complier를 사용하여 자바스크립트 소스 코드에서 바이트 코드를 생성하고, 나중에 인터프리터를 사용하여 해당 바이트 코드를 해석하는 역할을 한다.
최적화 컴파일러(optimization complier)인 Turbofan은 응용 프로그램이 실행 중일 때 백그라운드에서( 별도의 스레드에서 ) 바이트 코드를 최적화할 수 있으며, 이후에 대체될 매우 최적화된 기계어를 생성할 수 있다.
Turbofan은 Ignition 인터프리터로부터 프로파일링 데이터를 수신하고 핫 코드를 찾는다. 코드를 더 잘 최적화하는 방법을 추측하고( 자료형을 추측하여 ), 코드를 최적화하거나 최적화를 해제(de-optimize)할 수 있다.
What about other JavaScript engine?
우리는 V8 자바스크립트 엔진이 작동하는 방법에 대한 대략적인 개요를 살펴보았다. Firefox 브라우저용 SpiderMonkey 엔진 및 Internet Explorer 용 Chakra 엔진과 같은 유사한 모델이 이를 뒤따르고 있다.
일부 자바스크립트 엔진은 여러 개의 baseline 및 최적화 컴파일러가 있기 때문에 복잡해 보일 수 있지만, 간단히 말해서 동일한 최적화 모델을 따른다.
V8은 구글에서 개발했기 때문에 가장 널리 사용되는 자바스크립트 엔진 중 하나이다. V8 엔진은 끊임없이 진화하고 빨라지고 있다. 구글 크롬 외에도 Chromium 프로젝트, Electron.js 그리고 서버 사이드 자바스크립트 런타임인 Node.js도 V8 엔진을 사용한다.
JavaScript at runtime
자바스크립트 영역을 보호하기 위해 헌신하는 열정적인 프론트엔드 / 백엔드 개발자들이 많이 있다. 자바스크립트는 매우 이해하기 쉽고, 프론트엔드 개발의 필수적인 부분이다.
다른 프로그래밍 언어와 달리, 자바스크립트는 런타임 시 싱글 스레드 언어이다. 이는 코드 실행이 한 번에 하나씩 수행되는 것을 의미한다. 코드 실행이 순차적으로 이루어지기 때문에, 실행 시간이 오래 걸리는 코드는 그 이후에 실행되어야 하는 모든 것을 차단한다. 따라서 Chrome을 사용하는 동안 때때로 다음과 같은 화면을 볼 수 있다.
브라우저에서 웹 사이트를 열었을 때, 웹 사이트는 단일 자바스크립트 실행 스레드를 사용한다. 이 스레드는 웹 페이지 스크롤, 웹 페이지에 무언가를 출력, DOM 이벤트 수신(예: 사용자가 버튼을 클릭할 때) 및 기타 작업과 같은 모든 것을 처리한다.
그러나 자바스크립트 실행이 차단되면, 브라우저는 이러한 모든 작업을 중지한다. 즉, 브라우저가 정지되고 해당 작업이 완료될 때까지 응답하지 않는다.
무한 while loop를 통해 위와 같은 상황을 실제로 볼 수 있다.
while(true){}
while 루프는 시스템 리소스가 부족할 때까지 무한 반복되므로, 위 명령문 이후의 코드는 실행되지 않는다. 이는 무한 재귀 함수 호출에서도 발생할 수 있다.
최신 브라우저 덕분에, 열려 있는 모든 브라우저 탭이 단일 자바스크립트 스레드에 의존하는 것은 아니다. 대신 탭 또는 도메인별로 개별적인 자바스크립트 스레드를 사용한다. Google Chrome의 경우, 서로 다른 웹사이트 여러 탭을 열고 무한 while 루프 위에서 실행할 수 있다.
그러면 해당 코드가 실행된 현재 탭만 정지되고 다른 탭은 정상적으로 작동한다. 동일한 도메인/웹사이트의 페이지가 열려 있는 모든 탭들은 정지된다. 이는 Chrome이 하나의 사이트당 하나의 프로세스 정책을 구현했기 때문이다. 하나의 프로세스는 동일한 자바스크립트 실행 스레드를 사용한다.
자바스크립트의 프로그램 실행 방법을 시각화하기 위해서는 자바스크립트 런타임과 그 안에서 역할을 하는 다른 구성 요소들을 이해해야 한다. 이를 시각화하기 위해 간단한 자바스크립트 프로그램을 작성해보자.
function baz() {
console.log( 'Hello from baz' );
}
function bar() {
baz();
}
function foo() {
bar();
}
foo();
foo, bar, baz 세 개의 함수를 가진 간단한 자바스크립트 프로그램이 있다. 함수 foo는 함수 bar를 호출하고, 함수 bar는 함수 baz를 호출한다. 함수 baz는 런타임에 의해 제공된 console.log 함수를 사용하여 콘솔에 무언가를 기록하는 함수이다.
이 프로그램을 실행하면, 먼저 foo 함수가 호출된 다음 console.log()가 실행될 때까지 호출 체인(call chain)이 시작된다. 다이어그램을 사용하여 이를 시각화하고 런타임의 다양한 구성 요소를 검사해보자.
다른 프로그래밍 언어와 마찬가지로 자바스크립트 런타임은 하나의 스택과 하나의 힙 저장소가 있다. 힙은 임의의 순서로 메모리를 저장할 수 있는 무료 메모리 저장 장치이다. 상당한 시간 동안 지속될 데이터는 힙 내부로 들어간다. 힙은 자바스크립트 런타임에 의해 관리되고 가비지 컬렉터에 의해 정리된다. 힙에 대해 여기를 참고하라.
우리가 관심을 갖는 것은 스택이다. 스택은 프로그램의 현재 함수 실행 컨텍스트를 저장하는 LIFO(후입선출) 형태의 데이터 저장소이다. 위의 예시에서 프로그램이 메모리에 로드되면, 첫번째 함수인 foo를 호출로부터 실행을 시작한다.
따라서 첫 번째 스택 엔트리는 foo()이다. 함수 foo가 함수 bar를 호출하므로, 두 번째 스택 엔트리는 bar()이다. 함수 bar가 함수 baz를 호출하므로, 세 번째 스택 엔트리는 baz()이다. 마지막으로 함수 baz가 console.log를 호출하므로, 네 번째 스택 엔트리는 console.log(’Hello from baz’)이다.
함수가 무언가를 반환할 때까지(함수가 실행되는 동안), 스택에서 제거되지 않는다. 스택은 해당 엔트리(함수)가 어떤 값을 반환할 때마다 엔트리를 하나씩 팝하고 보류중인 함수 실행을 계속한다.
스택에서 각각의 엔트리를 스택 프레임이라고 한다. 스택 프레임은 호출된 함수의 인수, 지역 변수, 반환 주소값(반환 값이 사용되는 위치)과 같은 함수 호출에 대한 정보, 그리고 함수에 대한 다른 정보들을 포함한다.
위 그림처럼 console.log 함수 호출에 브레이크 포인트를 추가했을 때, 크롬 개발자 도구는 현재 함수 실행까지의 스택 프레임을 포함하는 콜스택을 표시해준다.
스택 프레임에서 임의의 함수 호출이 오류를 생성하는 경우, 자바스크립트는 해당 스택 프레임까지 코드 실행의 스냅샷인 stack trace를 출력한다.
function baz(){
throw new Error('Something went wrong.');
}
function bar() {
baz();
}
function foo() {
bar();
}
foo();
위 프로그램은 함수 baz 내부에서 오류가 발생한다. 자바스크립트에서 오류가 발생하면 아래 그림처럼 stack trace를 출력하여 무엇이 잘못되었는지, 그리고 어디에서 잘못되었는지를 표시한다.
위 그림처럼, 크롬의 개발자 도구는 에러 메세지를 표시할 뿐만 아니라 오류가 발생한 스택 프레임까지 stack trace도 보여준다. 만약 함수 baz가 에러 이후 다른 함수를 호출한다면, 해당 함수는 스택에 푸시되지 않을 것이다.
만약 자바스크립트의 스택 추적 및 이를 최대한 활용하는 법에 대해 자세히 알아보려면 스택 추적 API에 대한 해당 V8 문서를 읽어봐라.
자바스크립트는 싱글 스레드이기 때문에 프로세스 당 하나의 스택과 하나의 힙을 가진다. 따라서 다른 프로그램이 무언가를 실행하려면, 이전 프로그램이 완전히 실행될 때까지 기다려야 한다. 이 스레드는 일반적으로 메인 스레드 또는 메인 실행 스레드로 알려져 있다.
한 가지 시나리오를 생각해보자. 브라우저가 네트워크를 통해 일부 데이터를 로드하거나, 웹 페이지에 표시할 이미지를 로드하기 위해 HTTP 요청을 보내는 경우 어떻게 되는가? 해당 요청이 완료될 때까지 브라우저는 정지하는가? 그렇다면 사용자 경험은 매우 나쁠 것이다.
브라우저는 웹 애플리케이션(웹 페이지)에 포함된 모든 자바스크립트를 실행하는 자바스크립트 엔진이 함께 제공된다. 예를 들어, 구글은 V8 자바스크립트 엔진을 사용한다.
그러나 브라우저는 자바스크립트 엔진 그 이상의 것들을 사용한다. 아래 그림은 브라우저의 내부 구조를 나타낸 것이다.
복잡해 보이지만 각각의 개별 구성 요소들과 이들이 조화를 이루어 동작하는 방식을 이해하는 것이 좋다. 자바스크립트 런타임은 실제로 구성 요소가 추가로 2개 더 있다. 이벤트 루프와 콜백 큐. 콜백 큐는 메시지 큐 또는 태스크 큐라고도 한다.
브라우저는 자바스크립트 엔진 외에도 HTTP 요청, DOM 이벤트 수신, setTimeOut 및 setInterval을 통한 실행 지연, 캐싱, 데이터베이스 저장 등 여러 작업을 수행할 수 있는 다양한 애플리케이션을 포함한다. 브라우저의 이러한 기능은 풍부한 웹 애플리케이션과 더 나은 사용자 경험을 만드는 데 도움이 된다.
만약 브라우저가 이러한 작업들을 수행하기 위해 동일한 자바스크립트 스레드를 사용해야 한다면, 사용자 경험은 최악일 것이다. 예를 들어, 브라우저가 HTTP 네트워크 응답을 수신할 때 동일한 자바스크립트 스레드를 사용하여 해당 작업을 수행해야 한다면, 웹 페이지는 몇 초에서 몇 분동안 응답하지 않을 것이다.
그러므로 브라우저는 HTTP 요청, 응답 수신과 같은 작업을 수행하기 위한 자체 로직을 구현한다. 이러한 작업들은 브라우저에서 관리하는 다른 스레드에서 수행되고 자바스크립트는 이를 인식하지 못하므로, 자바스크립트의 메인 실행 스레드를 차단시키지 않는다.
브라우저는 이러한 기능을 구현하는데 C 또는 C++와 같은 저수준 프로그래밍 언어를 사용했을 것이다. 이를 통해 성능상의 이점, 그리고 자바스크립트에서 이러한 작업을 수행할 수 있도록 하는 깔끔한 자바스크립트 API를 제공한다. 예를 들어, 브라우저는 HTTP 요청을 보내기 위한 fetch API를 제공한다. 이러한 API들은 자바스크립트 사양의 일부가 아니므로 Web APIs로 알려져 있다.
이러한 Web APIs는 비동기식이다. 즉, 이러한 API가 백그라운드에서 작업을 수행하고, 작업을 완료하면 데이터를 반환하도록 지시할 수 있으며, 그동안 자바스크립트 코드를 계속 실행할 수 있다. API가 백그라운드에서 작업을 수행하도록 지시하는 동안 콜백 함수를 제공해야 한다. 콜백 함수의 역할은 Web API가 작업을 완료한 후 자바스크립트 메인 스레드에서 일부 자바스크립트 코드를 실행하는 것이다. 모든 구성 요소들이 어떻게 함께 동작하는지 이해해보자.
어떠한 함수를 호출하면, 해당 함수는 스택에 푸시될 것이다. 만약 그 함수가 Web API 호출을 포함한다면, 자바스크립트는 콜백 함수와 함께 이에 대한 제어를 Web API에 위임하고, 그 함수가 무언가를 반환할 때까지 다음 줄로 이동한다. 이제 콜백 함수는 메인 스레드와 별개인 별도의 스레드에서 작업을 수행하는 Web API와 함께 제공된다.
함수가 return문에 도달하면, 해당 함수가 스택에서 팝되고 다음 스택 엔트리로 이동한다. 한편 Web API는 백그라운드에서 작업을 수행하고 해당 작업과 관련된 콜백 함수를 기억한다. 작업이 완료되면 Web API는 해당 작업의 결과를 콜백 함수에 바인딩하고, 메시지 큐(일명 콜백 큐)에 콜백 함수와 함께 메시지를 게시(publish)한다.
이벤트 루프의 유일한 작업은 콜백 큐를 살펴보고 콜백 큐에 보류 중인 항목이 있으면 해당 콜백을 스택으로 푸시하는 것이다. 이벤트 루프는 스택이 비어 있으면 한 번에 하나의 콜백 함수를 스택으로 푸시한다. 나중에 스택은 콜백 함수를 실행할 것이다.
setTimeOut Web API를 사용하여 모든 것이 단계별로 동작하는지 살펴보자. setTimeOut Web API는 일정 시간 이후에 무언가를 실행할 때 주로 사용된다. 이 실행은 프로그램의 모든 코드가 실행이 완료되면 일어난다(스택이 비어 있을 때). setTimeOut의 구문은 다음과 같다.
setTimeout(callbackFunction, timeInMilliseconds);
callbackFunction은 timeInMilliseconds 이후에 실행되는 콜백 함수이다. 이전 프로그램을 수정하고, 이 API를 사용해보자.
function printHello() {
console.log('Hello from baz');
}
function baz() {
setTimeout(printHello, 3000);
}
function bar() {
baz();
}
function foo() {
bar();
}
foo();
프로그램에서 유일하게 수정된 부분은 printHello 함수의 실행을 3초 지연시킨 것이다. 위 경우 스택은 foo() ⇒ bar() ⇒ baz() 순으로 쌓일 것이다. 일단 함수 baz가 실행되고 setTimeOut API를 호출하면, 자바스크립트는 콜백 함수를 Web API로 전달하고, 다음 줄로 이동한다.
다음 줄이 없기 때문에 함수가 반환되면서 스택에서 baz, bar, foo 순으로 팝될 것이다. Web API는 3초가 경과하기를 기다린다. 3초가 지나면 해당 콜백 함수를 콜백 큐에 푸시하고, 스택이 비어있으므로 이벤트 루프는 이 콜백 함수를 스택에 푸시한다.
이벤트 루프와 콜백 큐는 동일한 퍼즐의 조각이다. 이들은 자바스크립트 엔진의 일부가 아니라 자바스크립트 엔진 외부에 있으며, 일반적으로 웹 브라우저나 Node.js와 같은 런타임에 의해 제공된다. 이벤트 루프는 자바스크립트 엔진의 API를 사용하여 통신하고 실행할 콜백 함수를 제공한다.
Even loop inside Node.js
Node.js는 더 많은 것들을 규정하기 때문에 많은 일을 해야 한다. 브라우저의 경우 백그라운드에서 수행할 수 있는 작업이 제한된다. 그러나 노드에서는 간단한 자바스크립트 프로그램의 경우에도 대부분의 작업을 백그라운드에서 수행할 수 있다.
Node.js는 Google의 V8 엔진을 사용하여 자바스크립트 런타임을 제공하고 libuv 라이브러리(c로 작성)를 사용하는 자체 이벤트 루프를 채택한다. 노드는 Web API와 같은 콜백 접근 방식을 따르며 브라우저와 유사한 방식으로 동작한다.
위의 노드 다이어그램은 브라우저 다이어그램과 유사한 것을 볼 수 있다. 오른쪽 섹션은 Web API처럼 보이지만 이는 이벤트 큐(콜백 큐/메시지 큐) 및 이벤트 루프도 포함한다. 이벤트 큐 및 이벤트 루프는 싱글 스레드에서 실행되는 반면, 작업 스레드(worker thread)는 비동기 I/O 작업을 제공해야 한다. 이것이 바로 Node.js를 이벤트 기반 non-blocking asynchronous I/O 아키텍처라고 하는 이유이다.
이벤트 루프와 콜백 큐 외에도, 최신 자바스크립트 엔진은 프로미스를 사용하기 위한 마이크로 태스크 큐 또한 포함한다. 해당 게시글을 참고해라.
※ 위 Node.js의 이벤트 루프 그림은 잘못된 그림일 수 있으니 해당 게시글을 참고해 주시기 바랍니다.
원문
https://medium.com/jspoint/how-javascript-works-in-browser-and-node-ab7d0d09ac2f