[Node.js] 자바스크립트로 백엔드 입문하기 ❶

이 글은 [Node.js 백엔드 개발자 되기]에서 발췌했습니다.
골든래빗 출판사

서버 사이드 자바스크립트 런타임인 Node.js로 백엔드에 입문하겠습니다. 어떤 원리로 서버 사이드에서 자바스크립트가 실행되는지 알아보고, 기술적인 특징인 싱글 스레드, 이벤트 루프, 그리고 Node.js 장단점을 알아봅시다. 총 2편입니다.

 

[Node.js] 자바스크립트로 백엔드 입문하기 ❶

 

1. Node.js 소개

Node.js는 서버 측 자바스크립트 런타임 환경입니다. 라이언 달*이 2009년 오픈 소스로 공개했습니다. Node.js는 브라우저 밖에서 자바스크립트를 사용하는 V8 엔진*을 사용합니다. Node.js 이전에는 논블로킹*/비동기* API를 서버 환경에 구현하는 데 상당한 노고가 필요했습니다. 동시 실행되는 스레드와 공유 자원을 프로그래머가 직접 만들고 관리해야 했기 때문입니다. 라이언 달은 이런 개발 환경을 논블로킹 감옥*이라고 표현했습니다. 비동기로 API를 제공하는 것이 편리하다고 생각한 라이언 달은 이벤트 기반(Event-Driven) 비동기 환경을 만들고 ‘JSConf EU20096’에서 Node.js라는 자바스크립트 런타임 환경을 처음 소개했습니다.

2010년에는 npm이라는 패키지 매니저를 공개했습니다. 오늘날 최신 언어들은 모두 잘 만든 패키지 매니저를 가지고 있습니다만, 2010년에는 npm처럼 편리한 패키지 매니저가 거의 없었습니다. 2011년에 마이크로소프트와 조이언트(Joyent)가 협력해 윈도우 버전의 Node.js를 출시했습니다. 그 전까지는 리눅스와 맥OS만 지원했습니다.

2012년에 npm을 만든 아이작 슐레이터로, 2014년에 티모시 J. 퐁텐으로 프로젝트 리더가 바뀝니다. 2014년 12월에는 Node.js 개발을 주도하던 조이언트 사와 지배구조 문제가 생겨서 io.js라는 이름으로 프로젝트가 분리되었습니다. 2015년 9월 Node.js v0.12와 io.js v3.3이 Node.js v4.0으로 병합됩니다. 이후 JS 재단과 Node.js 재단이 오픈JS(OpenJS) 재단으로 합쳐져 오늘날까지 이어지고 있습니다.

2021년 스택오버플로 설문에 따르면 가장 많이 사용되는 웹 프레임워크* 기술에서 익스프레스(Node.js 기반 웹 서버)가 3위를 차지할 정도로 Node.js는 많이 사용됩니다. Node.js는 I/O에 대한 관점을 완전히 새롭게 해주었다는 점에서는 프로그래밍 발전에 중요한 역할을 했습니다.

 

* 라이언 달은 현재 디노(Deno)라는 타입스크립트 기반 서버 런타임 환경을 만듭니다. Node를 No와 de로 잘라서 순서를 바꾸면 Deno가 됩니다. 라이언이 Node.js 프로젝트를 떠나게 된 것은 아쉽지만, 계속해서 좋은 발명품을 만들어서 개발 생태계를 풍성하게 해주길 기대합니다.

* https://v8.dev/blog

* 논블로킹은 함수 실행 완료를 기다리지 않고 다음 코드를 실행하는 것을 말합니다. 자세한 내용은 Node.js의 공식 문서를 확인해주세요. (https://nodejs.org/ko/docs/guides/blocking-vs-non-blocking)

* Node.js 백엔드 개발자 되기 5장 ‘자바스크립트의 비동기 처리’에서 비동기를 다룹니다.

* https://www.youtube.com/watch?v=F6k8lTrAE2g

* https://www.youtube.com/watch?v=YVvQhZbCb6c

* 스택오버플로 2021 개발자 설문 https://insights.stackoverflow.com/survey/2021#most-popular-technologies-webframe

 

2. Node.js는 서버에서 어떻게 자바스크립트를 실행할까?

Node.js는 V8 자바스크립트 엔진과 libuv 및 C/C++에 의존성을 가진 자바스크립트 런타임입니다. 런타임은 자바스크립트로 된 프로그램을 실행할 수 있는 프로그램입니다. 예를 들어 자바 코드는 자바 실행 환경인 JRE(Java Runtime Environment) 위에서 실행됩니다. C# 코드는 CLR(Common Language Runtime)이라는 런타임에서 실행됩니다.

 

💡Note: 반면 C 언어는 런타임 없이 코드를 실행합니다. C 언어처럼 컴파일한 결과물이 특정 CPU의 기계어인 언어를 네이티브 언어라고 합니다.

 

브라우저 세상에서만 작동하던 자바스크립트가 어떻게 해서 서버에서도 작동할 수 있게 되었는지 Node.js의 구성요소와 구조를 살펴봅시다.

 

2.1 Node.js의 구성요소

Node.js의 소스 코드*는 C++와 자바스크립트, 파이썬 등으로 이루어져 있습니다. 구성요소는 다음과 같습니다(파이썬 코드는 빌드와 테스트에만 사용되므로 구성요소에서는 제외했습니다).

 

▼ Node.js의 구성요소

* Node.js 소스 코드 URL : https://github.com/nodejs/node

 

Node.js는 각 계층이 각 하단에 있는 API를 사용하는 계층의 집합으로 설계되어 있습니다. 즉 ❶ 사용자 코드(자바스크립트)는 ❷ Node.js의 API를 사용하고, ❷ Node.js API는 C++에 바인딩되어 있는 소스이거나 직접 만든 ❸ C++ 애드온을 호출합니다. ❹ C++에서는 V8을 사용해 자바스크립트를 해석(JIT 컴파일러) 및 최적화하고 어떤 코드냐에 따라 C/C++ 종속성이 있는 코드를 실행합니다. 또한 DNS, HTTP 파서, OpenSSL, zlib 이외의 C/C++ 코드들은 ❺ libuv의 API를 사용해 해당 운영체제에 알맞는 API를 사용합니다.

Node.js의 구성요소 중 특히 V8과 libuv가 중요합니다. V8은 자바스크립트 코드를 실행하도록 해주고, libuv는 이벤트 루프 및 운영체제 계층 기능을 사용하도록 API를 제공합니다. V8과 이벤트 루프는 따로 다시 설명드리겠습니다. Node.js의 구성요소를 다음 표에 간략히 설명해두었습니다.

 

▼ Node.js의 구성요소

 

2.2 자바스크립트 실행을 위한 V8 엔진

V8*은 C++로 만든 오픈 소스 자바스크립트 엔진입니다. ‘엔진’은 사용자가 작성한 코드를 실행하는 프로그램을 말합니다. 엔진은 파서, 컴파일러, 인터프리터, 가비지 컬렉터, 콜 스택, 힙으로 구성되어 있습니다. V8 엔진은 자바스크립트를 실행할 수 있는 엔진이며, 인터프리터 역할을 하는 이그니션과 컴파일러 역할을 하는 터보팬을 사용해 컴파일합니다.

 

💡Note

  • 가비지 컬렉터(Garbage Collector): 메모리 누수를 방지하기 위해 주기적으로 사용하지 않는 메모리 공간을 회수하는 기능입니다.
  • 콜 스택(call stack): 콜 스택은 현재 실행 중인 서브 루틴에 관한 정보를 저장하는 스택
  • 힙(Heap): 힙은 객체나 동적 데이터가 저장되는 메모리 공간. 자료구조의 힙이 아닙니다.

 

다음은 V8 엔진이 어떤 방식으로 자바스크립트 소스를 컴파일하는지 나타내는 그림입니다.

 

▼ V8엔진의 자바스크립트 코드 컴파일 단계

 

자바스크립트 코드는 ❶ 파서에 전달되어→ ❷ 추상 구문 트리로 만들어집니다. 이후 ❸ 이그니션 인터프리터에 전달되면 → 이그니션은 추상 구문 트리를 ❹ 바이트코드로 만듭니다. ❺ 최적화가 필요한 경우이면 터보팬으로 넘깁니다. 그러면 ❺ 터보팬에서 컴파일 과정을 걸쳐서 ❻ 바이너리 코드가 됩니다. 최적화가 잘 안 된 경우는 ❼ 다시 최적화를 해제하고 이그니션의 인터프리터 기능을 사용합니다.

이처럼 인터프리터와 컴파일러의 장점을 동시에 가지고 있는 프로그램을 JIT(just-in time) 컴파일러라고 합니다. 속도가 빠르며, 적재적소에 최적화할 수 있다는 장점과 컴파일러와 인터프리터가 동시에 실행되어 메모리를 더 많이 쓴다는 단점이 있습니다.

 

▼ 이그니션과 터보팬의 특징 비교

 

2.3 이벤트 루프와 운영체제 단 비동기 API 및 스레드 풀을 지원하는 libuv

V8 엔진을 사용해서 서버에서 자바스크립트를 실행할 수 있다는 것을 이제 알았습니다. 그러면 Node.js는 HTTP, 파일, 소켓 통신 IO 기능 등 자바스크립트에는 없는 기능을 어떻게 제공하는 걸까요?

Node.js는 이 문제를 libuv라는 C++ 라이브러리를 사용해 해결합니다(libuv는 비동기 입출력, 이벤트 기반에 초점을 맞춘 라이브러리입니다). 그래서 자바스크립트 언어에서 C++ 코드를 실행할 수 있게 해두었습니다. 자바스크립트로 C++ 코드를 감싸서 사용하는 방식입니다(C++ 바인딩이라고 합니다).

 

▼ libuv 아키텍처*

 

libuv는 다양한 플랫폼에서 사용할 수 있는 이벤트 루프를 제공합니다(리눅스는 epoll, 윈도우는 IOCP, 맥OS는 kqueue, SunOS는 이벤트 포트). 또한 네트워크, 파일 IO, DNS, 스레드 풀 기능을 추가로 제공합니다. Node.js에서는 C++ 바인딩 기능으로 자바스크립트에서 libuv의 API를 사용합니다.

* http://docs.libuv.org/en/v1.x/design.html

 

2.4 Node.js 아키텍처

지금까지 Node.js를 구성하는 주요한 항목을 살펴보았습니다. 요약하면 Node.js는 자바스크립트 코드 실행에 필요한 런타임으로 V8 엔진을 사용하고, 자바스크립트 런타임에 필요한 이벤트 루프 및 운영체제 시스템 API를 사용하는 데는 libuv 라이브러리를 사용합니다. Node.js 애플리케이션의 코드가 어떻게 실행되는지를 살펴봅시다.

 

▼ Node.js 아키텍처

 

❶ 애플리케이션에서 요청이 발생합니다. V8 엔진은 자바스크립트 코드로 된 요청을 바이트 코드나 기계어로 변경합니다. ❷ 자바스크립트로 작성된 Node.js의 API는 C++로 작성된 코드를 사용합니다. ❸ V8 엔진은 이벤트 루프로 libuv를 사용하고 전달된 요청을 libuv 내부의 이벤트 큐에 추가합니다. ❹ 이벤트 큐에 쌓인 요청은 이벤트 루프에 전달되고, 운영체제 커널에 비동기 처리를 맡깁니다. 운영체제 내부적으로 비동기 처리가 힘든 경우(DB, DNS 룩업, 파일 처리 등)는 워커 스레드에서 처리합니다. ❺ 운영체제의 커널 또는 워커 스레드가 완료한 작업은 다시 이벤트 루프로 전달됩니다. ❻ 이벤트 루프에서는 콜백으로 전달된 요청에 대한 완료 처리를 하고 넘깁니다. ❼ 완료 처리된 응답을 Node.js 애플리케이션으로 전달합니다.

 

Node.js는 싱글 스레드라고 했는데, 워커 스레드가 있으면 싱글 스레드가 아니지 않나요?

Node.js의 이벤트 루프 부분이 싱글 스레드이고, 운영체제에서 비동기 I/O를 지원하지 않거나 구현이 복잡한 경우는 libuv 내부의 스레드 풀을 사용합니다. 즉 Node.js의 프로세스는 이벤트 루프에 사용하는 싱글 스레드 하나와 비동기 처리를 지원하는 스레드 풀로 구성되어 있습니다.

 

다음 편에서는 Node.js의 기술적 특징을 알아봅시다.

 

박승규


아직도 개발이 재미 있는 15년차 천상 개발자입니다. 웹 개발, 게임 백엔드 개발, 플랫폼 및 인프라 개발 등 다양한 영역을 경험했습니다. 현재는 카카오엔터테인먼트에서 백엔드 개발자로 일합니다.


현) 카카오엔터테인먼트 페이지 서비스 개발팀
전) 트리노드 (포코팡, 포코포코) 서버 개발자
전) NHN Japan 플랫폼 개발팀

Leave a Reply

©2020 GoldenRabbit. All rights reserved.
상호명 : 골든래빗 주식회사
(04051) 서울특별시 마포구 양화로 186, 5층 512호, 514호 (동교동, LC타워)
TEL : 0505-398-0505 / FAX : 0505-537-0505
대표이사 : 최현우
사업자등록번호 : 475-87-01581
통신판매업신고 : 2023-서울마포-2391호
master@goldenrabbit.co.kr
개인정보처리방침
배송/반품/환불/교환 안내