자바스크립트와 Node.js를 처음에 배웠을 때에는 두개의 차이점을 이해하지 못해서 많은 어려움을 느꼈던 적이 있습니다.
Node.js는 다른 런타임들과 달리 특이한 점이 많은 것 같습니다.
Node.js에 대해서 깊게 탐구해보겠습니다.
javascript를 브라우저 밖에서도 실행할 수 있도록 하는 javascript 런타임이다.
런타임
이란 특정 언어로 만든 프로그램을 실행할 수 있는 환경. 컴파일러, 인터프리터, 코드 실행 엔진 , 스레드풀 등으로 구성되어 있다. 따라서 Node.js는 V8엔진, libuv 스레드 풀로 구성되어 있기 때문에 런타임이라고 부를 수 있다.
우리가 작성한 코드(자바스크립트)는 런타임 코드가 아니다. 단지 "런타임"에 의해 실행될 코드이다.
Node.js는 크게 내장 라이브러리, v8엔진, libuv로 구성되어 있다.
구글에서 개발된 자바스크립트/웹어셈블리 엔진이며, C++로 만들어졌다.
V8은 자바스크립트를 바이트코드로 컴파일하거나 인터프리트하는 대신 실행하기 전 직접적인 기계어로 컴파일하여 성능을 향상시킨다. 추가적인 속도향상을 위해 인라인 캐싱과 같은 최적화 기법을 적용한다.
역할
1. 자바스크립트 코드를 컴파일하고 실행한다.
2. 변수 할당 및 저장을 위한 메모리 힙 제공
3. 콜스택 제공
4. Garbage collector
5. JavaScript 언어로 코드를 작성하기위한 데이터 유형, 연산자, 객체 및 함수를 제공
6. 이벤트 루프 제공 (외부 루프 사용 가능)
14.x LTS 버전에서 7->8.4 로 업그레이드되어 자바스크립트 퍼포먼스 향상 및 기본 환경에서 옵셔널 체이닝 등의 문법 사용 가능해짐
이벤트 루프는 블로킹 IO작업을 만나면 시스템 커널로 작업을 오프로드 시킴으로써 논블로킹 IO작업을 할 수 있도록 만들어주는 녀석이다. Node.js의 가장 큰 특징인 비동기 IO를 실현 가능하게 해준다.
이벤트루프는 여러개의 phase
를 가지고 있고, pahse는 큐를 가지고 있다. phase는 자신의 큐를 관리하고, 큐들은 FIFO방식으로 CPU가 할당(=이벤트루프가 해당 phase를 호출할때)될 때 콜백 함수를 처리한다. 이벤트 루프는 RR방식으로 노드 프로세스가 종료될 때 까지 규칙에 따라 여러 phase를 순회한다.
🤷♂️그럼 이벤트 루프에서 콜백함수를 실행하는건가요?
여기서 주의할 점은 이벤트 루프는 코드를 실행시키는 녀석이 아니다. 이벤트 루프는 단순히 phase를 순회하는 "루프"의 역할을 하고, V8엔진이 실제로 코드를 컴파일하고 실행하는 것이다.
큐에 콜백 함수가 들어오게 되면 이벤트 루프가 큐에 있는 콜백 함수를 꺼내 V8엔진에게 보낸다. 엔진은 콜스택을 사용해 코드를 컴파일 및 실행해 함수를 할당하고, 메모리 힙을 사용해 변수를 할당하고, GC를 이용해 실행이 완료된 변수와 함수 참조를 제거하는 것이다.
🤷♀️이벤트루프는 어디에 존재하는 것일까
여기서 나는 이 이벤트 루프가 대체 어디에 존재하는지가 궁금했다. v8에도 이벤트 루프가 있다고 하고 libuv에도 있다고 하고...했갈렸다.
결론은 Node.js의 경우 libuv를 이용해 이벤트루프를 구현하고, 크롬 브라우저의 경우 libevent를 이용한다고 한다.
V8에서는 외부 이벤트 루프를 플러그인 할 수 있어서 다른 이벤트 루프여도 같은 V8엔진에서 작동할 수 있다고 한다. 그리고 둘(브라우저와 노드)의 이벤트 루프가 다른 이유는, 노드의 경우 브라우저와 달리 서버에서 IO작업 등을 처리해야 하는 과정이 필요해서 다른 이벤트 루프를 사용한다고 한다.
libuv는 fs 모듈과 같은 IO등으로 인해 오래 걸리는 작업을 실행하기 위해 Node.js 런타임에 스레드 풀
을 제공하는 라이브러리 이다. 기본적으로 4개의 스레드를 제공하고, 설정으로 풀 크기를 변경할 수 있다. process.env.UV_THREADPOOL_SIZE
네트워크, file, DB I/O와 같은 작업들은 보통 "비싼"작업이라고 한다. 외부와 통신을 기다려야 하는 시간이 필요하기도 하고, 디스크 읽기 시간 등이 걸리기 때문이다. 이런 무거운 작업들을 Node에서는 libuv의 스레드 풀에게 위임하고 다른 작업들을 처리하게 된다. 그리고 libuv에서 작업을 끝내게 되면, 콜백을 이벤트 루프의 큐에 다시 넣어 결과를 처리한다.
한 블로그에서 각 구성의 역할에 대해 잘 비유한 내용이 있어 공유한다.
우리가 음식을 시키기 위해서 (코드를 실행하기 위해서) 웨이터(V8)에게 주문을 받아달라고 한다(코드를 실행).
그리고 주문한 음식이 조리가 오래 걸리는 음식(IO작업이 필요한 테스크)이라고 가정해보자. 웨이터는 셰프(스레드 풀)에게 주문서를 넘겨주고 셰프가 요리(테스크 실행)를 하게 된다. 요리가 완료되면, 셰프는 웨이터(이벤트 루프)에게 이를 알리고, 웨이터(v8)가 손님에게 음식을 가져다 준다.
참고