메모리 누수(Memory Leak)는 더 이상 쓰지 않는 메모리가 해제되지 않고 계속 남는 현상이다. 자바스크립트는 가비지 컬렉션(Garbage Collection, GC)으로 메모리를 자동으로 회수하므로, 누수는 대개 필요 없어진 객체를 가리키는 참조가 어딘가에 남아 있을 때 생긴다.
GC는 도달 가능성으로 판단한다
현대 엔진의 GC는 마크 앤 스윕(mark-and-sweep) 방식이다. 루트(자바스크립트에서는 전역 객체)에서 출발해 참조를 따라 닿는 객체를 모두 찾고, 닿지 않는 객체를 회수한다. 즉 "필요 없다"를 "도달할 수 없다(unreachable)"로 바꿔 판단한다.
- GC는 앞으로 그 객체를 다시 쓸지 알 수 없다. 다시 쓰지 않을 객체도 루트에서 닿으면 남는다
- 서로만 참조하는 순환 참조는 루트에서 닿지 않으면 회수된다. 순환 참조가 누수 원인이던 것은 옛 참조 카운팅(reference counting) 방식이다
- 자바스크립트에는 GC를 직접 실행하는 표준 API가 없다. 메모리를 놓아 주려면 참조를 끊어 닿지 않게 만들어야 한다
흔한 원인
- 의도하지 않은 전역 변수: 엄격 모드가 아닌 코드에서 선언하지 않은 변수에 대입하면 전역 객체에 속성이 생긴다. 전역 객체는 루트이므로 거기 매달린 값은 페이지가 살아 있는 동안 회수되지 않는다. 엄격 모드(
'use strict')에서는 이런 대입이ReferenceError가 되고, ES 모듈은 처음부터 엄격 모드다 - 분리된 DOM 노드(detached DOM node): DOM 트리에서 떼어 냈지만 JS가 아직 참조하는 노드다. DOM 노드는 DOM 트리와 JS 코드 어느 쪽에서도 참조하지 않아야 회수된다. 떼어 낸
ul을 변수에 담아 두면 그 아래li들까지 함께 남는다 - 클로저가 붙잡은 큰 참조: 클로저(Closure)는 바깥 스코프의 변수를 참조한다. 이벤트 핸들러나 캐시에 등록된 클로저가 살아 있으면, 그 클로저가 참조하는 큰 배열이나 DOM 노드도 루트에서 닿으므로 남는다
- 해제하지 않은 타이머와 이벤트 리스너:
setInterval콜백은clearInterval로 취소할 때까지 계속 실행된다. 리스너는removeEventListener로 떼거나signal로 취소하기 전까지 대상에 등록된 채 남는다.window처럼 페이지 내내 살아 있는 대상이면 리스너도 계속 남는다. 콜백이 참조하는 값도 같이 남으므로, 쓰임이 끝나면clearInterval이나removeEventListener로 해제한다
function openPanel(panel) {
const rows = loadBigData() // 큰 배열
const id = setInterval(() => render(panel, rows), 1000)
const controller = new AbortController()
window.addEventListener('resize', () => render(panel, rows), { signal: controller.signal })
return function close() { // 패널을 닫을 때 부른다
clearInterval(id) // 타이머 해제
controller.abort() // signal로 등록한 리스너 일괄 해제
}
}removeEventListener는 등록할 때와 같은 함수 참조가 있어야 뗄 수 있다. 익명 함수로 등록했다면 위처럼signal옵션을 주고abort()로 떼는 편이 간단하다- React의 Effect 정리 함수나 옵저버의 구독 해제도 같은 문제를 다룬다(Effect 정리 함수, 옵저버 패턴)
- 객체에 데이터를 덧붙이되 그 객체가 사라질 때 같이 사라져야 한다면
WeakMap을 쓴다.WeakMap의 키는 약하게 참조되어, 다른 곳에서 키를 참조하지 않으면 키와 값 모두 회수될 수 있다
찾는 법
크롬 개발자 도구 Memory 패널에서 힙 스냅샷을 찍고 클래스 필터에 Detached를 입력하면 분리된 DOM 트리를 찾을 수 있다(크롬 개발자 도구 팁). 노드를 고르면 어떤 변수가 그 노드를 참조하는지 보인다. 스냅샷 두 장을 비교해 쌓이는 객체와 붙잡는 참조(retainer)를 찾는 절차는 운영 Node 프로세스의 메모리 누수 찾기에 정리했다.
출처: MDN - Memory management: Mark-and-sweep algorithm · WeakMaps and WeakSets · MDN - Strict mode: Assigning to undeclared variables · MDN - Closures · MDN - addEventListener: Memory issues · signal · MDN - setInterval · Chrome DevTools - Discover detached DOM tree memory leaks