[Node.js] Node.js 개요
개요
Node.js는 Chrome V8 엔진으로 빌드된 Javascript 런타임으로, 기존의 자바스크립트가 웹 브라우저 기반으로만 작동하였다면 이를 컴퓨터 자체에서도 동작할 수 있도록 하는 크로스 플랫폼 자바스크립트 런타임 환경이다.
Node.js라는 플랫폼을 통해 이제 개발자는 기존에 웹 브라우저에서만 동작하며 웹 사이트를 동적으로 만드는 작업에 제한되었던 것과 달리 자바, 파이썬과 같은 프로그래밍 언어로서의 자바스크립트를 사용할 수 있게 되었다. 즉, 독립적인 애플리케이션 제작이 가능해졌다.
이로 인해 이제 자바스크립트로도 서버 사이드 작업을 할 수 있게 되었다. DB 연동, RESTFul API 제작 등이 그 예이다.
Node.js의 특징 중 하나는 이벤트 기반 및 논블로킹 I/O 기반을 통해 여러 요청들을 병렬적으로 처리할 수 있다는 것이다. 본래 자바스크립트는 싱글 스레드 기반이기에 한 번에 단 한 개의 요청(또는 작업)만을 처리할 수 있다. 이러한 한계점을 극복하기 위해 Node.js에서는 이벤트 루프 및 논블로킹 모델을 기반으로 한다.
동기, 비동기는 둘 이상의 작업이 직렬적으로 순차적으로 진행되는 방식인지, 병렬적으로 동시에 진행되는 방식인지, 즉 작업의 흐름에 대한 관점이다. 블로킹, 논블로킹은 각각 현재 작업을 차단, 대기하느냐 아니냐에 대한 관점이다. 이렇게 보면 블로킹, 논블로킹은 동기, 비동기와 같은 개념으로 보인다. 동기적인 방식이 진행되려면 하나의 작업을 먼저 수행해야하기에 그 다음 작업의 진행을 차단해야 한다. 반대로 비동기적 방식이 진행되려면 둘 이상의 작업이 서로에 의해 차단되지 않아야 한다.
일례로, 자바스크립트에 존재하는 setTimeout() 함수는 비동기 함수이지만 동시에 논블로킹 함수이기도 한다.
하지만 엄밀한 의미에서는 동기, 비동기와 블로킹, 논블로킹은 조금 다른 개념이다. 방금 설명했듯, 동기, 비동기는 작업의 흐름 순서에 초점을 두며, 블로킹, 논블로킹은 작업의 흐름을 차단(대기)시키냐 아니냐의 관점으로 바라보기 때문이다.
이러한 엄밀한 의미에서의 차이로 인해 사실 동기-비동기와 블로킹-논 블로킹의 조합이 가능하여 4가지의 조합이 가능하다. 동기 - 블로킹, 비동기 - 블로킹, 동기 - 논블로킹, 비동기 - 논블로킹 이렇게 가능하다. 그리고 이러한 조합에 따라 프로그램의 성능 및 효율성에 영향을 끼친다.
일반적인 의미에서는 이렇게 4가지 조합이 가능하지만 Node.js에서는 딱 두 가지 조합만으로 구성되어 있다고 한다. 비동기 작업 시에는 비동기 - 논 블로킹 방식을 사용하고, 순차적 작업 흐름이 필요한 순간에는 동기 - 블로킹 방식을 사용한다고 한다.
위에서 언급한 내용과 더불어 여기서 언급하지 않은 Node.js의 특징까지 모두 정리하면 다음과 같다.
Node.js의 특징
- Chrome V8 엔진으로 빌드된 크로스 플랫폼 자바스크립트 런타임 환경으로, 자바스크립트를 서버에서도 사용 가능하게 하며, 독립적인 애플리케이션 제작을 가능케 한다.
- 이벤트 기반과 논블로킹 I/O에 기반하기에 본래 싱글 스레드인 자바스크립트의 단점을 보완하여 작업(또는 요청)들을 병렬로 처리해 대규모 트래픽을 효율적으로 처리할 수 있다.
- 클라이언트와 서버 모두 자바스크립트라는 단일 언어를 사용하므로 개발 효율성이 높아지며, 언어의 일관성을 확보할 수 있다.
- Node.js는 NPM과 같은 모듈 시스템을 이용하여 필요한 모듈만 추가하여 확장할 수 있어 유연한 확장성을 가진다.
- 웹소켓도 지원하기에 채팅, 주식, 게임과 같은 실시간 애플리케이션 제작에도 유리하다.
- 서버로 사용 시 경량 서버 역할을 하기에 설정 및 배포 과정이 간단하다.
- Node.js로 제작한 애플리케이션은 여러 운영체제에서도 실행 가능하다. 즉, 크로스 플랫폼이다.
- 비동기식 파일 입출력, DB 연동 등의 작업도 가능하다.
- DB 연동 작업 및 외부 API와의 통신 작업을 할 수 있어 미들웨어 서버의 역할도 할 수 있다.
ES Module (ESM) VS CommonJS (CJS)
Node.js의 등장으로 인해 이제 자바스크립트도 모듈화가 가능해졌다. 그런데 자바스크립트에서는 모듈 시스템이 두 가지이다. ES Module (ESM)과 CommonJS (CJS)이다. 많은 패키지들은 CJS를 기반으로 많이 작성되었다고 하나, 현재는 ESM 방식이 주로 사용된다고 한다.
ESM 방식으로 작성된 모듈은 주로 다음의 형태를 띤다.
// 의사코드
import MyModule from './myModule.mjs';
function myFunc() {...}
function otherFunc() {...}
export { myFunc, otherFunc };
반면, CJS 방식으로 작성된 모듈은 다음의 형태를 띤다.
// require를 이용하여 ./myMath 모듈 import
const { odd, even } = require('./myMath');
// 이 모듈에서만 사용 가능한 지역 함수.
function chkOddEven(arg) {
if (arg % 2) {
return odd;
}
return even;
}
// 위 함수를 외부 모듈에서도 사용하고자 한다면 다음의 코드 입력
module.exports = chkOddEven;
즉, 모듈을 import, export하는 방식이 다른 것을 볼 수 있다. 그렇기에 CJS 방식으로 작성된 모듈과 ESM 방식으로 작성된 모듈끼리의 호환성이 없다.
이 외에도 ESM과 CJS 간의 여러 차이점이 존재한다.
- ESM 방식은 import 구문을 코드 중간에 호출할 수 없고, 반드시 모듈 최상위에 호출해야 한다. 반면 CJS 방식은 import문에 해당하는 require문을 코드 중간에 사용할 수 있다.
- CJS 방식의 모듈 import 시스템은 동적인 특성을 가진다. 즉, 코드 중간에 require 문을 만나면 그때그때 바로 다른 모듈을 import하는 시스템이다.
- 반면, ESM은 반드시 모듈 최상위에서 다른 모듈을 import 해야하기에 정적인 특성을 가진다. 즉, 필요한 모듈이 있으면 처음부터 한꺼번에 import 해와야 하는 시스템이다.
- 이러한 차이가 존재하는 이유는, ESM에서는 모듈 종속성, 즉 모듈들 간의 import로 인한 종속성 관계를 모두 판단한 후에 코드를 실행하는 반면, CJS는 코드 상에서 require문을 만나면 모듈들 간의 종속성이 어떻든 상관하지 않고 그 즉시 해당 모듈을 불러오는 시스템이기 때문이다. 쉽게 말하면 ESM은 계획적이고, CJS는 즉흥적이라고 말할 수 있겠다.
ESM에서는 모듈간 종속성을 확인하는 방법으로, 각각의 모듈들을 tree를 구성하는 하나의 Node로 보고, 이들을 재귀적 깊이 우선 탐색(DFS, Depth First Search)을 이용하여 모듈들의 종속성 관계를 트리로 구조화하는 방식을 취한다. 조금 더 자세히 말하면 다음의 과정을 거친다.
- 생성(parsing) : 모듈들 간의 import문을 재귀적으로 검색하여 모든 모듈들의 내용을 메모리에 적재(loading)한다. 아직 모듈 간의 종속성 관계를 파악하지는 않은 상태.
- 인스턴스화 : 바로 이전의 “생성” 과정에서 얻은 모듈들의 트리 구조를 참조하여 각각의 모듈 내에서 export된 객체 참조를 메모리에 유지, 종속성 관계를 추적한다. 이 과정에서 export 관계를 맵으로 만든다. (아마도 그래프 이론에서 인접리스트 (Adjacent List)로 각 노드들 간의 참조 관계 정보를 구축하는 것으로 추측된다)
- 평가 : 모듈 간 종속성을 맵으로 만든 것일 뿐, 실질적 코드는 아직 메모리 상에 존재하지 않는다. 따라서 이전에 만들어진 모듈 종속성 트리 그래프를 토대로 DFS 후위 깊이 우선 탐색 방식을 따라 트리의 leaf node에서 root node로 아래에서 위로 올라가는 방식으로 코드들을 평가한다. 이 과정에서 export된 모든 값들의 초기화가 보장된다.
이러한 ESM의 모듈 종속성 평가 방식에 의해, CJS와의 차이가 또 발생한다.
- ESM은 모든 모듈 간의 종속성 분석이 완료되어야 코드가 진행되기에 CJS 방식에 비해선 실행 속도가 느리다고 볼 수 있다. 이러한 점에서는 ESM이 불리하다.
- 그러나 CJS는 require문을 만나면 바로 외부 모듈을 import 하는 방식이기에, 두 모듈이 상호 순환 참조 관계에 놓여있으면 이 상호 순환 참조 관계임을 제대로 파악하지 못한다. a라는 모듈이 b라는 모듈을 import하고, b라는 모듈이 a라는 모듈을 import하고 있을 경우, 코드 순서를 따라 a가 b를 먼저 import하는 방식일 경우 a에는 b의 정보가 있으나, b에는 a의 정보가 없는 문제가 발생한다. 이는 ESM처럼 미리 모듈 간 종속성을 파악하지 않고 즉각적으로 모듈을 import하는 CJS의 시스템적 문제 때문이다.
- 모듈 종속성 관계를 미리 파악하는 ESM의 특성에 의해, 사용되지 않는 코드는 제거하는 코드 최적화가 가능해진다. 이렇게 사용되지 않는 코드의 제거를 통해 코드를 최적화하는 것을 tree shaking이라 한다. 마치 나무를 흔들어 죽은 나뭇잎을 떨어트리는 것과 같은 이치이다. (이러한 tree shaking이란 용어는 주로 자바스크립트 진영에서 자주 사용되는 것으로 보인다)
앞으로 Node.js과 관련된 내용을 다룰 때 필자는 되도록 ESM 방식으로 코드를 작성할 것이니 참고바란다.
Node.js 살펴보기
Java에서는 외부 라이브러리를 사용하고자 한다면 라이브러리 제작자가 제공하는 Jar 파일을 가져와 추가하거나, maven, gradle과 같은 프로젝트 관리 도구를 사용할 경우 의존성을 각각 pom.xml, build.gradle 파일에 추가하는 방식으로 외부 패키지를 사용한다.
Node.js에서도 외부 패키지를 가져와 사용할 수 있는데, 이 때 npm(Node Package Manage)을 주로 사용한다.
외부 라이브러리 정보를 package.json에 등록하고, 터미널 창에서 npm i
를 입력하여 실질적으로 필요한 패키지들을 로컬 컴퓨터에 다운로드하는 방식이다. 그래서 프로젝트 폴더 생성하고 나서 다음의 명령어를 입력하면 이후에 동적으로 외부 패키지를 손쉽게 추가할 수 있다.
npm init -y
여기서 -y
는 설치 과정에서 터미널창에서 질문을 하는데, 모두 yes로 자동 설정하여 번거로움을 없애고자 할 때 사용된다.
PS C:\Work\NodeJsStudyWorkSpace\first-nodejs> npm init -y
Wrote to C:\Work\NodeJsStudyWorkSpace\first-nodejs\package.json:
{
"name": "first-nodejs",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}
해당 명령어를 사용하면 프로젝트 폴더 밑에 package.json이 새로 생성된다.
그리고 package.json 내용은 다음과 같이 초기화되어 있다.
{
"name": "first-nodejs", // 프로젝트 폴더명
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": ""
}
package.json
이제 정말로 외부 라이브러리를 설치할 수 있는지 살펴보자. 아래와 같은 명령어를 통해 axios를 사용해보도록 하겠다.
npm i axios
PS C:\Work\NodeJsStudyWorkSpace\first-nodejs> npm i axios
added 9 packages, and audited 10 packages in 778ms
1 package is looking for funding
run `npm fund` for details
found 0 vulnerabilities
명령어 실행 결과
{
"name": "first-nodejs",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
// 추가됨
"dependencies": {
"axios": "^1.7.9"
}
}
package.json
위와 같이 “dependencies” 프로퍼티가 새로 추가되었고, 그 안의 value로 “axios” 정보가 추가됨을 알 수 있다. 앞으로 npm i 명령어를 통해 다른 외부 라이브러리를 설치할 때에도 해당 라이브러리 정보가 package.json에 기록된다. 이후에 의존성 패키지인 node_modules를 삭제하더라도 이 package.json만 잘 보존되어 있다면 나중에 다른 곳에서도 npm i
만 치면 의존성들을 그대로 사용할 수 있게 된다.
이제 외부 의존성인 axios를 사용해보자. index.mjs 파일을 생성하여 다음의 코드를 작성하였다.
import axios from "axios";
const fetchData = async () => {
try {
const response = await axios(`https://jsonplaceholder.typicode.com/posts/${1}`);
// 가져온 데이터 출력
console.log(response.data);
} catch(error) {
console.log("데이터 가져오는 도중 에러 발생");
console.log('===== 에러 전문 =====');
console.log(error);
console.log('===== 에러 전문 끝 =====');
}
}
fetchData();
index.mjs
콘솔창에서 모듈을 실행하려면 node 파일명
명령어를 입력하면 된다.
PS C:\Work\NodeJsStudyWorkSpace\first-nodejs> node index.mjs
{
userId: 1,
id: 1,
title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
body: 'quia et suscipit\n' +
'suscipit recusandae consequuntur expedita et cum\n' +
'reprehenderit molestiae ut ut quas totam\n' +
'nostrum rerum est autem sunt rem eveniet architecto'
}
index.mjs 실행 결과
외부 데이터를 잘 가져오는 것을 확인할 수 있다.
참고로 ESM 모듈은 .mjs
확장자를 가진다. CJS 모듈은 .cjs
모듈을 가지나 .js
확장자도 CJS 모듈로 인식된다. 이 상태에서 ESM 모듈을 명령어 창에서 실행하려면 node index
가 아니라 node index.mjs
와 같이 작성해야 에러 없이 실행된다. 만약 확장자를 생략하면 다음의 에러를 볼 수 있다.
> node index
node:internal/modules/cjs/loader:1252
throw err;
^
Error: Cannot find module 'C:\Work\NodeJsStudyWorkSpace\first-nodejs\index'
at Function._resolveFilename (node:internal/modules/cjs/loader:1249:15)
at Function._load (node:internal/modules/cjs/loader:1075:27)
at TracingChannel.traceSync (node:diagnostics_channel:315:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:218:24)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:170:5)
at node:internal/main/run_main_module:36:49 {
code: 'MODULE_NOT_FOUND',
requireStack: []
}
만약 ESM 모듈의 확장자를 .js
로 바꾸고자 한다면 package.json에 "type": "module"
프로퍼티를 추가하면 된다. 단 이 경우 CJS를 사용하고자 한다면 해당 파일의 확장자명은 반드시 .cjs
라고 명시해야만 한다. (그런데 하나의 프로젝트 폴더 안에 ESM과 CJS를 혼용하여 사용할 일이 있을까? 이 둘은 어차피 호환도 안되는 데 말이다)
package.json에 다음과 같이 "type": "module"
를 추가해보았다.
{
"name": "first-nodejs",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"axios": "^1.7.9"
},
"type": "module" // 추가
}
package.json
그 후 index.mjs
를 index.js
로 확장자를 바꾼 후 콘솔창에 node index
라고만 쳐도 다음과 같이 결과가 잘 나온다.
> node index
{
userId: 1,
id: 1,
title: 'sunt aut facere repellat provident occaecati excepturi optio reprehenderit',
body: 'quia et suscipit\n' +
'suscipit recusandae consequuntur expedita et cum\n' +
'reprehenderit molestiae ut ut quas totam\n' +
'nostrum rerum est autem sunt rem eveniet architecto'
}
References
[1] 에이콘아카데미(강남) 강의
[2] 이고잉, “생활코딩! Node.js 노드제이에스 프로그래밍”, (위키북스, 2022)
[3] 빠르게 배우는 Node.js와 NPM 설치부터 개념잡기
[4] Node.js — Introduction to Node.js
[5] 동기-비동기, 블로킹-논블로킹
👩💻 완벽히 이해하는 동기/비동기 & 블로킹/논블로킹
[6] 박영권 - “데이터 과학자 + 프로그래머 세상”
[7] ESM VS CJS. 그리고 모듈 종속성 분석 시스템의 차이
[8] ESM VS CJS
자바스크립트의 표준 정의 : CommonJS vs ES Modules
[9] Tree shaking
Tree shaking - MDN Web Docs Glossary: Definitions of Web-related terms | MDN
[10] Tree shaking
This content is licensed under
CC BY-NC 4.0
댓글남기기