-
Notifications
You must be signed in to change notification settings - Fork 0
Heap Snapshot
라몽이 edited this page Sep 19, 2025
·
1 revision
실무에서 메모리 누수(또는 과도한 메모리 사용)를 진단하고 해결하기 위한 단계별 튜토리얼입니다.
- 대상: Frontend 개발자(React/Next.js) + Node.js 백엔드 개발자
- 목적: DevTools로 힙 스냅샷을 찍고
증가/누수를 확인하는 법, 결과 해석법, 자주 나오는 누수 패턴과 해결법, 자동화 스크립트 예시 - 전제: Chrome(또는 Chromium 계열) 최신 버전, Node.js(테스트용), 로컬 개발서버 접근 가능
- 기준(Baseline) 힙 스냅샷 찍기
- 문제 재현 시나리오를 반복(수동 또는 자동)
- 두 번째 힙 스냅샷 찍기
- 스냅샷 비교 → 증가한 타입/객체 찾기
- Retainers(참조 경로)를 따라 원인 코드 탐색
- 코드 수정 → 다시 재현 + 스냅샷 비교로 검증
- Memory 탭 접속 →
Heap snapshot선택 →Take snapshot클릭 - 왼쪽 사이드바에 "Snapshot 1"처럼 저장됨
- 권장 순서: Snapshot(기준) → reproduce scenario → Snapshot(이후)
- 왼쪽에서 두 개 이상의 스냅샷 선택(또는 오른쪽 클릭하여 비교)
-
Comparison뷰에서 count(객체 수), shallow size, retained size 증가를 확인
Tip:
retained size(해당 객체 때문에 살아있는 전체 메모리)를 우선 확인합니다. shallow size는 혼동을 줄 수 있음.
- Memory 탭 →
Allocation instrumentation on timeline선택 → Record - 반복 동작(예: modal 열기/닫기 50회) 수행 → Stop
- 타임라인에서 특정 구간을 확대하면 어떤 함수에서 할당이 일어났는지 확인 가능
사용처: 반복 액션에서 메모리가 지속적으로 증가하는 패턴(누수)을 찾을 때 매우 유용
- Summary: 타입별 객체 개수와 메모리 점유
- Comparison: 두 스냅샷의 변화 (증가한 항목부터 조사)
- Containment: 객체가 어떻게 포함되어 있는지 트리로 확인
- Retainers: 특정 객체를 잡고 있는 참조 체인을 보여줌(누수가 풀리지 않는 이유 추적)
-
(Detached): DOM 노드가
Detached로 남아있으면 DOM 누수 의심
문제(React/vanilla)
// vanilla 예시 (누수)
function showModal() {
const btn = document.getElementById('ok');
btn.addEventListener('click', () => console.log('ok'));
// 닫을 때 removeEventListener 하지 않음
}수정
function showModal() {
const btn = document.getElementById('ok');
function onClick() { console.log('ok'); }
btn.addEventListener('click', onClick);
return () => btn.removeEventListener('click', onClick);
}React에서는 useEffect의 cleanup을 항상 작성하세요.
문제
function startPolling () {
setInterval(doWork, 1000); // 멈추는 로직 없음
}수정
let id;
function startPolling() { id = setInterval(doWork, 1000); }
function stopPolling() { clearInterval(id); id = null; }React: useEffect(() => { const id = setInterval(...); return () => clearInterval(id); }, []);
문제
function mk() {
const big = new Array(1e6).fill('x');
return function () { /* 참조 유지 */ };
}
const fn = mk(); // big가 GC되지 않음수정: 큰 데이터 사용 후 big = null 또는 scope 밖으로 빼서 참조 끊기.
- Heap snapshot의
Detached항목을 찾으세요 - 원인: DOM을 제거했지만 배열/캐시/전역변수에서 참조 유지
- 해결: 해당 캐시/변수에서 참조 제거
-
Memory탭의 Collect garbage 버튼: 수동 GC 트리거(테스트용) - Console:
getEventListeners($0)(현재 Elements 패널에서 선택한 노드의 리스너 보기) - DevTools에서 소스맵 활성화하면 스택/할당 위치를 더 정확히 볼 수 있음
주의: 힙 스냅샷은 메모리를 추가로 소모할 수 있으므로 서비스 영향 고려(복제/비활성 인스턴스 권장)
node --heapsnapshot-signal=SIGUSR2 server.js
# 프로세스에 SIGUSR2 보내면 .heapsnapshot 파일 생성
kill -SIGUSR2 <PID>// debug-snapshot.js
const v8 = require('v8');
const fs = require('fs');
function saveSnapshot(filename = `heap-${Date.now()}.heapsnapshot`) {
const stream = v8.getHeapSnapshot();
const out = fs.createWriteStream(filename);
stream.pipe(out);
out.on('finish', () => console.log('saved', filename));
}
module.exports = { saveSnapshot };- DevTools Memory 탭으로 드래그&드랍하거나, 스냅샷 리스트에서
Load기능 사용 - 열면 동일한 Summary/Comparison/Retainers 뷰로 분석 가능
이 예시는 로컬 개발서버를 대상으로 동작합니다. 실제 운영에서는 접근 보안/헤드리스 브라우저 권한을 신중히 설정하세요.
// tools/collectHeapWithPuppeteer.js
// Node.js + Puppeteer로 페이지를 열고 동작을 반복하고 CDP를 통해 힙 스냅샷을 수집해 파일로 저장
const fs = require('fs');
const puppeteer = require('puppeteer');
async function saveHeapSnapshot(url, outPath) {
const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] });
const page = await browser.newPage();
const client = await page.target().createCDPSession();
// HeapProfiler 이벤트 수집을 위해 enable
await client.send('HeapProfiler.enable');
// 힙 스냅샷 조각(chunk)을 모아서 저장
let chunks = [];
client.on('HeapProfiler.addHeapSnapshotChunk', ({ chunk }) => {
chunks.push(chunk);
});
await page.goto(url);
// TODO: 여기서 자동화 동작(모달 열기/닫기 등)을 반복
// ex) for (let i=0; i<100; i++) { await page.click('#open'); await page.click('#close'); }
// 힙 스냅샷 촬영
await client.send('HeapProfiler.takeHeapSnapshot', { reportProgress: false });
// 파일로 저장
const data = chunks.join('');
fs.writeFileSync(outPath, data);
await browser.close();
}
// 사용 예
if (require.main === module) {
const url = process.argv[2] || 'http://localhost:3000';
const out = process.argv[3] || 'puppeteer-heapsnapshot.heapsnapshot';
saveHeapSnapshot(url, out).then(() => console.log('done')).catch(console.error);
}- 위 스크립트는 CDP 이벤트
HeapProfiler.addHeapSnapshotChunk를 모아.heapsnapshot파일로 쓴다. - 생성한 파일은 DevTools로 열어 분석 가능
-
Comparison에서 증가량이 큰 항목(count 또는 retained size)을 먼저 파고든다 - Retainers/Retainer path를 따라가면서 어떤 전역/클로저가 참조를 유지하는지 확인
- Detached DOM, event listeners, timers(Interval), global caches를 우선 체크
- 문제가 되는 컴포넌트/함수의 코드 위치를 찾으면 해당 리소스 해제(또는 약한 참조 사용) 적용
- 이벤트 리스너 제거(또는 cleanup) 확인
- setInterval / setTimeout 정리 확인
- 큰 데이터(이미지, 배열)를 상태/state에 그대로 두지 않음
- 사용 후 참조(null 할당 등) 끊기
- React: 불필요한 마운트/언마운트 반복 제거
- Source map 활성화(로컬 디버깅 시)
- Heap snapshot: 시점 기반 전체 힙 구조 분석
- Allocation timeline: 시간 기반 증가 추적
- Allocation sampling: 함수/위치별 할당량 샘플링
- 이 문서로 충분히 로컬/복제 환경에서 문제를 찾을 수 있음
- 다음으로는 프로덕션 메트릭 수집(Prometheus, APM)과 CI에서의 시크릿 스캔/메모리 회귀 테스트 자동화를 권장합니다
원하면 이 문서를 PDF로 변환하거나, pre-commit 설정·GitHub Actions · prometheus alert rule · puppeteer 스크립트 등 필요한 파일을 바로 만들어 드립니다.