SHIN
Node.js 실전 팁 20선
20편Node.js 서버가 시간이 지남에 따라 메모리를 계속 먹으면 메모리 누수를 의심해야 합니다.
| 원인 | 설명 |
|---|---|
| 전역 변수 축적 | 배열/맵에 계속 추가, 정리 안 함 |
| 이벤트 리스너 미제거 | 컴포넌트 해제 시 off() 미호출 |
| 클로저 참조 | 오래된 대형 객체를 클로저가 붙잡음 |
| 캐시 무한 성장 | Map/Object에 TTL 없이 계속 저장 |
| Timer 미정리 | setInterval clearInterval 미호출 |
// ❌ 누수 - 캐시 크기 무제한
const cache = new Map();
app.get('/user/:id', async (req, res) => {
if (!cache.has(req.params.id)) {
cache.set(req.params.id, await db.user.findById(req.params.id));
}
res.json(cache.get(req.params.id));
});
// ✅ LRU 캐시로 크기 제한
import LRU from 'lru-cache';
const cache = new LRU({ max: 500, ttl: 1000 * 60 * 5 }); // 최대 500개, 5분 TTL// ❌ 누수 - 요청마다 리스너 추가, 제거 안 함
server.on('connection', (socket) => {
process.on('exit', () => socket.destroy()); // 요청마다 추가됨!
});
// ✅ WeakRef + 명시적 제거
server.on('connection', (socket) => {
const onExit = () => socket.destroy();
process.once('exit', onExit); // once 사용
socket.on('close', () => process.off('exit', onExit)); // 소켓 닫힐 때 제거
});function logMemory(label = '') {
const m = process.memoryUsage();
const fmt = (b) => (b / 1024 / 1024).toFixed(2) + ' MB';
console.log(`[Memory ${label}] heap: ${fmt(m.heapUsed)}/${fmt(m.heapTotal)}, RSS: ${fmt(m.rss)}`);
}
// 주기적 모니터링
setInterval(() => logMemory('periodic'), 30_000);node --inspect app.js
# Chrome에서 chrome://inspect 접속
# Memory 탭 → Take heap snapshot
# 스냅샷 비교로 누수 객체 식별// ❌ Map은 강한 참조 — GC 방해
const metadata = new Map();
function attach(obj) { metadata.set(obj, { ts: Date.now() }); }
// ✅ WeakMap — 객체가 GC되면 자동 정리
const metadata = new WeakMap();
function attach(obj) { metadata.set(obj, { ts: Date.now() }); }// 힙 임계값 초과 시 경고
const HEAP_LIMIT = 512 * 1024 * 1024; // 512 MB
setInterval(() => {
const { heapUsed } = process.memoryUsage();
if (heapUsed > HEAP_LIMIT) {
console.error(`힙 임계값 초과: ${(heapUsed / 1024 / 1024).toFixed(0)} MB`);
// 알림 발송, 재시작 트리거 등
}
}, 10_000);