Node.js 개요
Node.js란 무엇인가?
Node.js는 브라우저 밖에서 JavaScript를 실행할 수 있게 해주는 런타임 환경입니다. 2009년 Ryan Dahl이 발표한 이후 서버 사이드 JavaScript의 표준으로 자리잡았습니다.
전통적인 웹 서버(Apache, Tomcat)는 요청 하나당 스레드 하나를 점유하는 방식이었습니다. 동시 접속자가 늘어나면 스레드도 선형적으로 늘어나고, 메모리와 CPU가 금방 고갈됩니다. Node.js는 이 문제를 이벤트 드리븐 논블로킹 I/O 모델로 해결했습니다.
V8 + libuv: Node.js의 두 엔진
┌─────────────────────────────────────┐
│ Node.js 런타임 │
│ │
│ ┌──────────┐ ┌───────────────┐ │
│ │ V8 │ │ libuv │ │
│ │ (JS 실행) │ │ (비동기 I/O) │ │
│ └──────────┘ └───────────────┘ │
│ │
│ ┌─────────────────────────────┐ │
│ │ Node.js Core API │ │
│ │ (fs, http, crypto, ...) │ │
│ └─────────────────────────────┘ │
└─────────────────────────────────────┘
V8 엔진
구글이 Chrome을 위해 개발한 고성능 JavaScript 엔진입니다. JIT(Just-In-Time) 컴파일로 JavaScript 코드를 기계어로 변환합니다. Node.js는 이 V8을 내장해 JavaScript를 서버에서 실행합니다.
libuv
C 언어로 작성된 크로스 플랫폼 비동기 I/O 라이브러리입니다. 이벤트 루프, 스레드 풀, 파일 시스템, 네트워킹을 담당합니다. Node.js의 논블로킹 특성은 대부분 libuv 덕분입니다.
이벤트 드리븐 논블로킹 I/O 모델
블로킹 vs 논블로킹
블로킹 방식은 파일을 읽는 동안 해당 스레드 전체가 멈춥니다:
// 블로킹 코드 (동기)
const fs = require('fs');
const data = fs.readFileSync('/large-file.txt', 'utf8'); // 파일 읽는 동안 멈춤
console.log('파일 읽기 완료:', data.length, '바이트');
console.log('이 코드는 위가 끝나야 실행됨');
논블로킹 방식은 I/O 작업을 백그라운드에 맡기고 즉시 다음 코드를 실행합니다:
// 논블로킹 코드 (비동기)
const fs = require('fs');
fs.readFile('/large-file.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log('파일 읽기 완료:', data.length, '바이트');
});
console.log('이 코드가 먼저 실행됨!'); // 파일 읽기 전에 출력
이벤트 루프
Node.js의 핵심입니다. 단일 스레드에서 동시성을 처리하는 메커니즘입니다:
┌──────────────────────────────────┐
│ 이벤트 루프 │
│ │
│ ┌──────────┐ │
│ │ timers │ setTimeout, setInterval
│ └─────┬────┘ │
│ │ │
│ ┌─────▼────────────┐ │
│ │ pending callbacks│ I/O 에러 │
│ └─────┬────────────┘ │
│ │ │
│ ┌─────▼──────┐ │
│ │ poll │ 새 I/O 이벤트 대기 │
│ └─────┬──────┘ │
│ │ │
│ ┌─────▼──────┐ │
│ │ check │ setImmediate │
│ └─────┬──────┘ │
│ │ │
│ ┌─────▼─────────────┐ │
│ │ close callbacks │ │
│ └───────────────────┘ │
└──────────────────────────────────┘
// 이벤트 루프 동작 확인
console.log('1: 동기 코드');
setTimeout(() => console.log('3: setTimeout (0ms)'), 0);
Promise.resolve().then(() => console.log('2: Promise (마이크로태스크)'));
console.log('1-2: 또 동기 코드');
// 출력 순서:
// 1: 동기 코드
// 1-2: 또 동기 코드
// 2: Promise (마이크로태스크)
// 3: setTimeout (0ms)
npm 생태계
npm(Node Package Manager)은 세계 최대 오픈 소스 패키지 저장소입니다. 230만 개 이상의 패키지가 등록되어 있습니다.
package.json 기초
# 새 프로젝트 초기화
mkdir my-project && cd my-project
npm init -y
생성되는 package.json:
{
"name": "my-project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC"
}
패키지 설치
# 일반 의존성 설치
npm install express
# 개발 의존성 설치
npm install --save-dev nodemon
# 전역 설치
npm install -g pm2
# 설치 후 package.json 확인
cat package.json
node_modules 구조
my-project/
├── node_modules/
│ ├── express/
│ ├── body-parser/
│ └── ...
├── package.json
└── package-lock.json
.gitignore에 반드시 추가:
node_modules/
Node.js vs 브라우저 차이
| 항목 | Node.js | 브라우저 |
|---|---|---|
| 전역 객체 | global | window |
| DOM API | 없음 | document, navigator 등 |
| 파일 시스템 | fs 모듈로 접근 가능 | 접근 불가 (보안) |
| 모듈 시스템 | CommonJS(require) + ESM | ESM(import) |
| 실행 환경 | 서버/로컬 머신 | 샌드박스 환경 |
| 프로세스 관리 | process 객체 | 없음 |
// Node.js에서만 동작
const os = require('os');
console.log('플랫폼:', process.platform);
console.log('Node 버전:', process.version);
console.log('CPU 코어:', os.cpus().length);
console.log('메모리:', Math.round(os.totalmem() / 1024 / 1024 / 1024), 'GB');
// 브라우저에서는 존재하지 않는 전역 객체
console.log(typeof window); // undefined (Node.js)
console.log(typeof global); // object (Node.js)
console.log(typeof process); // object (Node.js)
모듈 시스템 차이
// Node.js CommonJS (기본)
const express = require('express');
module.exports = { myFunction };
// Node.js ESM (package.json에 "type": "module" 추가 필요)
import express from 'express';
export { myFunction };
Node.js 버전 관리
Node.js는 짝수 버전(LTS)과 홀수 버전(Current)으로 나뉩니다:
| 버전 | 종류 | 특징 |
|---|---|---|
| 20.x | LTS (Active) | 장기 지원, 프로덕션 권장 |
| 22.x | LTS (Maintenance) | 보안 패치만 |
| 23.x | Current | 최신 기능, 실험적 |
| 24.x | 개발 중 | 미래 버전 |
nvm으로 버전 관리
# nvm 설치 (macOS/Linux)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
# 특정 버전 설치
nvm install 20
nvm install 22
# 버전 전환
nvm use 20
# 기본 버전 설정
nvm alias default 20
# 설치된 버전 목록
nvm ls
# 현재 버전 확인
node --version
npm --version
실전: 첫 Node.js 서버 실행
기본 HTTP 서버
// server.js
const http = require('http');
const hostname = '127.0.0.1';
const port = 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.end('안녕하세요, Node.js 서버입니다!\n');
});
server.listen(port, hostname, () => {
console.log(`서버 실행 중: http://${hostname}:${port}/`);
});
node server.js
# 서버 실행 중: http://127.0.0.1:3000/
curl http://127.0.0.1:3000
# 안녕하세요, Node.js 서버입니다!
JSON API 서버
// api-server.js
const http = require('http');
const users = [
{ id: 1, name: '김철수', role: 'admin' },
{ id: 2, name: '이영희', role: 'user' },
{ id: 3, name: '박지민', role: 'user' },
];
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (req.url === '/users' && req.method === 'GET') {
res.statusCode = 200;
res.end(JSON.stringify({ success: true, data: users }));
} else if (req.url === '/health') {
res.statusCode = 200;
res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() }));
} else {
res.statusCode = 404;
res.end(JSON.stringify({ success: false, message: '404 Not Found' }));
}
});
server.listen(3000, () => {
console.log('API 서버: http://localhost:3000');
console.log('엔드포인트: GET /users, GET /health');
});
REPL 활용
# Node.js REPL 실행
node
# REPL 내부에서
> const arr = [1, 2, 3, 4, 5]
> arr.filter(n => n % 2 === 0)
[ 2, 4 ]
> arr.reduce((acc, n) => acc + n, 0)
15
> .exit
Node.js 주요 활용 분야
언제 Node.js가 빛나는가?
- 실시간 애플리케이션: 채팅, 알림, 게임 서버 — 이벤트 드리븐 모델의 강점
- API 서버: RESTful API, GraphQL — 빠른 JSON 처리
- 마이크로서비스: 경량화, 빠른 시작 시간
- 스트리밍 서비스: 대용량 파일/미디어 스트리밍
- CLI 도구: npm, webpack, ESLint 등 대부분의 JS 도구가 Node.js 기반
언제 다른 선택을 고려해야 하는가?
- CPU 집약적 연산: 이미지/동영상 처리, 암호화, 머신러닝 — Python/Go가 더 적합
- 멀티스레드 필요: 기본적으로 단일 스레드 (worker_threads로 해결 가능)
- 메모리 집약적 작업: V8 힙 메모리 제한 (~1.5GB 기본)
고수 팁
process 객체 활용
// process 정보 출력
console.log('Node 버전:', process.version);
console.log('플랫폼:', process.platform); // win32, linux, darwin
console.log('아키텍처:', process.arch); // x64, arm64
console.log('PID:', process.pid);
console.log('작업 디렉토리:', process.cwd());
console.log('실행 경로:', process.execPath);
// 환경 변수
process.env.NODE_ENV = 'production';
console.log('환경:', process.env.NODE_ENV);
// 프로세스 종료
process.on('exit', (code) => {
console.log(`프로세스 종료 코드: ${code}`);
});
// 정상 종료
process.exit(0);
// 에러 종료
process.exit(1);
성능 측정
// 실행 시간 측정
const { performance } = require('perf_hooks');
const start = performance.now();
// 측정할 작업
let sum = 0;
for (let i = 0; i < 1_000_000; i++) {
sum += i;
}
const end = performance.now();
console.log(`실행 시간: ${(end - start).toFixed(2)}ms`);
console.log(`결과: ${sum}`);
// console.time 방법
console.time('loop');
let total = 0;
for (let i = 0; i < 1_000_000; i++) total += i;
console.timeEnd('loop');
메모리 사용량 모니터링
// 메모리 사용량 확인
const used = process.memoryUsage();
console.log('메모리 사용량:');
for (const [key, value] of Object.entries(used)) {
console.log(` ${key}: ${Math.round(value / 1024 / 1024 * 100) / 100} MB`);
}
// heapUsed: 실제 사용 중인 힙 메모리
// heapTotal: 전체 힙 크기
// rss: 운영체제가 할당한 전체 메모리
// external: C++ 바인딩 메모리