--- id: wiki-2026-0508-브라우저-메모리-누수-탐지-browser-memory-le title: 브라우저 메모리 누수 탐지(Browser Memory Leak Detection) category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Memory Leak Detection, Browser Memory Profiling, JS Memory Leak] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [browser, memory-leak, performance, devtools, javascript] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: JavaScript framework: Chrome DevTools --- # 브라우저 메모리 누수 탐지(Browser Memory Leak Detection) ## 매 한 줄 > **"매 SPA 가 시간이 지날수록 느려진다면 90% memory leak"**. JS 의 GC 는 매 reachable object 만 살리지만, detached DOM, forgotten timer, closure capture, global accumulation 이 매 unintended retention 을 만든다. 매 Chrome DevTools `Memory` panel + `Performance` panel 의 heap snapshot diff + allocation timeline 이 매 standard tool. ## 매 핵심 ### 매 4 가지 흔한 leak pattern - **Detached DOM**: 매 element 를 removeChild 했지만 JS reference 가 살아있음 → DOM tree + element subtree 통째로 retained. - **Forgotten timer / listener**: `setInterval` / `addEventListener` 의 callback closure 가 매 component scope 를 hold. - **Closure over big data**: outer function 의 large array 를 inner closure 가 무의식적으로 capture. - **Global accumulation**: `window.cache[key] = ...` 식의 무한 적재. ### 매 detection workflow (3 snapshot technique) 1. App load → snapshot A. 2. 매 leak-suspect action 100 회 (open/close modal …) → snapshot B. 3. 매 동일 action 100 회 더 → snapshot C. 4. **Comparison**: B → C 사이 Delta 가 0 이어야 정상. 매 keep growing → leak. ### 매 응용 1. SPA debugging (React/Vue route transition leak). 2. Memory regression CI (puppeteer + heap snapshot). 3. Production monitoring (`performance.memory`). ## 💻 패턴 ### Detect detached DOM (DevTools console) ```javascript // 매 Memory panel → "Detached" filter 로 search // 또는 console 에서: function findDetached() { const all = $$('*'); // visible // queryObjects(HTMLElement) — DevTools console 전용 console.log('visible:', all.length); } queryObjects(HTMLElement); // 매 DevTools console only ``` ### Cleanup pattern (idiomatic) ```javascript // React useEffect cleanup useEffect(() => { const id = setInterval(tick, 1000); const onResize = () => recalc(); window.addEventListener('resize', onResize); return () => { clearInterval(id); window.removeEventListener('resize', onResize); }; }, []); ``` ### WeakMap / WeakRef for caches ```javascript // 매 strong ref → leak. WeakMap → key GC 되면 entry 자동 사라짐 const meta = new WeakMap(); function attach(node, info) { meta.set(node, info); } // node 가 detached 되고 reference 끊기면 entry GC // WeakRef + FinalizationRegistry (2021+) const reg = new FinalizationRegistry(id => console.log('GC:', id)); const w = new WeakRef(obj); reg.register(obj, 'my-resource'); ``` ### performance.memory monitoring (Chrome only) ```javascript function logMemory() { const m = performance.memory; console.log({ used: (m.usedJSHeapSize / 1048576).toFixed(1) + 'MB', total: (m.totalJSHeapSize / 1048576).toFixed(1) + 'MB', limit: (m.jsHeapSizeLimit / 1048576).toFixed(1) + 'MB', }); } setInterval(logMemory, 5000); ``` ### 3-snapshot diff (puppeteer CI) ```javascript import puppeteer from 'puppeteer'; const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('http://localhost:3000'); const session = await page.target().createCDPSession(); async function snapshot() { await session.send('HeapProfiler.collectGarbage'); const m = await page.evaluate(() => performance.memory.usedJSHeapSize); return m; } const a = await snapshot(); for (let i = 0; i < 100; i++) await page.click('#open-modal'), await page.click('#close-modal'); const b = await snapshot(); for (let i = 0; i < 100; i++) await page.click('#open-modal'), await page.click('#close-modal'); const c = await snapshot(); const growth = c - b; if (growth > 1_000_000) throw new Error(`Memory leak: +${growth} bytes between B→C`); ``` ### AbortController for fetch / event cleanup ```javascript const ac = new AbortController(); window.addEventListener('scroll', onScroll, { signal: ac.signal }); fetch(url, { signal: ac.signal }); // component unmount: ac.abort(); // 매 listener + fetch 동시 cleanup ``` ### Common React leak — stale closure capture ```javascript // 매 BAD — bigData 를 closure 가 잡음, unmount 후에도 retain useEffect(() => { const bigData = computeMassive(); const id = setTimeout(() => console.log(bigData[0]), 60_000); // cleanup 없음 → leak }, []); // 매 GOOD useEffect(() => { const bigData = computeMassive(); const id = setTimeout(() => console.log(bigData[0]), 60_000); return () => clearTimeout(id); }, []); ``` ## 매 결정 기준 | 상황 | Tool | |---|---| | ad-hoc 탐색 | DevTools Memory → Heap snapshot | | allocation 출처 추적 | Performance → record + Memory checkbox | | detached DOM | Memory → "Detached" filter | | CI regression | puppeteer + `HeapProfiler.collectGarbage` + `performance.memory` | | production monitoring | `performance.memory` polling → telemetry | **기본값**: 매 3-snapshot heap diff (A=load, B=after-100-actions, C=after-200-actions). B→C 0 growth 가 합격선. ## 🔗 Graph - 부모: [[Browser_Performance]] · [[Memory_Management]] - 변형: [[Heap_Snapshot]] · [[Allocation_Profiling]] · [[Detached_DOM]] - 응용: [[React_Memory_Leak]] · [[SPA_Performance]] - Adjacent: [[WeakMap]] · [[WeakRef]] · [[FinalizationRegistry]] · [[AbortController]] · [[렌더링 최적화 개념 설명 자료]] ## 🤖 LLM 활용 **언제**: SPA 가 time-on-page 로 점점 느려질 때, mobile OOM, infinite scroll page 의 retention. **언제 X**: server-side memory leak (그건 Node heap profiler 영역). ## ❌ 안티패턴 - **GC 강제 호출 시도**: 매 `gc()` 는 standard 아님. DevTools "Collect garbage" 버튼이 유일. - **`performance.memory` 만 보고 판단**: 매 Chromium 전용, Firefox/Safari 없음. heap snapshot 이 ground truth. - **`removeChild` 만 하고 reference cleanup 안 함**: 매 detached DOM leak 의 source. - **`setInterval` 매 component mount 마다 + cleanup 없음**: 매 SPA killer. ## 🧪 검증 / 중복 - Verified (Chrome DevTools docs, web.dev memory profiling, V8 blog). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — leak patterns + 3-snapshot diff + WeakRef/AbortController patterns |