Skip to main content
Advertisement

Node.js 실전 고수 팁

환경 변수 관리 (dotenv)

dotenv 설치 및 기본 사용

npm install dotenv
# .env 파일 (절대 git에 커밋하지 말 것)
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
JWT_SECRET=my-very-secret-key-change-in-production
REDIS_URL=redis://localhost:6379
API_KEY=sk-1234567890abcdef
// 앱 진입점 최상단에서 로드
require('dotenv').config();

// 또는 경로 지정
require('dotenv').config({ path: '.env.local' });

// 환경 변수 사용
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
const jwtSecret = process.env.JWT_SECRET;

if (!jwtSecret) {
throw new Error('JWT_SECRET 환경 변수가 설정되지 않았습니다');
}

환경별 설정 파일 관리

.env                  # 기본값 (git 커밋 가능, 민감 정보 제외)
.env.local # 로컬 오버라이드 (git 제외)
.env.development # 개발 환경
.env.production # 프로덕션 환경
.env.test # 테스트 환경
.env.example # 예시 파일 (git 커밋, 실제 값 대신 주석)
# .env.example (팀원이 참고하는 템플릿)
PORT=3000
NODE_ENV=development
DATABASE_URL=postgresql://localhost:5432/mydb
JWT_SECRET=change-me-in-production
// config.js — 환경 변수를 중앙 집중식으로 관리
require('dotenv').config();

const config = {
env: process.env.NODE_ENV || 'development',
isDev: process.env.NODE_ENV !== 'production',
port: Number(process.env.PORT) || 3000,

db: {
url: required('DATABASE_URL'),
poolSize: Number(process.env.DB_POOL_SIZE) || 10,
},

jwt: {
secret: required('JWT_SECRET'),
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
},

redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379',
},
};

function required(key) {
const value = process.env[key];
if (!value) {
throw new Error(`필수 환경 변수 누락: ${key}`);
}
return value;
}

module.exports = config;

PM2 프로세스 관리

PM2는 Node.js 프로덕션 프로세스 매니저입니다. 자동 재시작, 로그 관리, 클러스터 모드를 지원합니다.

npm install -g pm2

기본 명령어

# 앱 시작
pm2 start app.js
pm2 start app.js --name "my-api"

# 여러 인스턴스 (클러스터 모드)
pm2 start app.js -i max # CPU 코어 수만큼
pm2 start app.js -i 4 # 4개 인스턴스

# 상태 확인
pm2 status
pm2 list

# 로그 확인
pm2 logs # 모든 로그
pm2 logs my-api # 특정 앱 로그
pm2 logs --lines 100 # 최근 100줄

# 재시작/중지/삭제
pm2 restart my-api
pm2 reload my-api # 무중단 재시작
pm2 stop my-api
pm2 delete my-api

# 모니터링 대시보드
pm2 monit

# 메모리 초과 시 자동 재시작
pm2 start app.js --max-memory-restart 500M

PM2 설정 파일

// ecosystem.config.js
module.exports = {
apps: [
{
name: 'api-server',
script: 'src/index.js',
instances: 'max', // CPU 코어 수만큼
exec_mode: 'cluster', // 클러스터 모드
watch: false, // 프로덕션에서는 false
max_memory_restart: '500M', // 500MB 초과 시 재시작

env: {
NODE_ENV: 'development',
PORT: 3000,
},
env_production: {
NODE_ENV: 'production',
PORT: 8080,
},

log_date_format: 'YYYY-MM-DD HH:mm:ss',
error_file: './logs/err.log',
out_file: './logs/out.log',
merge_logs: true, // 클러스터 로그 합치기

// 자동 재시작 설정
autorestart: true,
restart_delay: 4000, // 재시작 전 4초 대기
max_restarts: 10,
min_uptime: '5s', // 5초 이상 실행 시 정상 시작으로 간주
},
{
name: 'worker',
script: 'src/worker.js',
instances: 2,
exec_mode: 'fork',
cron_restart: '0 3 * * *', // 매일 새벽 3시 재시작
},
],
};
# 설정 파일로 시작
pm2 start ecosystem.config.js
pm2 start ecosystem.config.js --env production

# 시스템 재부팅 시 자동 시작
pm2 startup
pm2 save # 현재 프로세스 목록 저장

클러스터 모드: CPU 코어 활용

Node.js는 단일 스레드이지만, 클러스터 모듈로 여러 CPU 코어를 활용할 수 있습니다.

// cluster.js
const cluster = require('cluster');
const os = require('os');
const http = require('http');

const numCPUs = os.cpus().length;

if (cluster.isPrimary) {
console.log(`마스터 프로세스 PID: ${process.pid}`);
console.log(`CPU 코어: ${numCPUs}개 → 워커 ${numCPUs}개 생성`);

// 워커 생성
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}

// 워커 종료 시 재시작
cluster.on('exit', (worker, code, signal) => {
console.log(`워커 ${worker.process.pid} 종료 (code: ${code}, signal: ${signal})`);
console.log('새 워커 생성 중...');
cluster.fork();
});

// 워커 준비 완료 이벤트
cluster.on('online', (worker) => {
console.log(`워커 ${worker.process.pid} 시작`);
});
} else {
// 워커 프로세스
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
pid: process.pid,
message: `워커 ${process.pid}가 처리`,
}));
});

server.listen(3000, () => {
console.log(`워커 ${process.pid}: 포트 3000 대기 중`);
});
}

무중단 배포 패턴

// cluster-reload.js
const cluster = require('cluster');
const os = require('os');

if (cluster.isPrimary) {
const numCPUs = os.cpus().length;

// 초기 워커 시작
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}

// SIGUSR2 신호로 무중단 재시작
process.on('SIGUSR2', () => {
console.log('무중단 재시작 시작...');
const workers = Object.values(cluster.workers);

// 순차적으로 워커 재시작
function reloadWorker(index) {
if (index >= workers.length) {
console.log('무중단 재시작 완료');
return;
}

const worker = workers[index];
console.log(`워커 ${worker.process.pid} 재시작 중...`);

// 새 워커 시작 후 이전 워커 종료
const newWorker = cluster.fork();
newWorker.once('listening', () => {
worker.disconnect();
worker.once('exit', () => {
reloadWorker(index + 1);
});
});
}

reloadWorker(0);
});
}

// 재시작 명령
// kill -SIGUSR2 $(cat app.pid)

에러 처리: uncaughtException, unhandledRejection

// error-handling.js

// 처리되지 않은 Promise 거부
process.on('unhandledRejection', (reason, promise) => {
console.error('처리되지 않은 Promise 거부:');
console.error(' 이유:', reason);
// 로그 기록 후 프로세스 종료
// Node.js 15+에서는 기본적으로 프로세스 종료
process.exit(1);
});

// 처리되지 않은 예외
process.on('uncaughtException', (err) => {
console.error('처리되지 않은 예외:', err.message);
console.error(err.stack);

// 중요: uncaughtException 후 프로세스 상태는 불안정
// 반드시 프로세스를 종료하고 PM2/k8s가 재시작하게 해야 함
process.exit(1);
});

// 우아한 종료 처리
let isShuttingDown = false;

function gracefulShutdown(signal) {
if (isShuttingDown) return;
isShuttingDown = true;

console.log(`${signal} 수신, 종료 중...`);

// 서버 닫기
server.close(() => {
console.log('HTTP 서버 닫힘');

// DB 연결 닫기
// database.close().then(() => {
// console.log('DB 연결 닫힘');
// process.exit(0);
// });

process.exit(0);
});

// 강제 종료 타임아웃
setTimeout(() => {
console.error('강제 종료 (30초 타임아웃)');
process.exit(1);
}, 30_000).unref(); // unref: 이 타이머가 이벤트 루프를 막지 않도록
}

process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT')); // Ctrl+C

성능 모니터링

Node.js 빌트인 Inspector

# Inspector 활성화
node --inspect app.js
node --inspect=0.0.0.0:9229 app.js # 원격 접속
node --inspect-brk app.js # 첫 줄에서 중지

# Chrome DevTools에서 접속
# chrome://inspect
// 성능 측정
const { performance, PerformanceObserver } = require('perf_hooks');

// 마크 기반 측정
performance.mark('start');
// ... 작업 ...
performance.mark('end');
performance.measure('작업 시간', 'start', 'end');

const obs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
}
});
obs.observe({ entryTypes: ['measure'] });

// v8 힙 스냅샷
const v8 = require('v8');
const stats = v8.getHeapStatistics();
console.log({
heapSizeLimit: `${(stats.heap_size_limit / 1024 / 1024).toFixed(0)} MB`,
totalHeapSize: `${(stats.total_heap_size / 1024 / 1024).toFixed(0)} MB`,
usedHeapSize: `${(stats.used_heap_size / 1024 / 1024).toFixed(0)} MB`,
});

clinic.js 프로파일링

npm install -g clinic

# CPU 프로파일링
clinic doctor -- node app.js

# 이벤트 루프 블로킹 감지
clinic bubbleprof -- node app.js

# 불꽃 그래프
clinic flame -- node app.js

보안 기본

Helmet.js: HTTP 보안 헤더

npm install helmet
const express = require('express');
const helmet = require('helmet');

const app = express();

// 기본 보안 헤더 전체 적용
app.use(helmet());

// 개별 설정
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'cdn.example.com'],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
},
},
hsts: {
maxAge: 31_536_000, // 1년
includeSubDomains: true,
preload: true,
},
frameguard: { action: 'deny' }, // X-Frame-Options: DENY
noSniff: true, // X-Content-Type-Options: nosniff
xssFilter: true, // X-XSS-Protection
}));

Rate Limiting

npm install express-rate-limit
const rateLimit = require('express-rate-limit');

// 전역 제한
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 200,
message: { error: '요청 횟수 초과' },
standardHeaders: true,
legacyHeaders: false,
skip: (req) => req.ip === '127.0.0.1', // 로컬 IP 제외
});

// 로그인 엔드포인트 강화
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1시간
max: 10, // 최대 10회 시도
message: { error: '로그인 시도 횟수 초과, 1시간 후 재시도' },
skipSuccessfulRequests: true, // 성공한 요청은 카운트하지 않음
});

app.use(globalLimiter);
app.post('/auth/login', loginLimiter, loginHandler);

입력 검증 및 SQL 인젝션 방지

// SQL 인젝션 방지: 파라미터화된 쿼리 사용
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// 안전하지 않은 방식 (절대 사용 금지)
// const result = await pool.query(`SELECT * FROM users WHERE id = ${req.params.id}`);

// 안전한 방식: 파라미터화된 쿼리
async function getUser(id) {
const result = await pool.query(
'SELECT id, name, email FROM users WHERE id = $1',
[id] // 자동으로 이스케이프
);
return result.rows[0];
}

// XSS 방지: 출력 시 이스케이프
function escapeHtml(str) {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}

worker_threads: CPU 집약적 작업

const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const os = require('os');

// 피보나치 계산 (CPU 집약적)
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}

if (isMainThread) {
// 메인 스레드
async function runWithWorker(n) {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename, {
workerData: { n }
});
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`워커 종료 코드: ${code}`));
});
});
}

// 여러 작업 병렬 처리
Promise.all([
runWithWorker(40),
runWithWorker(41),
runWithWorker(42),
]).then(results => {
console.log('결과:', results);
});
} else {
// 워커 스레드
const { n } = workerData;
const result = fibonacci(n);
parentPort.postMessage(result);
}

고수를 위한 추가 팁

디버깅 팁

// NODE_DEBUG 환경 변수로 내장 모듈 디버그
// NODE_DEBUG=fs,http node app.js

// 커스텀 디버그 네임스페이스 (debug 패키지)
// npm install debug
const debug = require('debug');
const log = debug('app:server');
const dbLog = debug('app:db');

log('서버 시작'); // DEBUG=app:* node app.js

// util.debuglog (내장)
const util = require('util');
const appLog = util.debuglog('app');
appLog('이 메시지는 NODE_DEBUG=app 설정 시만 출력됨');

메모리 누수 감지

// 메모리 누수 주요 원인
// 1. 전역 변수 누적
// 2. 이벤트 리스너 미제거
// 3. 클로저에 큰 데이터 캡처
// 4. 타이머 미정리

// 이벤트 리스너 누수 예방
const emitter = new EventEmitter();

function onData(data) { /* ... */ }
emitter.on('data', onData);

// 사용 후 반드시 제거
emitter.off('data', onData);

// 또는 once 사용
emitter.once('data', onData);

// 타이머 정리
const timer = setInterval(() => {}, 1000);
// 앱 종료 시
clearInterval(timer);
Advertisement