🟢
JavaScript 런타임 가이드

Node.js 완전 가이드

👥 방문자 수

JavaScript를 서버에서 실행하는 Node.js. npm 생태계, 이벤트 루프, 파일 시스템, HTTP 서버, Express.js까지 한국어로 배우세요.

Node.js 22 LTS npm/pnpm Express.js ES Modules V8 엔진

1. Node.js란?

Node.js는 Chrome의 V8 JavaScript 엔진을 기반으로 서버 사이드에서 JavaScript를 실행하는 런타임입니다. 싱글 스레드, 이벤트 기반, 논블로킹 I/O 모델로 높은 동시성을 처리합니다.

구성 요소역할
V8 엔진JavaScript 코드를 네이티브 머신 코드로 컴파일 (Google Chrome과 동일)
libuv이벤트 루프, 비동기 I/O, 스레드 풀 관리 (C++ 라이브러리)
Node.js APIfs, http, crypto, stream 등 내장 모듈
npm230만+ 패키지를 가진 세계 최대의 패키지 레지스트리
ℹ️
이벤트 루프의 핵심: 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 가이드