📁 학습

Node.js는 어떻게 동작할까


Node.js의 등장

Node.js는 자바스크립트 런타임입니다. Node.js가 등장하기 전에는 자바스크립트는 브라우저 환경에서만 구동될 수 있었지만 Node.js의 등장 이후 자바스크립트를 컴퓨터에서 실행할 수 있게 되었습니다. 2008년 구글이 V8엔진을 사용하여 크롬을 출시했고, V8 엔진을 통해 자바스크립트의 속도 문제가 해결되었습니다. 따라서 2009년에 라이언 달이 V8 엔진 기반의 노드 프로젝트를 시작하며 Node.js가 등장했습니다. Node.js는 V8과 더불어 libuv라는 C와 C++로 구현된 I/O 라이브러리로 구성되어 있습니다. 덕분에 Node.js는 비동기적이며 빠른 실행 환경을 제공할 수 있게 되었습니다.

Node.js의 동작 원리

1. 이벤트 기반

이벤트 기반 시스템에서는 특정 이벤트가 발생할 때 어떤 동작을 발생시킬지 미리 등록해두어야합니다. 이것이 바로 이벤트 리스너에 콜백 함수를 등록하는 것입니다.

이벤트 기반 시스템은 이벤트 루프, 콜스택, 테스크 큐, 백그라운드를 조합하여 동작합니다. 먼저, 각각의 구성요소가 하는 역할을 간단하게 짚고 넘어가겠습니다.

위의 구성요소들이 어떻게 이벤트 기반 시스템을 동작하게 하는지 아래 예시를 통해 이벤트 방식이 어떻게 동작 하는지 살펴보겠습니다.

1-1. Node.js의 이벤트 시스템 동작 과정

function run() {
    console.log("사실 아직 안갔지롱~");
}
 
console.log("안녕");
setTimeout(run, 1000);
console.log("잘가");

이 코드를 실행하면 console.log가 어떤 순서로 출력될까요? 로그의 내용으로도 유추할 수 있겠지만 “안녕” → “잘가” → “사실 아직 안갔지롱~”순서로 출력됩니다. 내부적으로 어떤 동작을 해서 다음과 같은 로그가 출력되는지 gif를 통해 살펴보겠습니다. 코드 1이 실행될 때 동작 과정

  1. 먼저 전역 컨텍스트인 anonymous가 호출 스택에 들어갑니다.
  2. console.log(”안녕”)이 호출 스택에 들어갑니다.
  3. 호출 스택의 맨 위에 있는 console.log(”안녕”)이 호출되고 “안녕”이라는 로그가 찍히게 됩니다.
  4. setTimeout(run,1000)이 호출 스택에 들어갑니다.
  5. setTimeout(run,1000)이 호출되면 타이머와 함께 run 콜백을 백그라운드로 보냅니다.
  6. 백그라운드 영역에서 1초를 세는 동안 console.log(”잘가”)가 호출 스택에 들어갔다가 pop되면서 실행되고, “잘가”라는 로그가 찍히게 됩니다.
  7. 백그라운드에서 1초를 센 후 run 함수를 태스크 큐로 보냅니다.
  8. 이벤트 루프는 호출 스택이 비어있으면 태스트 큐에 있는 run 함수를 꺼내 호출 스택에 올립니다.
  9. run 함수가 호출되며 스택에서 꺼내지고 “사실 아직 안갔지롱~”이라는 로그가 출력됩니다.

이벤트 루프는 호출 스택이 비어 있을 때만 태스크 큐에 있는 함수를 호출 스택으로 가져오기 때문에 호출 스택에 너무 많은 함수들이 있으면 타이머 시간이 모두 지난 후에도 콜백함수가 실행되지 않을 수 있습니다. 그래서 setTimeout의 시간이 정확하지 않은 경우가 발생할 수 있습니다.

1-2. Javascript 브라우저 환경에서 이벤트 방식의 동작 과정

Node.js 환경과 다르게 브라우저 환경에서는 이벤트 방식이 어떻게 동작하는지 다음 예시를 통해 살펴보도록 하겠습니다.

$.on("button", "click",
    function () {
        console.log("hello");
    }
)

위의 코드가 실행되면 클릭 이벤트 리스너에 console.log("hello")를 출력하는 콜백 함수가 등록됩니다. 그리고 브라우저 클릭 이벤트가 발생하면 Call Stack, Web Apis, Callback Queue에 다음과 같은 일이 일어납니다.

📌 맨 왼쪽 위부터 화살표 방향대로 진행됩니다. 버튼 클릭 이벤트 동작 과정

  1. 프로그램을 실행하면 전체 코드가 콜스택에 등록됩니다.
  2. 콜스택의 코드를 해석해서 클릭 이벤트 리스너에 콜백함수를 등록합니다.
  3. 버튼이 클릭되는 순간 callback queue에 anoymous가 등록됩니다. anoymous는 처음 실행 시의 전역 컨텍스트를 의미합니다. 자바스크립트 코드는 실행되는 순간 전역 컨텍스트 안에서 동작한다고 생각해주세요.
  4. 이벤트 루프에 의해 callback queue에 있던 전역 컨텍스트가 콜스택으로 옮겨지고, 클릭 이벤트 리스너의 콜백함수 안에 있던 console.log도 콜스택 영역으로 push됩니다.
  5. 콜스택 맨 위에 있는 동작부터 pop되면서 코드가 실행됩니다.

2. 논 블로킹 I/O

Node.js에서는 I/O 작업을 백그라운드로 넘겨서 동시에 처리합니다. 따라서 동시에 처리될 수 있는 작업은 최대한 묶어서 백그라운드로 넘겨야 시간을 절약할 수 있습니다.

❓이쯤되면 생기는 궁금증!!

Javascript는 싱글 스레드 언어인데 백그라운드의 I/O 작업은 어떻게 동시에 실행될까요? 만약 백그라운드의 작업이 동시에 실행된다면, setTimeout등을 통해 우리가 작성한 함수를 콜백으로 넘기면 동시에 실행될까요?

우리가 작성한 함수는 절대로 동시에 실행될 수 없습니다. 왜냐하면 Javascript는 싱글 스레드 언어이기 때문입니다. 그럼 여기서 또 의문점이 생깁니다. 백그라운드 영역에 타이머가 여러개이거나, 네트워크 요청이 여러개인 경우 모두 동시에 실행되기 때문입니다. 어떻게 이것이 가능할까요?

바로 libuv라는 Nodejs의 코어 라이브러리 덕분입니다. libuv에서 타이머, 파일 I/O, 네트워크 요청 등을 비동기적으로 처리합니다. 이 작업들은 백그라운드에서 병렬로 처리되고, 작업이 완료되면 해당 작업의 콜백 함수가 실행됩니다.

그렇다면 우리는 왜 setImmediate같은 것을 사용해서 콜백 함수를 등록하는 걸까요? 어차피 동시에 실행될 수도 없는데… 그것은 바로 작업의 순서를 변경해주기 위해서 입니다. 작업의 순서를 순서를 변경함으로써 엄청나게 오래걸리는 작업 때문에 간단한 작업들이 대기하는 상황을 막을 수 있습니다.

3. 싱글 스레드

Node.js는 싱글 스레드, 논 블로킹 방식으로 동작합니다. 싱글 스레드를 이해하기 위해서 프로세스와 스레드가 무엇인지에 대해 살펴보겠습니다.

프로세스

프로세스는 운영체제에 의해 실행되는 독립적인 작업 단위입니다. (비공식적으로는 프로세스는 실행중인 프로그램입니다) 프로세스의 메모리 배치는 일반적으로 스택, 힙, 데이터, 텍스트 영역으로 구분되어있습니다. 각 프로세스는 독립된 주소 공간을 할당받아서 자신만의 자원을 사용합니다. 때문에 한 프로세스에 문제가 생겨도 다른 프로세스에 영향을 미치지 않습니다. 이러한 프로세스의 특성으로 안정성과 격리성이 높지만 프로세스 간의 전환 및 자원 공유에는 오버헤드가 발생합니다.

스레드

스레드는 프로세스 내에서 실행되는 작은 작업의 단위입니다. 하나의 프로세스 안에 여러개의 스레드를 가질 수 있습니다. 스레드는 같은 프로세스에 속한 다른 스레드와 코드, 데이터 섹션, 열린 파일이나 신호와 같은 운영체제 자원들을 공유합니다.

노드 프로세스에는 스레드가 하나뿐일까?

Node.js를 실행하면 프로세스가 하나 생성됩니다. 그리고 그 프로세스에서 스레드들을 생성하는데 하나가 아닌 여러 개의 스레드를 생성합니다. 하지만 그 중에서 우리가 직접 제어할 수 있는 스레드는 하나뿐입니다. 이것이Node.js를 싱글 스레드라고 부르는 이유입니다.

싱글 스레드, 논 블로킹

Node.js는 싱글 스레드 방식입니다. 따라서 Node.js는 동시에 하나의 작업만 처리할 수 있습니다. 하지만 위에서 설명한 논 블로킹 방식으로 인해 비동기 프로그래밍을 가능하게 하고, 동시성을 구현할 수 있습니다.

참고 문서