들어가며
가끔씩 다양한 언어의 빌드를 지원하는 사이트를 보면 자바스크립트라는 언어로 코드를 작성하고 싶은데, 자바스크립트는 안 보이고 node.js라고 쓸 때가 있다. 예를 들어서 repl.it이 그렇다. 처음에는 어째서 자바스크립트가 아닐까 생각했는데, node.js 생태계에 익숙해지면서 node.js라고 적는 이유를 알 것 같게 되었다.
그 외에도 다양한 언어를 통해서 서버를 만들면서 자바스크립트의 작동 원리는 굉장히 신기했다. 자바스크립트 싱글 스레드 기반이며 논 블로킹 패러다임의 비동기적인 동시성 언어입니다. 라는 한 줄에 담긴 심오한 작동 원리가 있다.
그 외에도 웹 브라우저를 위해 태어난 언어인 자바스크립트를 높은 퍼포먼스로 작동시키기 위해 C++로 짜인 node.js의 모듈이 돕는다거나, 이벤트 루프와 콜백 큐와 같은 재미있는 개념들이 있다. 사람에 따라 다르지만 나는 이러한 자바스크립트의 특성을 좋아한다.
본론
node.js
자바스크립트에 대한 장황한 설명을 하기 전에 먼저 node.js에 대한 설명을 살펴보자. 공식 홈페이지인 nodejs.org의 소개에 따르면 다음과 같다.
node.js는 오픈 소스 및 크로스 플랫폼 자바스크립트 런타임 환경입니다. 거의 모든 프로젝트에 널리 사용되는 도구입니다! Node.js는 구글 크롬의 핵심인 V8 자바스크립트 엔진을 브라우저 외부에서 실행합니다. 따라서 Node.js의 성능은 매우 뛰어납니다.
브라우저 외부에서 실행한다는 점이 포인트인데, 자바스크립트는 원래 웹사이트를 위해서 태어난 언어이다. 1995년 넷스케이프 커뮤니케이션즈의 브렌던 아이크에 의해서 개발되었는데, 이때 대부분의 웹 페이지는 정적이었다. 이러한 웹 환경에서 사용자 상호작용을 개선하고, 브라우저에서 동적인 기능을 제공하기 위해서 자바스크립트가 탄생하게 된 것이다.
이렇게 태생이 웹 브라우저를 위한 자바스크립트를 동작시키기 위해서는 V8 엔진을 사용한다. 이러한 자바스크립트 엔진인 V8을 HTML, CSS와 함께 웹 브라우저에 탑재시켜 웹에서 동작시키게 하지 않고, 자바스크립트 자체만을 동작시키기 위해서는 V8 엔진을 브라우저의 밖에서도 실행시킬 수 있게 해야 한다. 그러한 것을 구현한 것이 바로 node.js인 것이다!
Node.js 앱은 모든 요청에 대해서 새 스레드를 생성하지 않고 단일 프로세스에서 실행된다. Node.js는 표준 라이브러리에 Javascript 코드가 차단되는 것을 방지하는 비동기 I/O Primitive 세트를 제공하며, 일반적으로 Node.js 라이브러리는 논블로킹 패러다임을 사용하여 작성되므로 블로킹 동작이 일반적이지 않고 예외적으로 발생한다.
네트워크에서 읽기, 데이터베이스 또는 파일 시스템 액세스 등의 I/O 작업을 수행할 때 스레드를 차단하고 대기 중인 CPU 사이클을 낭비하는 대신, Node.js는 응답이 돌아오면 작업을 재개합니다.
첫 문단의 내용은 Node.js는 새 스레드를 생성하지 않고 단일 프로세스라는 점인데, 이는 자바스크립트가 싱글 스레드라는 것을 의미한다. 하지만 비동기 I/O 프리미티브 세트를 제공하고, 논블로킹 패러다임을 통해서 멀티 스레드처럼 동작한다는 것이다. 다음 문단에서 네트워크 트래픽을 타는 코드나, 데이터베이스 및 파일 시스템 액세스 등의 I/O 작업을 수행할 때, 스레드를 차단하지 않는다는 것이 아니라는 것이다. 뒤에 설명할 내용을 먼저 살짝 이야기하면 이러한 작업을 블로킹 동작으로 두지 않고 논블로킹으로 처리할 수 있게 하여 CPU 사이클을 낭비하지 않는다.
앞서 자바스크립트 엔진은 V8 엔진이라고 설명했는데, 이러한 V8 엔진의 구조는 Call Stack을 하나 가지고 있다. 정말 구조적으로 한 번에 하나밖에 처리 못하는 명실상부 싱글 스레드 언어인데, 위의 블로킹을 일으킬 수 있을 함수를 다른 곳(Web APIs)에 제쳐두고 Call Stack에 쌓인 일을 처리함으로써 CPU 사이클을 낭비하지 않는, 멀티 스레딩처럼 보이게끔 동작한다는 것이다.
V8 엔진
먼저 자바스크립트를 실행시키는 엔진인 V8 엔진은 구글에서 제작한 C++로 작성된 자바스크립트 엔진이다. 구글에서 작성되었기 때문에 구글 크롬에 탑재되어 자바스크립트를 작동시키는 역할을 하고 있다. 오픈 소스이기 때문에 깃허브 레포지토리가 열려 있으며, 이를 node.js 및 크롬 브라우저에서 활용한다.
작동원리는 아래와 같은데, 먼저 자바스크립트 소스 코드를 파서에게 넘긴다. 파서는 소스 코드를 AST(추상 구문 트리)로 구분하여 이를 Ignition에게 넘긴다. ( AST는 쉽게 이야기하면 파서가 특정한 줄을 읽어, 해당 코드의 길이는 얼마인지, 타입은 얼마인지... 등등 깊게 코드에 대한 정보를 트리 형태로 나타낸 것이다 ) 이렇게 쪼개서 Ignition에게 넘기면 자바스크립트를 바이트 코드로 변환하는 인터프리터의 역할을 수행한다.
이렇게 바이트 코드의 레벨으로 소스 코드가 분해되면 컴퓨터가 실행시킬 수 있게 된다. 설명하지 않은 Compiler TurboFan과 Optimized Machine Code는 바이트 코드를 실행하면서 자주 실행되는 코드를 TurboFan으로 보내 최적화된 코드로 컴파일시키는 역할을 한다. 말 그대로 이러한 과정을 통해서 터보팬처럼 식히는 역할을 하는 것이다.
V8이 원래 제네시스 G90이나 기아 K9같은 차에 탑재되는 8 기통 엔진의 종류를 의미하는 단어를 생각하면 V8의 구조는 참으로 재미있다고 볼 수 있다. 이에 대한 자세한 설명을 담은 포스팅은 링크를 포스팅 말단에 남겨 두었다.
자바스크립트의 동작
싱글 스레드 == 싱글 콜 스택 == 한 번에 한 줄의 코드를 실행한다는 것 - Philip Roberts
앞서 V8 엔진에 대해서 설명했다. 파서부터 Ignition으로 이어지는 구조는 자바스크립트가 싱글 스레드라는 것을 보여준다. 이는 싱글 콜 스택을 가지고 있다는 뜻이고 한 번에 한 줄의 코드를 실행한다는 뜻이 된다. 그런데 어떻게 이런 싱글 스레드 언어가 멀티 스레드 언어와 같은 퍼포먼스를 내는 것일까? 그것은 바로 웹 API, 이벤트 루프, 콜백 큐의 도움을 통해서 구현하는 것이다. V8 엔진 로고 안에 위치한 Stack이 참고로 Call Stack이 된다.
위 사진은 크롬 브라우저에 탑재된 자바스크립트가 동작하는 원리이다. node.js는 Web API를 사용하지 않기 때문에 대신에 자체적인 모듈을 통해서 대체하여 역할을 수행한다. 비동기 처리를 지원하는 동작들은 이렇게 Web API에 쌓이게 되고 ( 예를 들어 setTimeout 및 네트워크 작업 ) 이러한 작업이 끝나면 callback queue로 보내진다. 이후 Event Loop가 이를 감지해서 적절하게 자바스크립트의 스택에 함수를 집어 넣는 역할을 한다.
다음의 예제를 통해서 Call Stack, Web Apis 그리고 Callback Queue로 떠나는 자바스크립트의 여정에 대해서 알아보자.
먼저, 일반적으로 비동기 함수가 없는 경우에 대해서 살펴보자. init(); 함수를 호출했기 때문에 해당 함수 안에 있는 console.log이 Call Stack에 쌓인다.
이렇게 쌓이게 된 console.log는 쌓이자마자 바로바로 처리해 버리게 된다. 모든 console.log들의 처리가 끝나면 마지막으로 init함수가 Call Stack에서 제거되면서 함수의 실행은 끝난다. 이는 일반적인 동기 함수가 실행되는 경우이다. 다음으로는 비동기 함수에 대해서 알아보자.
가장 유명한 콜백 함수는 setTimeout이다. 자바스크립트에서 일반적인 동기 함수는 실행시키면 Call Stack에 쌓였다가 하나씩 처리하게 된다.
하지만 setTimeout와 같은 비동기 함수는 먼저 Call Stack에 간 다음 WebApis로 가게 된다. Web Apis내에서 동작을 마치면 콜백 함수(setTimeout 안에 들어 있는 함수)들을 콜백 큐에 집어 넣는다. 이후 이벤트 루프가 하나씩 하나씩 함수를 콜 스택으로 올려 보내게 된다.
setTimeout 비동기 함수를 여러개 쓴다고 하자. 소스 코드를 한 줄씩 훑으며 차례대로 Call Stack에 먼저 갔다가 Web Apis로 보내지게 된다. 그 후 setTimeout에 들어 있던 콜백 함수들이 처리되면 각자 Callback Queue에 쌓이게 되고, 이렇게 쌓인 큐를 이벤트 루프가 하나씩 Call Stack으로 보내게 된다. 이렇게 비동기 처리의 과정이 이러지는 것이다.
이러한 과정을 통해서 Call Stack( 콜 스택은 V8에 내장되어 있음), Web APis, Callback Queue, Event Loop들이 상호작용하면서 싱글 스레드 기반의 자바스크립트 언어가 멀티 스레드인 타 언어에 비해 꿀리지 않는 퍼포먼스를 내는 이유는 이러한 이유이다.
이번 포스팅에서는 자바스크립트의 동작 원리에 대해서 살펴보았다.
마치며
자바스크립트에 대해서 깊게 관심이 없었을 때는 그냥 인터프리터 언어겠거니 하면서 개발하기 시작했는데 ( 심지어 자바스크립트 딥 다이브를 스터디하면서도 큰 감명이 없었다 ) 시간이 흘러 프로그래밍 언어의 역사나 과거사들을 알게 되면서 갑자기 문득 자바스크립트에 대해서 의문점이 생기게 되었다. ( node.js란 뭐지? 왜 자바스크립트라고 안 부르는 거지? 태생이 웹페이지인 언어를 어떻게 쓰는 거지? )
개발을 하게 되면서 얻게 된 잡다한 지식 및 개발자들과 이야기를 통해서 어렴풋이 어떤 언어인지 알 수 있게 되었고, 이 참에 블로그 포스팅하기 위해 깊게 파고들기 시작했다. 생각보다 자바스크립트를 억지로 실행시키는 구조는 재미있었다는 것을 알 수 있었다.
자바스크립트에 대해서 우호적으로 쓰긴 했지만 자바스크립트에도 뭔가 이상한 포인트가 많기 때문에 ( 수많은 개발자들을 함정에 빠뜨리는 Date()와 심오하게 계산되는 NaN 및 Undefined 등 ) 개인적으로 좋아하지만 최고의 언어 같은 느낌은 아닌 것 같다. 이 글을 쓰는 지금에는 Deno가 나와서 node.js를 갈아치우려 한다거나, 자바스크립트(JS)에서 타입을 정적으로 지정해 주는 안정성 높은 타입스크립트(TS)를 쓰는 추세에서 몇몇 사람들이 TS를 JS로 교체한다거나 하는 다양한 일들이 일어나고 있지만, 나는 그런 사고뭉치 같은 이 언어를 좋아한다.
참고
- [node.js] Introduction : Introduction to Node.js
- [Evans Library] V8 엔진은 어떻게 내 코드를 실행하는 걸까? : https://evan-moon.github.io/2019/06/28/v8-analysis/
- [JSConf] What the heck is the event loop anyway? : https://www.youtube.com/watch?v=8aGhZQkoFbQ&list=PLKAkSNBeJkOU-FYXXob5GZISXrjZ1w29r&index=31&ab_channel=JSConf
감사합니다.
'Language > Javascript' 카테고리의 다른 글
웹 브라우저의 역사 ( Netscape, IE, firefox, safari ) (1) | 2024.11.22 |
---|