<?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>기타 on Chanyeol Dev</title>
    <link>https://chanyeols.com/categories/%EA%B8%B0%ED%83%80/</link>
    <description>Recent content in 기타 on Chanyeol Dev</description>
    <generator>Hugo</generator>
    <language>ko-kr</language>
    <lastBuildDate>Mon, 20 Apr 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://chanyeols.com/categories/%EA%B8%B0%ED%83%80/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>SSH PortProxy로 안전하게 원격 서비스 접근하기</title>
      <link>https://chanyeols.com/posts/ssh-portproxy-configuration-guide/</link>
      <pubDate>Mon, 20 Apr 2026 00:00:00 +0000</pubDate>
      <guid>https://chanyeols.com/posts/ssh-portproxy-configuration-guide/</guid>
      <description>ssh portproxy로 안전한 포트 포워딩 구성법: 로컬/원격 서비스 접근, 방화벽 우회, 트러블슈팅까지 단계별 가이드</description>
      <content:encoded><![CDATA[<h2 id="튜토리얼-개요">튜토리얼 개요</h2>
<p><img alt="SSH PortProxy로 안전하게 원격 서비스 접근하기" loading="lazy" src="/images/ssh-portproxy-configuration-guide-body.jpg"></p>
<p>SSH PortProxy는 방화벽 제약이 있는 환경에서도 안전하게 원격 서비스에 접근할 수 있는 강력한 기술입니다. 기존 SSH 터널링과 유사하지만, 로컬 포트 포워딩을 더욱 직관적으로 구성할 수 있으며, 별도의 터널링 소프트웨어 없이도 작동합니다. 예를 들어 개발 서버가 외부 접속을 차단하고 있더라도, SSH 게이트웨이 서버를 경유하여 로컬 머신에서 <code>localhost:8080</code>으로 접근하면 원격 서버의 웹 서비스에 연결할 수 있습니다. 이는 데이터베이스 접근이나 내부 관리 콘솔 접속 시 특히 유용합니다.</p>
<p>PortProxy의 핵심 장점은 다음과 같습니다:</p>
<ol>
<li><strong>방화벽 우회</strong>: 원격 서버의 인바운드 허용 없이도 게이트웨이 서버의 SSH 포트(예: 22)만 열려 있으면 접근 가능</li>
<li><strong>암호화 통신</strong>: 모든 트래픽이 SSH 터널 내에서 암호화되므로 중간자 공격 방지</li>
<li><strong>간단한 설정</strong>: 단일 SSH 명령어 또는 설정 파일 수정만으로 구성 완료</li>
</ol>
<blockquote>
<p>💡 기존 SSH 터널링과의 차이점: <code>-L</code> 옵션은 명시적 포트 바인딩이 필요하지만, PortProxy는 동적 포트 할당이 가능해 유연성이 높습니다.</p>
</blockquote>
<p>공식 문서: <a href="https://www.openssh.com/portforwarding.html">OpenSSH Port Forwarding</a></p>
<hr>
<h2 id="사전-준비-사항">사전 준비 사항</h2>
<h3 id="1-ssh-서버-환경-확인">1. SSH 서버 환경 확인</h3>
<p>PortProxy를 사용하려면 OpenSSH 5.4 이상이 설치되어 있어야 합니다. 다음 명령어로 버전을 확인하세요:</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>ssh -V
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 출력 예시: OpenSSH_9.2p1, OpenSSL 3.0.2 15 Mar 2023</span>
</span></span></code></pre></div><p>서버 측 <code>/etc/ssh/sshd_config</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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># /etc/ssh/sshd_config</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">AllowTcpForwarding yes</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">PermitTTY no </span><span style="color:#6e7681"> </span><span style="color:#8b949e;font-style:italic"># 필요 시 활성화</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">X11Forwarding no</span><span style="color:#6e7681">
</span></span></span></code></pre></div><p>변경 후 SSH 서비스 재시작:</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>sudo systemctl restart sshd
</span></span></code></pre></div><h3 id="2-접근-권한-검증">2. 접근 권한 검증</h3>
<p>원격 서버의 대상 포트(예: 80, 3306)에 대한 접근 권한이 있는지 확인해야 합니다. 다음 명령어로 테스트:</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>telnet target-server <span style="color:#a5d6ff">80</span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 또는</span>
</span></span><span style="display:flex;"><span>nc -zv target-server <span style="color:#a5d6ff">80</span>
</span></span></code></pre></div><p>접속 거부 시 방화벽 규칙 또는 애플리케이션 설정을 점검해야 합니다.</p>
<h3 id="3-인증-설정-테스트">3. 인증 설정 테스트</h3>
<p>비밀번호 또는 SSH 키 기반 인증이 작동하는지 확인합니다:</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>ssh user@gateway-server -o <span style="color:#79c0ff">PreferredAuthentications</span><span style="color:#ff7b72;font-weight:bold">=</span>password
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 또는 키 인증 테스트</span>
</span></span><span style="display:flex;"><span>ssh -i ~/.ssh/id_rsa user@gateway-server
</span></span></code></pre></div><p>인증 실패 시 <code>~/.ssh/config</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-bash" data-lang="bash"><span style="display:flex;"><span>Host gateway
</span></span><span style="display:flex;"><span>  HostName 192.168.1.10
</span></span><span style="display:flex;"><span>  User admin
</span></span><span style="display:flex;"><span>  IdentityFile ~/.ssh/id_rsa_gateway
</span></span><span style="display:flex;"><span>  IdentitiesOnly yes
</span></span></code></pre></div><hr>
<h2 id="단계별-portproxy-설정">단계별 PortProxy 설정</h2>
<h3 id="1-cli-명령어를-통한-즉시-설정">1. CLI 명령어를 통한 즉시 설정</h3>
<p>가장 간단한 방법은 SSH 명령어 인자에 <code>-D</code> 옵션을 사용하는 것입니다. 예를 들어 로컬 포트 8080을 원격 서버의 80포트에 바인딩하려면:</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>ssh -D <span style="color:#a5d6ff">8080</span> user@gateway-server
</span></span></code></pre></div><p>이후 로컬 머신에서 브라우저 또는 curl로 <code>localhost:8080</code>에 접근하면 게이트웨이 서버를 경유하여 원격 서버의 웹 페이지에 연결됩니다.</p>
<p>특정 호스트의 특정 포트에 고정하려면 <code>-L</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-bash" data-lang="bash"><span style="display:flex;"><span>ssh -L 8080:target-db-server:3306 user@gateway-server
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 로컬 8080 → 게이트웨이 경유 → target-db-server:3306</span>
</span></span></code></pre></div><h3 id="2-설정-파일을-통한-영구-구성">2. 설정 파일을 통한 영구 구성</h3>
<p>매번 CLI를 입력하지 않으려면 <code>~/.ssh/config</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-bash" data-lang="bash"><span style="display:flex;"><span>Host db-proxy
</span></span><span style="display:flex;"><span>  ProxyCommand ssh -W %h:%p gateway-server
</span></span><span style="display:flex;"><span>  LocalForward <span style="color:#a5d6ff">53306</span> target-db-server:3306
</span></span></code></pre></div><p>이후 <code>mysql -h 127.0.0.1 -P 53306 -u user -p</code>로 바로 접근 가능합니다.</p>
<h3 id="3-os별-설정-차이">3. OS별 설정 차이</h3>
<ul>
<li><strong>Windows (OpenSSH)</strong>: <code>ssh -D 1080 user@gateway</code> 실행 후 PuTTY의 <code>Proxy &gt; SOCKS</code> 설정에서 <code>127.0.0.1:1080</code> 지정</li>
<li><strong>macOS</strong>: 기본 터미널에서 Linux와 동일한 명령어 사용. GUI 도구인 <a href="https://github.com/breakwa11/ShadowsocksX-NG">ShadowsocksX-NG</a>를 활용할 수도 있습니다.</li>
<li><strong>Linux</strong>: <code>autossh</code>로 백그라운드 실행 가능
<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>autossh -M <span style="color:#a5d6ff">0</span> -f -N -L 8080:target:80 user@gateway
</span></span></code></pre></div></li>
</ul>
<blockquote>
<p>⚠️ Windows에서 WSL(Windows Subsystem for Linux)을 사용할 경우, 호스트 머신의 포트를 WSL에 노출하려면 <code>netsh interface portproxy</code> 설정이 추가로 필요합니다.</p>
</blockquote>
<hr>
<h2 id="트러블슈팅">트러블슈팅</h2>
<h3 id="1-connection-refused-오류">1. Connection Refused 오류</h3>
<ul>
<li><strong>포트 접근 권한</strong>: 원격 서버에서 <code>netstat -tuln</code> 또는 <code>ss -tuln</code>으로 대상 포트가 리슨 중인지 확인
<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>ss -tuln | grep :3306
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 출력 예시: tcp  LISTEN 0      128    0.0.0.0:3306   0.0.0.0:*</span>
</span></span></code></pre></div></li>
<li><strong>게이트웨이 권한</strong>: <code>sshd_config</code>에서 <code>AllowTcpForwarding</code>가 <code>yes</code>인지 확인</li>
</ul>
<h3 id="2-selinux방화벽-문제">2. SELinux/방화벽 문제</h3>
<ul>
<li><strong>SELinux 모드 확인</strong>:
<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>getenforce
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 출력: Enforcing → 문제 가능성 있음</span>
</span></span></code></pre></div>일시적 해결:
<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>setenforce <span style="color:#a5d6ff">0</span>
</span></span></code></pre></div>영구 적용:
<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>sudo sed -i <span style="color:#a5d6ff">&#39;s/SELINUX=enforcing/SELINUX=permissive/&#39;</span> /etc/selinux/config
</span></span></code></pre></div></li>
<li><strong>방화벽 허용</strong>:
<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>sudo ufw allow from 192.168.1.0/24 to any port <span style="color:#a5d6ff">22</span>
</span></span><span style="display:flex;"><span>sudo firewall-cmd --permanent --add-port<span style="color:#ff7b72;font-weight:bold">=</span>22/tcp
</span></span></code></pre></div></li>
</ul>
<h3 id="3-로컬-포트-충돌">3. 로컬 포트 충돌</h3>
<p>로컬 포트가 이미 사용 중일 경우 <code>netstat</code> 또는 <code>ss</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-bash" data-lang="bash"><span style="display:flex;"><span>ss -tuln | grep :8080
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 프로세스 종료</span>
</span></span><span style="display:flex;"><span>kill -9 <span style="color:#ff7b72">$(</span>ss -tulnpe | grep :8080 | awk <span style="color:#a5d6ff">&#39;{print $6}&#39;</span> | cut -d<span style="color:#a5d6ff">&#39;,&#39;</span> -f1<span style="color:#ff7b72">)</span>
</span></span></code></pre></div><hr>
<h2 id="마치며">마치며</h2>
<p>SSH PortProxy는 방화벽 제약이 있는 환경에서도 안전하게 원격 서비스에 접근할 수 있는 실용적인 기술입니다. 데이터베이스 접근 시 <code>-L 53306:db-server:3306</code>으로 로컬 포트를 바인딩하거나, 관리 콘솔 접근 시 SOCKS 프록시(<code>-D 1080</code>)를 구성하는 등 다양한 시나리오에 적용할 수 있습니다.</p>
<h3 id="핵심-요약">핵심 요약</h3>
<ol>
<li>PortProxy는 방화벽 우회와 암호화된 통신을 동시에 제공합니다.</li>
<li><code>~/.ssh/config</code>를 활용하면 다중 포트 포워딩을 간편하게 관리할 수 있습니다.</li>
<li>TCP Wrappers(<code>/etc/hosts.allow</code>)와 결합하면 특정 IP만 접근을 허용할 수 있습니다.</li>
</ol>
<h3 id="참고-링크">참고 링크</h3>
<ul>
<li><a href="https://www.ssh.com/ssh/tunneling/example">OpenSSH 포트 포워딩 공식 문서</a></li>
<li><a href="https://wiki.archlinux.org/title/TCP_wrappers">TCP Wrappers 보안 강화 가이드</a></li>
</ul>
]]></content:encoded>
    </item>
    <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>
