개요

JavaScript 메모리 누수는 웹 애플리케이션의 성능을 저하시키는 주요 원인 중 하나입니다. 특히 SPA(Single Page Application)에서 페이지 전환 시 메모리 사용량이 지속적으로 증가하거나, DOM 요소를 제거했음에도 불구하고 참조가 유지되는 문제가 빈번히 발생합니다. 이 가이드에서는 Chrome DevTools를 활용해 메모리 누수를 진단하고 해결하는 체계적인 방법을 단계별로 설명합니다. 실제 사례를 바탕으로 한 코드 예제와 검증 방법을 포함해, 개발자들이 즉시 적용할 수 있는 실용적인 솔루션을 제공합니다.
문제 상황: 메모리 누수의 대표적 증상
1. 페이지 전환 시 메모리 사용량 지속 증가
SPA에서 라우터가 페이지 전환을 처리할 때, 이전 페이지의 리소스가 제대로 해제되지 않으면 메모리 사용량이 선형적으로 증가합니다. 예를 들어 React 애플리케이션에서 useEffect 정리 함수를 누락하거나, Vue에서 beforeUnmount 생명주기에서 이벤트 리스너를 제거하지 않는 경우 발생합니다.
// React에서 정리 함수 누락 사례
useEffect(() => {
const interval = setInterval(() => {
console.log('매 1초마다 실행');
}, 1000);
// 컴포넌트 언마운트 시 interval 정리 누락
}, []);
위 코드는 컴포넌트가 언마운트되어도 setInterval이 계속 실행되며 메모리를 점유합니다. Chrome Task Manager에서 해당 탭의 메모리 사용량이 시간에 따라 증가하는 것을 확인할 수 있습니다.
2. DOM 요소 제거 후에도 참조 유지
Detached DOM 트리 문제는 개발자가 DOM 요소를 참조하는 변수를 명시적으로 null로 초기화하지 않을 때 발생합니다. 예를 들어 jQuery를 사용하는 레거시 코드에서 흔히 발견됩니다.
// jQuery 캐시 참조 유지 사례
let cachedElement = $('#heavy-component');
cachedElement.remove(); // DOM에서 제거되지만 변수는 참조 유지
이 경우 DevTools Memory 탭에서 Detached DOM 노드를 확인할 수 있으며, GC(Garbage Collector)가 작동하지 않아 메모리 회수가 불가능합니다.
3. Timer 미정리로 인한 누적
setInterval, setTimeout, 또는 Promise 체이닝에서 생성된 타이머가 누적되면 메모리 누수로 이어집니다. 특히 재귀적 타이머 사용 시 주의가 필요합니다.
// 타이머 누적 사례
function startPolling() {
fetch('/api/data').then(response => {
console.log(response);
startPolling(); // 무한 재귀 호출
});
}
startPolling();
위 코드는 컴포넌트가 언마운트되어도 계속 실행되며, 메모리 누수와 불필요한 네트워크 요청을 유발합니다.
팁: 타이머 관련 작업은 반드시
clearInterval()또는AbortController로 정리해야 합니다.
원인 분석: Chrome DevTools로 메모리 누수 탐지
1. 힙 스냅샷 비교를 통한 객체 추적
Chrome DevTools Memory 탭에서 힙 스냅샷을 찍어 객체 인스턴스 수와 크기를 비교할 수 있습니다. 예를 들어 페이지 전환 전후의 스냅샷을 비교하면 누수 의심 객체를 식별할 수 있습니다.
- DevTools(F12) → Memory 탭 선택
Take heap snapshot클릭 후 페이지 로드- 페이지 전환 수행 후 다시 스냅샷 캡처
- Comparison 뷰에서 크기 변화가 큰 객체 확인
2. Detached DOM 트리 탐지
Memory 탭에서 Detached DOM tree 필터를 활성화하면 부모와 연결되지 않은 DOM 노드를 확인할 수 있습니다. 이는 메모리 누수의 명확한 지표입니다.
# DevTools 콘솔에서 강제 GC 실행
window.gc();
강제 GC 후에도 Detached DOM이 남아 있다면 참조 관계가 해제되지 않은 것입니다.
3. 이벤트 리스너와 클로저 패턴 식별
메모리 누수의 80% 이상은 클로저나 전역 변수에 의한 참조 유지에서 발생합니다. 예를 들어 이벤트 핸들러가 this를 캡처링하면서 컴포넌트 인스턴스를 참조할 수 있습니다.
// 클로저 기반 메모리 누수 사례
class Counter {
constructor() {
this.count = 0;
document.body.addEventListener('click', this.handleClick.bind(this));
}
handleClick() {
this.count++;
console.log(this.count);
}
}
// Counter 인스턴스가 이벤트 리스너에 의해 유지됨
이 경우 WeakMap을 사용해 참조를 약화시키거나, addEventListener의 두 번째 매개변수로 { once: true }를 사용할 수 있습니다.
주의:
bind(this)는 클로저를 생성하므로, 클래스 필드 문법을 사용하는 것이 더 안전합니다.
해결 방법: 메모리 누수 방지 전략
1. WeakMap/WeakSet으로 참조 관계 변경
WeakMap과 WeakSet은 강한 참조를 생성하지 않아 GC가 객체를 회수할 수 있습니다. 특히 DOM 요소와 데이터를 매핑할 때 유용합니다.
// WeakMap 적용 사례
const elementData = new WeakMap();
const element = document.createElement('div');
elementData.set(element, { metadata: 'value' });
// element가 GC 대상일 때 metadata도 자동 회수
위 코드는 Map 대신 WeakMap을 사용해 DOM 요소가 제거될 때 관련 데이터도 함께 해제됩니다.
2. 컴포넌트 언마운트 시 리소스 정리
React, Vue, Angular와 같은 프레임워크에서는 생명주기 훅을 활용해 리소스를 정리해야 합니다.
// React useEffect 정리 함수
useEffect(() => {
const controller = new AbortController();
const { signal } = controller;
async function fetchData() {
const response = await fetch('/api/data', { signal });
// 데이터 처리
}
fetchData();
return () => controller.abort(); // 언마운트 시 요청 중단
}, []);
AbortController는 fetch 요청을 중단시키고, 메모리 누수를 방지합니다.
3. 모듈 번들러 설정 최적화 (Tree Shaking)
Webpack이나 Vite와 같은 번들러에서 사용하지 않는 코드를 제거하려면 package.json에 sideEffects: false를 설정하거나, Babel 플러그인을 활용할 수 있습니다.
# webpack.config.js
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
},
};
이 설정은 Tree Shaking을 활성화해 번들 크기를 줄이고, 불필요한 코드가 메모리에 로드되는 것을 방지합니다.
검증: 메모리 누수 해결 확인
1. Performance 탭으로 메모리 프로파일링
Performance 탭에서 메모리 사용량을 실시간으로 모니터링하며, 강제 GC 후 메모리 회수를 확인할 수 있습니다.
- Performance 탭 →
Memory체크박스 활성화 Start profiling클릭- 페이지 전환 또는 작업 수행
Collect garbage버튼으로 강제 GC 실행- 메모리 사용량이 기준선으로 복귀하는지 확인
2. 강제 GC 후 메모리 회수 테스트
DevTools에서 window.gc()를 실행한 후 메모리 사용량이 감소하지 않으면 여전히 누수가 존재하는 것입니다. Node.js 환경에서는 --expose-gc 플래그로 GC를 노출할 수 있습니다.
# Node.js에서 GC 실행
node --expose-gc app.js
> gc()
3. 장기 테스트 환경 구성
로컬 개발 환경뿐 아니라 프로덕션에서도 메모리 누수를 테스트해야 합니다. Chrome의 about:memory 페이지나 Lighthouse를 활용할 수 있습니다.
# Lighthouse CLI로 성능 진단
npx lighthouse https://your-app.com --output=json --output-path=report.json
마치며
- 메모리 누수의 3대 증상은 페이지 전환 시 메모리 증가, Detached DOM, 타이머 누적입니다.
- WeakMap/WeakSet과 리소스 정리 함수로 참조를 약화시키고, Tree Shaking으로 번들 크기를 최적화하세요.
- Performance 탭과 강제 GC로 해결 여부를 반드시 검증해야 합니다.