1. Node.js란?
Node.js는 Chrome의 V8 JavaScript 엔진을 기반으로 서버 사이드에서 JavaScript를 실행하는 런타임입니다. 싱글 스레드, 이벤트 기반, 논블로킹 I/O 모델로 높은 동시성을 처리합니다.
| 구성 요소 | 역할 |
|---|---|
| V8 엔진 | JavaScript 코드를 네이티브 머신 코드로 컴파일 (Google Chrome과 동일) |
| libuv | 이벤트 루프, 비동기 I/O, 스레드 풀 관리 (C++ 라이브러리) |
| Node.js API | fs, http, crypto, stream 등 내장 모듈 |
| npm | 230만+ 패키지를 가진 세계 최대의 패키지 레지스트리 |
이벤트 루프의 핵심: Node.js는 싱글 스레드이지만, I/O 작업(파일 읽기, 네트워크 요청)을 OS나 스레드 풀에 위임하고 완료 시 콜백을 실행합니다. 덕분에 I/O 집약적 작업에서 매우 높은 처리량을 달성합니다.
2. 설치 & 버전 관리 (nvm)
nvm(Node Version Manager)을 사용하면 여러 Node.js 버전을 쉽게 전환할 수 있습니다.
# nvm 설치 (macOS/Linux)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
# Windows: nvm-windows 사용
# https://github.com/coreybutler/nvm-windows
# Node.js 22 LTS 설치
nvm install 22
nvm use 22
nvm alias default 22
# 버전 확인
node --version # v22.x.x
npm --version # 10.x.x
# 특정 프로젝트에서 버전 고정 (.nvmrc)
echo "22" > .nvmrc
nvm use # .nvmrc의 버전 자동 사용bash
3. npm & package.json
{
"name": "my-node-app",
"version": "1.0.0",
"description": "Node.js 예제 프로젝트",
"main": "src/index.js", // CommonJS 진입점
"type": "module", // ES Modules 사용 (생략 시 CJS)
"engines": {
"node": ">=22.0.0",
"npm": ">=10.0.0"
},
"scripts": {
"start": "node src/index.js",
"dev": "node --watch src/index.js", // Node 18+: 내장 watch 모드
"build": "tsc",
"test": "node --test",
"lint": "eslint src/",
"format": "prettier --write src/"
},
"dependencies": {
"express": "^4.18.2",
"dotenv": "^16.4.5",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/express": "^4.17.21",
"typescript": "^5.4.5",
"eslint": "^9.0.0"
}
}json
# npm 주요 명령어
npm install # package.json 의존성 설치
npm install express # 패키지 설치 (dependencies)
npm install -D typescript # 개발 의존성 (devDependencies)
npm uninstall express # 패키지 제거
npm update # 모든 패키지 업데이트
npm run dev # scripts.dev 실행
npm audit # 보안 취약점 검사
npm audit fix # 취약점 자동 수정
# pnpm (npm 대비 디스크 절약 + 속도 향상)
npm install -g pnpm
pnpm install
pnpm add express
pnpm add -D typescript
# npx: 설치 없이 패키지 실행
npx create-next-app@latest
npx tsx src/script.ts # TypeScript 파일 직접 실행bash
4. ES Modules vs CommonJS
>// ── ES Modules (ESM) ─── package.json에 "type": "module" 필요
// src/utils/math.js
export function add(a, b) { return a + b }
export function multiply(a, b) { return a * b }
export const PI = 3.14159
// 기본 내보내기
export default class Calculator {
add(a, b) { return a + b }
}
// src/index.js
import Calculator, { add, multiply, PI } from './utils/math.js' // .js 확장자 필수
import path from 'node:path' // 내장 모듈은 node: 접두사 권장
import { readFile } from 'node:fs/promises'
// __dirname 대체 (ESM에서는 __dirname이 없음)
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
// 동적 임포트 (코드 스플리팅, 조건부 로드)
async function loadModule() {
const { default: heavy } = await import('./heavy-module.js')
return heavy
}javascript
>// ── CommonJS (CJS) ─── 기본값 (type: "module" 없을 때)
// src/utils/math.js
function add(a, b) { return a + b }
const PI = 3.14159
module.exports = { add, PI }
// 또는: exports.add = add
// src/index.js
const { add, PI } = require('./utils/math')
const path = require('path')
const fs = require('fs')
// __dirname, __filename 기본 제공
// 혼용 (ESM에서 CJS 패키지 사용) - 대부분의 npm 패키지가 CJS이므로 자동으로 동작
import express from 'express' // CJS 패키지를 ESM에서 임포트 가능javascript
5. 이벤트 루프 & 비동기
Node.js의 이벤트 루프는 6단계로 나뉩니다. 각 단계에서 해당 큐의 콜백을 실행합니다.
>// 이벤트 루프 단계 우선순위 (높은 순)
// process.nextTick → Promise microtask → timers → I/O → setImmediate → close
// 1. process.nextTick - 현재 작업 완료 직후, 다음 루프 시작 전
process.nextTick(() => console.log('1. nextTick'))
// 2. Promise microtask
Promise.resolve().then(() => console.log('2. Promise'))
// 3. setTimeout (timers 단계)
setTimeout(() => console.log('3. setTimeout 0ms'), 0)
// 4. setImmediate (check 단계)
setImmediate(() => console.log('4. setImmediate'))
console.log('0. 동기 코드')
// 출력 순서: 0 → 1 → 2 → 3 → 4javascript
>// async/await + 병렬 처리
async function fetchUserData(userId) {
// 순차 실행 (느림 - 총 2초 소요)
const user = await fetchUser(userId) // 1초
const profile = await fetchProfile(userId) // 1초
// 병렬 실행 (빠름 - 총 1초 소요)
const [user2, profile2] = await Promise.all([
fetchUser(userId),
fetchProfile(userId),
])
// Promise.allSettled: 하나가 실패해도 모두 대기
const results = await Promise.allSettled([
fetchUser(userId),
fetchPosts(userId), // 실패할 수 있음
fetchComments(userId),
])
const fulfilled = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value)
// Promise.race: 가장 먼저 완료되는 것만
const fastest = await Promise.race([
fetchFromCacheServer(),
fetchFromMainServer(),
])
return user2
}
// AbortController로 요청 타임아웃
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController()
const id = setTimeout(() => controller.abort(), timeoutMs)
try {
const response = await fetch(url, { signal: controller.signal })
return await response.json()
} finally {
clearTimeout(id)
}
}javascript
6. 파일 시스템 (fs)
>import { promises as fs } from 'node:fs'
import { createReadStream, createWriteStream } from 'node:fs'
import path from 'node:path'
// ── 파일 읽기/쓰기 ───────────────────────────────────────
// 전체 파일 읽기
const content = await fs.readFile('./data.json', 'utf8')
const data = JSON.parse(content)
// 파일 쓰기
await fs.writeFile('./output.json', JSON.stringify(data, null, 2), 'utf8')
// 파일 추가 (append)
await fs.appendFile('./log.txt', `${new Date().toISOString()} - 로그\n`)
// ── 디렉토리 작업 ──────────────────────────────────────────
// 디렉토리 생성 (중첩 포함)
await fs.mkdir('./dist/assets', { recursive: true })
// 디렉토리 목록
const entries = await fs.readdir('./src', { withFileTypes: true })
for (const entry of entries) {
if (entry.isDirectory()) console.log(`📁 ${entry.name}`)
else console.log(`📄 ${entry.name}`)
}
// ── 파일 정보 & 조작 ────────────────────────────────────────
const stats = await fs.stat('./package.json')
console.log('크기:', stats.size, 'bytes')
console.log('수정일:', stats.mtime)
console.log('파일?', stats.isFile())
await fs.rename('./old.txt', './new.txt') // 이동/이름변경
await fs.copyFile('./src.txt', './dst.txt') // 복사
await fs.unlink('./delete-me.txt') // 삭제
// ── path 모듈 ─────────────────────────────────────────────
const filePath = path.join(__dirname, 'data', 'users.json')
const dir = path.dirname(filePath) // 디렉토리 경로
const base = path.basename(filePath) // 파일명 (확장자 포함)
const ext = path.extname(filePath) // 확장자 (.json)
const name = path.basename(filePath, ext) // 파일명 (확장자 제외)
const abs = path.resolve('./relative/path') // 절대 경로javascript
7. HTTP 서버 내장 모듈
>import { createServer } from 'node:http'
import { URL } from 'node:url'
const PORT = process.env.PORT ?? 3000
const server = createServer(async (req, res) => {
const url = new URL(req.url, `http://localhost:${PORT}`)
const pathname = url.pathname
const method = req.method
// 공통 헤더
res.setHeader('Content-Type', 'application/json')
res.setHeader('X-Powered-By', 'Node.js')
// 요청 바디 읽기
async function readBody() {
return new Promise((resolve, reject) => {
let body = ''
req.on('data', chunk => (body += chunk.toString()))
req.on('end', () => {
try { resolve(JSON.parse(body)) }
catch { resolve({}) }
})
req.on('error', reject)
})
}
// 라우팅
if (pathname === '/api/health' && method === 'GET') {
res.writeHead(200)
res.end(JSON.stringify({ status: 'ok', uptime: process.uptime() }))
} else if (pathname === '/api/users' && method === 'GET') {
const page = Number(url.searchParams.get('page') ?? 1)
res.writeHead(200)
res.end(JSON.stringify({ users: [], page }))
} else if (pathname === '/api/users' && method === 'POST') {
const body = await readBody()
res.writeHead(201)
res.end(JSON.stringify({ id: Date.now(), ...body }))
} else {
res.writeHead(404)
res.end(JSON.stringify({ error: 'Not Found' }))
}
})
server.listen(PORT, () => {
console.log(`서버 실행 중: http://localhost:${PORT}`)
})
// graceful shutdown
process.on('SIGTERM', () => {
server.close(() => process.exit(0))
})javascript
8. Express.js
>import express from 'express'
import cors from 'cors'
import helmet from 'helmet'
const app = express()
// ── 기본 미들웨어 ─────────────────────────────────────────
app.use(helmet()) // 보안 헤더 설정
app.use(cors({ origin: process.env.CORS_ORIGIN ?? '*' }))
app.use(express.json({ limit: '10mb' })) // JSON 바디 파싱
app.use(express.urlencoded({ extended: true })) // 폼 데이터 파싱
// 정적 파일 제공
app.use('/static', express.static('public'))
// ── 라우터 ────────────────────────────────────────────────
const usersRouter = express.Router()
// GET /api/users
usersRouter.get('/', async (req, res, next) => {
try {
const { page = 1, limit = 20, search = '' } = req.query
const users = await UserService.findAll({ page: +page, limit: +limit, search })
res.json({ success: true, data: users })
} catch (err) {
next(err) // 에러 핸들러로 전달
}
})
// GET /api/users/:id
usersRouter.get('/:id', async (req, res, next) => {
try {
const user = await UserService.findById(req.params.id)
if (!user) return res.status(404).json({ success: false, error: '사용자 없음' })
res.json({ success: true, data: user })
} catch (err) {
next(err)
}
})
// POST /api/users
usersRouter.post('/', async (req, res, next) => {
try {
const { name, email } = req.body
if (!name || !email) {
return res.status(422).json({ error: 'name과 email이 필요합니다' })
}
const user = await UserService.create({ name, email })
res.status(201).json({ success: true, data: user })
} catch (err) {
next(err)
}
})
app.use('/api/users', usersRouter)
// ── 에러 핸들러 (4개 파라미터 필수) ──────────────────────────
app.use((err, req, res, next) => {
console.error(err.stack)
const statusCode = err.status ?? err.statusCode ?? 500
res.status(statusCode).json({
success: false,
error: statusCode < 500 ? err.message : '서버 오류가 발생했습니다',
})
})
app.listen(3000, () => console.log('http://localhost:3000'))javascript
9. 환경 변수 & 설정
>// .env
PORT=3000
NODE_ENV=development
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=super-secret-key-change-in-production
CORS_ORIGIN=http://localhost:5173bash
>// Node.js 20.6+ 내장 --env-file 지원
// node --env-file=.env src/index.js
// 또는 dotenv 패키지
import 'dotenv/config' // 파일 상단에 한 번만
// Zod로 환경 변수 타입 검증
import { z } from 'zod'
const envSchema = z.object({
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(['development', 'test', 'production']).default('development'),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
CORS_ORIGIN: z.string().default('*'),
})
// 서버 시작 시 검증 실패하면 즉시 종료
export const env = (() => {
const result = envSchema.safeParse(process.env)
if (!result.success) {
console.error('환경 변수 오류:', result.error.format())
process.exit(1)
}
return result.data
})()javascript
10. 스트림 & 버퍼
스트림은 대용량 데이터를 메모리에 전부 올리지 않고 청크 단위로 처리합니다. 파일 전송, 데이터 변환에 필수입니다.
>import { createReadStream, createWriteStream } from 'node:fs'
import { pipeline, Transform } from 'node:stream'
import { promisify } from 'node:util'
import { createGzip } from 'node:zlib'
import { createInterface } from 'node:readline'
const pipelineAsync = promisify(pipeline)
// ── 파일 스트림으로 대용량 파일 복사 ────────────────────────
// readFile 대신 스트림 사용 → 메모리 사용량 O(1)
await pipelineAsync(
createReadStream('./large-input.csv'),
createGzip(), // 압축 변환 스트림
createWriteStream('./output.csv.gz')
)
// ── 커스텀 Transform 스트림 ──────────────────────────────────
class CSVToJSONTransform extends Transform {
#headers = []
#isFirst = true
_transform(chunk, encoding, callback) {
const lines = chunk.toString().split('\n').filter(Boolean)
for (const line of lines) {
const values = line.split(',')
if (this.#isFirst) {
this.#headers = values
this.#isFirst = false
continue
}
const obj = Object.fromEntries(
this.#headers.map((h, i) => [h.trim(), values[i]?.trim()])
)
this.push(JSON.stringify(obj) + '\n')
}
callback()
}
}
// ── readline: 줄 단위 파일 처리 ──────────────────────────────
const rl = createInterface({
input: createReadStream('./data.txt'),
crlfDelay: Infinity,
})
let lineCount = 0
for await (const line of rl) {
lineCount++
if (line.startsWith('#')) continue // 주석 건너뜀
console.log(`Line ${lineCount}: ${line}`)
}javascript
11. 테스트 (Node Test Runner)
Node.js 18부터 내장 테스트 러너(node:test)가 제공됩니다. Jest 없이 바로 테스트를 작성할 수 있습니다.
>// test/math.test.js
import { describe, it, before, after, beforeEach, mock } from 'node:test'
import assert from 'node:assert/strict'
import { add, multiply, divide } from '../src/utils/math.js'
describe('Math Utils', () => {
describe('add()', () => {
it('두 양수를 더한다', () => {
assert.equal(add(2, 3), 5)
})
it('음수와 양수를 더한다', () => {
assert.equal(add(-1, 1), 0)
})
})
describe('divide()', () => {
it('0으로 나누면 에러를 던진다', () => {
assert.throws(
() => divide(10, 0),
{ message: '0으로 나눌 수 없습니다' }
)
})
})
})
// 비동기 테스트
describe('UserService', () => {
let mockDb
before(async () => {
mockDb = await createTestDatabase()
})
after(async () => {
await mockDb.destroy()
})
it('이메일로 사용자를 찾는다', async (t) => {
// 내장 mock
const fetchMock = mock.fn(async () => ({ id: 1, email: 'test@example.com' }))
t.mock.method(UserService, 'findByEmail', fetchMock)
const user = await UserService.findByEmail('test@example.com')
assert.equal(user.email, 'test@example.com')
assert.equal(fetchMock.mock.callCount(), 1)
})
})javascript
># 테스트 실행
node --test # test/ 폴더 전체 실행
node --test test/math.test.js # 특정 파일
node --test --watch # watch 모드 (변경 시 자동 재실행)
node --test --test-reporter=tap # TAP 포맷 출력
node --test --coverage # 코드 커버리지 (Node.js 22+)bash
12. 다음 단계
Node.js 심화 로드맵
• Fastify: Express보다 3배 빠른 웹 프레임워크, JSON Schema 기반 검증
• NestJS: Angular 스타일의 TypeScript 기반 엔터프라이즈 프레임워크
• Prisma: 타입 안전한 ORM, 자동 마이그레이션, DB 시각화
• Bun: Zig로 작성된 초고속 JS 런타임 (Node.js 대체제)
• Worker Threads: CPU 집약적 작업을 멀티 스레드로 처리
• Cluster 모드: 멀티 코어 CPU 활용으로 처리량 향상
연계 가이드: JavaScript 가이드 · TypeScript 가이드
• Fastify: Express보다 3배 빠른 웹 프레임워크, JSON Schema 기반 검증
• NestJS: Angular 스타일의 TypeScript 기반 엔터프라이즈 프레임워크
• Prisma: 타입 안전한 ORM, 자동 마이그레이션, DB 시각화
• Bun: Zig로 작성된 초고속 JS 런타임 (Node.js 대체제)
• Worker Threads: CPU 집약적 작업을 멀티 스레드로 처리
• Cluster 모드: 멀티 코어 CPU 활용으로 처리량 향상
연계 가이드: JavaScript 가이드 · TypeScript 가이드