[Troubleshooting] JavaScript 메모리 누수, 'Heap Dump'로 잡아낸 실전 디버깅 사례
현대적인 자바스크립트 엔진(V8 등)의 가비지 컬렉션(GC)은 매우 영리하지만, 개발자의 부주의까지 모두 해결해주지는 않는다. 특히 SPA(Single Page Application)나 고성능 Node.js 서버 환경에서 발생하는 메모리 누수는 시스템 전체를 서서히 마비시키는 '소리 없는 살인마'와 같다. 17년 차 풀스택 개발자의 관점에서, 실제 겪었던 누수 사례와 이를 추적하는 팩트 중심의 디버깅 프로세스를 정리한다.
1. 현상: "시간이 지날수록 서비스가 느려진다"
메모리 누수의 전형적인 증상은 점진적 성능 저하다. 처음엔 쾌적하던 서비스가 시간이 지날수록 응답 속도가 떨어지고, 결국 브라우저 탭이 응답하지 않거나(OOM: Out Of Memory) 서버 프로세스가 재시작되는 현상이 반복된다.
2. 주요 원인: 범인은 언제나 '살아있는 참조'
GC는 "더 이상 참조되지 않는 객체"를 수거한다. 누수는 사용이 끝났음에도 참조가 끊기지 않았을 때 발생한다.
① 전역 변수의 남용 (Accidental Global Variables)
var, let, const 없이 선언된 변수는 window 혹은 global 객체에 바인딩되어 애플리케이션 수명 내내 메모리를 점유한다.
② 해제되지 않은 타이머와 콜백 (Forgotten Timers)
setInterval이나 setTimeout 내부에서 외부 변수를 참조하고 있다면, 타이머를 clearInterval로 명시적으로 제거하기 전까지 해당 변수들은 메모리에서 해제되지 않는다.
③ 클로저(Closure)의 함정
특정 함수가 종료된 후에도 클로저에 의해 참조되는 변수들은 GC의 수거 대상에서 제외된다. 특히 대형 객체를 참조하는 클로저가 반복 생성될 경우 치명적이다.
④ 탈착된 DOM 노드 (Detached DOM Nodes)
화면에서 제거되었지만, JavaScript 변수가 해당 DOM 노드를 여전히 참조하고 있는 경우다. 트리 전체가 메모리에 남게 되는 원인이 된다.
3. 실전 디버깅 3단계 (Chrome DevTools 활용)
Step 1: 가시화 (Performance Monitor)
Chrome 개발자 도구의 'Performance monitor' 탭을 연다. 서비스를 이용하면서 **'JS Heap Size'**가 우상향하는지 확인한다. GC가 동작했음에도 저점이 계속 높아진다면 100% 누수다.
Step 2: 스냅샷 비교 (Heap Snapshot)
'Memory' 탭에서 세 번의 스냅샷을 찍는다.
초기 상태
특정 동작 반복 후
다시 초기 화면으로 복귀 후
이후 'Comparison' 뷰를 통해 1번과 3번 사이에서 삭제되지 않고 남아있는 객체를 추적한다.
Step 3: 할당 추적 (Allocation Instrumentation)
누수가 발생하는 시점에 어떤 함수가 객체를 생성했는지 실시간으로 확인한다. **'Allocation timeline'**을 통해 파란색 막대(할당)가 회색(해제)으로 변하지 않는 구간을 집중 분석한다.
4. 실제 디버깅 사례: "이벤트 리스너의 배신"
모 기업의 대시보드 프로젝트에서 페이지 이동 시마다 메모리가 약 50MB씩 증폭되는 현상을 발견했다.
원인: 차트 라이브러리 연동 시
window.resize이벤트를 등록했으나, 컴포넌트가unmount될 때removeEventListener를 호출하지 않았다.결과: 페이지를 나갈 때마다 차트 인스턴스와 데이터 대용량 배열이
window객체에 묶여 누적되었다.해결:
useEffect의 cleanup 함수(React 기준)나 컴포넌트 파괴 라이프사이클에서 리스너를 명시적으로 제거하여 해결했다.
5. 핵심 체크리스트
| 점검 항목 | 조치 사항 |
| Event Listeners | addEventListener가 있다면 반드시 remove가 세트인가? |
| Global State | Redux, Pinia 등 전역 상태에 불필요하게 큰 객체를 담지 않았나? |
| Third-party Libs | 라이브러리 인스턴스를 명시적으로 destroy() 하고 있는가? |
| WeakMap/WeakSet | 캐시 목적이라면 참조가 끊기면 자동 수거되는 Weak 자료구조를 썼나? |
댓글
댓글 쓰기