<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Memory-Leak on Chanyeol Dev</title>
    <link>https://chanyeols.com/tags/memory-leak/</link>
    <description>Recent content in Memory-Leak on Chanyeol Dev</description>
    <generator>Hugo</generator>
    <language>ko-kr</language>
    <lastBuildDate>Sat, 18 Apr 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://chanyeols.com/tags/memory-leak/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Chrome DevTools로 잡는 JavaScript 메모리 누수</title>
      <link>https://chanyeols.com/posts/javascript-memory-leak-troubleshooting-guide/</link>
      <pubDate>Sat, 18 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://chanyeols.com/posts/javascript-memory-leak-troubleshooting-guide/</guid>
      <description>javascript-메모리-누수-트러블슈팅-완벽-가이드-크롬-디버그-방법-및-실제-사례-분석</description>
      <content:encoded><![CDATA[<h2 id="개요">개요</h2>
<p><img alt="Chrome DevTools로 잡는 JavaScript 메모리 누수" loading="lazy" src="https://pixabay.com/get/g81144dab2fa3160d1c331363701fed736fa85b5204224b6ed7e791a406e76822510e4b5aab22d13e0b1f770813df8787d8fd4ae2d6180285a04537540d20edd8_1280.jpg"></p>
<p>JavaScript 메모리 누수는 웹 애플리케이션의 성능을 저하시키는 주요 원인 중 하나입니다. 특히 SPA(Single Page Application)에서 페이지 전환 시 메모리 사용량이 지속적으로 증가하거나, DOM 요소를 제거했음에도 불구하고 참조가 유지되는 문제가 빈번히 발생합니다. 이 가이드에서는 Chrome DevTools를 활용해 메모리 누수를 진단하고 해결하는 체계적인 방법을 단계별로 설명합니다. 실제 사례를 바탕으로 한 코드 예제와 검증 방법을 포함해, 개발자들이 즉시 적용할 수 있는 실용적인 솔루션을 제공합니다.</p>
<h2 id="문제-상황-메모리-누수의-대표적-증상">문제 상황: 메모리 누수의 대표적 증상</h2>
<h3 id="1-페이지-전환-시-메모리-사용량-지속-증가">1. 페이지 전환 시 메모리 사용량 지속 증가</h3>
<p>SPA에서 라우터가 페이지 전환을 처리할 때, 이전 페이지의 리소스가 제대로 해제되지 않으면 메모리 사용량이 선형적으로 증가합니다. 예를 들어 React 애플리케이션에서 <code>useEffect</code> 정리 함수를 누락하거나, Vue에서 <code>beforeUnmount</code> 생명주기에서 이벤트 리스너를 제거하지 않는 경우 발생합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// React에서 정리 함수 누락 사례
</span></span></span><span style="display:flex;"><span>useEffect(() =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> interval <span style="color:#ff7b72;font-weight:bold">=</span> setInterval(() =&gt; {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;매 1초마다 실행&#39;</span>);
</span></span><span style="display:flex;"><span>  }, <span style="color:#a5d6ff">1000</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#8b949e;font-style:italic">// 컴포넌트 언마운트 시 interval 정리 누락
</span></span></span><span style="display:flex;"><span>}, []);
</span></span></code></pre></div><p>위 코드는 컴포넌트가 언마운트되어도 <code>setInterval</code>이 계속 실행되며 메모리를 점유합니다. Chrome Task Manager에서 해당 탭의 메모리 사용량이 시간에 따라 증가하는 것을 확인할 수 있습니다.</p>
<h3 id="2-dom-요소-제거-후에도-참조-유지">2. DOM 요소 제거 후에도 참조 유지</h3>
<p>Detached DOM 트리 문제는 개발자가 DOM 요소를 참조하는 변수를 명시적으로 <code>null</code>로 초기화하지 않을 때 발생합니다. 예를 들어 jQuery를 사용하는 레거시 코드에서 흔히 발견됩니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// jQuery 캐시 참조 유지 사례
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">let</span> cachedElement <span style="color:#ff7b72;font-weight:bold">=</span> $(<span style="color:#a5d6ff">&#39;#heavy-component&#39;</span>);
</span></span><span style="display:flex;"><span>cachedElement.remove(); <span style="color:#8b949e;font-style:italic">// DOM에서 제거되지만 변수는 참조 유지
</span></span></span></code></pre></div><p>이 경우 DevTools Memory 탭에서 Detached DOM 노드를 확인할 수 있으며, GC(Garbage Collector)가 작동하지 않아 메모리 회수가 불가능합니다.</p>
<h3 id="3-timer-미정리로-인한-누적">3. Timer 미정리로 인한 누적</h3>
<p><code>setInterval</code>, <code>setTimeout</code>, 또는 Promise 체이닝에서 생성된 타이머가 누적되면 메모리 누수로 이어집니다. 특히 재귀적 타이머 사용 시 주의가 필요합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 타이머 누적 사례
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">function</span> startPolling() {
</span></span><span style="display:flex;"><span>  fetch(<span style="color:#a5d6ff">&#39;/api/data&#39;</span>).then(response =&gt; {
</span></span><span style="display:flex;"><span>    console.log(response);
</span></span><span style="display:flex;"><span>    startPolling(); <span style="color:#8b949e;font-style:italic">// 무한 재귀 호출
</span></span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>startPolling();
</span></span></code></pre></div><p>위 코드는 컴포넌트가 언마운트되어도 계속 실행되며, 메모리 누수와 불필요한 네트워크 요청을 유발합니다.</p>
<blockquote>
<p><strong>팁</strong>: 타이머 관련 작업은 반드시 <code>clearInterval()</code> 또는 <code>AbortController</code>로 정리해야 합니다.</p>
</blockquote>
<h2 id="원인-분석-chrome-devtools로-메모리-누수-탐지">원인 분석: Chrome DevTools로 메모리 누수 탐지</h2>
<h3 id="1-힙-스냅샷-비교를-통한-객체-추적">1. 힙 스냅샷 비교를 통한 객체 추적</h3>
<p>Chrome DevTools Memory 탭에서 힙 스냅샷을 찍어 객체 인스턴스 수와 크기를 비교할 수 있습니다. 예를 들어 페이지 전환 전후의 스냅샷을 비교하면 누수 의심 객체를 식별할 수 있습니다.</p>
<ol>
<li>DevTools(F12) → Memory 탭 선택</li>
<li><code>Take heap snapshot</code> 클릭 후 페이지 로드</li>
<li>페이지 전환 수행 후 다시 스냅샷 캡처</li>
<li>Comparison 뷰에서 크기 변화가 큰 객체 확인</li>
</ol>
<p><img alt="힙 스냅샷 비교 예시" loading="lazy" src="https://developer.chrome.com/docs/devtools/memory-performance/articles/heap-snapshot"></p>
<h3 id="2-detached-dom-트리-탐지">2. Detached DOM 트리 탐지</h3>
<p>Memory 탭에서 <code>Detached DOM tree</code> 필터를 활성화하면 부모와 연결되지 않은 DOM 노드를 확인할 수 있습니다. 이는 메모리 누수의 명확한 지표입니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># DevTools 콘솔에서 강제 GC 실행</span>
</span></span><span style="display:flex;"><span>window.gc<span style="color:#ff7b72;font-weight:bold">()</span>;
</span></span></code></pre></div><p>강제 GC 후에도 Detached DOM이 남아 있다면 참조 관계가 해제되지 않은 것입니다.</p>
<h3 id="3-이벤트-리스너와-클로저-패턴-식별">3. 이벤트 리스너와 클로저 패턴 식별</h3>
<p>메모리 누수의 80% 이상은 클로저나 전역 변수에 의한 참조 유지에서 발생합니다. 예를 들어 이벤트 핸들러가 <code>this</code>를 캡처링하면서 컴포넌트 인스턴스를 참조할 수 있습니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 클로저 기반 메모리 누수 사례
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">class</span> Counter {
</span></span><span style="display:flex;"><span>  constructor() {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">this</span>.count <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>;
</span></span><span style="display:flex;"><span>    document.body.addEventListener(<span style="color:#a5d6ff">&#39;click&#39;</span>, <span style="color:#ff7b72">this</span>.handleClick.bind(<span style="color:#ff7b72">this</span>));
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  handleClick() {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">this</span>.count<span style="color:#ff7b72;font-weight:bold">++</span>;
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#ff7b72">this</span>.count);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// Counter 인스턴스가 이벤트 리스너에 의해 유지됨
</span></span></span></code></pre></div><p>이 경우 <code>WeakMap</code>을 사용해 참조를 약화시키거나, <code>addEventListener</code>의 두 번째 매개변수로 <code>{ once: true }</code>를 사용할 수 있습니다.</p>
<blockquote>
<p><strong>주의</strong>: <code>bind(this)</code>는 클로저를 생성하므로, 클래스 필드 문법을 사용하는 것이 더 안전합니다.</p>
</blockquote>
<h2 id="해결-방법-메모리-누수-방지-전략">해결 방법: 메모리 누수 방지 전략</h2>
<h3 id="1-weakmapweakset으로-참조-관계-변경">1. WeakMap/WeakSet으로 참조 관계 변경</h3>
<p><code>WeakMap</code>과 <code>WeakSet</code>은 강한 참조를 생성하지 않아 GC가 객체를 회수할 수 있습니다. 특히 DOM 요소와 데이터를 매핑할 때 유용합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// WeakMap 적용 사례
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> elementData <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> WeakMap();
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> element <span style="color:#ff7b72;font-weight:bold">=</span> document.createElement(<span style="color:#a5d6ff">&#39;div&#39;</span>);
</span></span><span style="display:flex;"><span>elementData.set(element, { metadata<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;value&#39;</span> });
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// element가 GC 대상일 때 metadata도 자동 회수
</span></span></span></code></pre></div><p>위 코드는 <code>Map</code> 대신 <code>WeakMap</code>을 사용해 DOM 요소가 제거될 때 관련 데이터도 함께 해제됩니다.</p>
<h3 id="2-컴포넌트-언마운트-시-리소스-정리">2. 컴포넌트 언마운트 시 리소스 정리</h3>
<p>React, Vue, Angular와 같은 프레임워크에서는 생명주기 훅을 활용해 리소스를 정리해야 합니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// React useEffect 정리 함수
</span></span></span><span style="display:flex;"><span>useEffect(() =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> controller <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> AbortController();
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> { signal } <span style="color:#ff7b72;font-weight:bold">=</span> controller;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> fetchData() {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> response <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> fetch(<span style="color:#a5d6ff">&#39;/api/data&#39;</span>, { signal });
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 데이터 처리
</span></span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  fetchData();
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">return</span> () =&gt; controller.abort(); <span style="color:#8b949e;font-style:italic">// 언마운트 시 요청 중단
</span></span></span><span style="display:flex;"><span>}, []);
</span></span></code></pre></div><p><code>AbortController</code>는 fetch 요청을 중단시키고, 메모리 누수를 방지합니다.</p>
<h3 id="3-모듈-번들러-설정-최적화-tree-shaking">3. 모듈 번들러 설정 최적화 (Tree Shaking)</h3>
<p>Webpack이나 Vite와 같은 번들러에서 사용하지 않는 코드를 제거하려면 <code>package.json</code>에 <code>sideEffects: false</code>를 설정하거나, Babel 플러그인을 활용할 수 있습니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># webpack.config.js</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">module.exports = {</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">mode</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#39;production&#39;</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">optimization</span>:<span style="color:#6e7681"> </span>{<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">usedExports</span>:<span style="color:#6e7681"> </span><span style="color:#79c0ff">true</span>,<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span>},<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span>}<span style="color:#a5d6ff">;</span><span style="color:#6e7681">
</span></span></span></code></pre></div><p>이 설정은 Tree Shaking을 활성화해 번들 크기를 줄이고, 불필요한 코드가 메모리에 로드되는 것을 방지합니다.</p>
<h2 id="검증-메모리-누수-해결-확인">검증: 메모리 누수 해결 확인</h2>
<h3 id="1-performance-탭으로-메모리-프로파일링">1. Performance 탭으로 메모리 프로파일링</h3>
<p>Performance 탭에서 메모리 사용량을 실시간으로 모니터링하며, 강제 GC 후 메모리 회수를 확인할 수 있습니다.</p>
<ol>
<li>Performance 탭 → <code>Memory</code> 체크박스 활성화</li>
<li><code>Start profiling</code> 클릭</li>
<li>페이지 전환 또는 작업 수행</li>
<li><code>Collect garbage</code> 버튼으로 강제 GC 실행</li>
<li>메모리 사용량이 기준선으로 복귀하는지 확인</li>
</ol>
<h3 id="2-강제-gc-후-메모리-회수-테스트">2. 강제 GC 후 메모리 회수 테스트</h3>
<p>DevTools에서 <code>window.gc()</code>를 실행한 후 메모리 사용량이 감소하지 않으면 여전히 누수가 존재하는 것입니다. Node.js 환경에서는 <code>--expose-gc</code> 플래그로 GC를 노출할 수 있습니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Node.js에서 GC 실행</span>
</span></span><span style="display:flex;"><span>node --expose-gc app.js
</span></span><span style="display:flex;"><span>&gt; gc<span style="color:#ff7b72;font-weight:bold">()</span>
</span></span></code></pre></div><h3 id="3-장기-테스트-환경-구성">3. 장기 테스트 환경 구성</h3>
<p>로컬 개발 환경뿐 아니라 프로덕션에서도 메모리 누수를 테스트해야 합니다. Chrome의 <code>about:memory</code> 페이지나 Lighthouse를 활용할 수 있습니다.</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Lighthouse CLI로 성능 진단</span>
</span></span><span style="display:flex;"><span>npx lighthouse https://your-app.com --output<span style="color:#ff7b72;font-weight:bold">=</span>json --output-path<span style="color:#ff7b72;font-weight:bold">=</span>report.json
</span></span></code></pre></div><h2 id="마치며">마치며</h2>
<ol>
<li><strong>메모리 누수의 3대 증상</strong>은 페이지 전환 시 메모리 증가, Detached DOM, 타이머 누적입니다.</li>
<li><strong>WeakMap/WeakSet</strong>과 <strong>리소스 정리 함수</strong>로 참조를 약화시키고, <strong>Tree Shaking</strong>으로 번들 크기를 최적화하세요.</li>
<li><strong>Performance 탭</strong>과 <strong>강제 GC</strong>로 해결 여부를 반드시 검증해야 합니다.</li>
</ol>
<ul>
<li><a href="https://developer.chrome.com/docs/devtools/memory-performance/">Chrome DevTools Memory 문서</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management">MDN Web Docs: Memory Management</a></li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
