메모리 모델 — 가비지 컬렉션 알고리즘과 메모리 프로파일링
자바스크립트는 가비지 컬렉션(GC)이 자동으로 메모리를 관리하지만, 개발자가 메모리 누수를 만들 수 있습니다. GC의 동작 원리를 이해하면 메모리 누수를 예방하고 디버깅할 수 있습니다.
메모리 수명주기
1. 할당 (Allocation) → 변수 선언, 객체 생성
2. 사용 (Use) → 읽기, 쓰기
3. 해제 (Release) → GC가 자동으로 처리
// 1. 할당
const obj = { name: "정훈" }; // 힙에 객체 할당
const arr = [1, 2, 3]; // 힙에 배열 할당
const str = "hello"; // 문자열 할당
// 2. 사용
console.log(obj.name);
// 3. 해제 — 참조가 사라지면 GC 대상
// obj = null; → GC가 나중에 메모리 해제
가비지 컬렉션 알고리즘
Mark-and-Sweep (표시하고 쓸어내기)
V8을 포함한 현대 엔진이 사용하는 기본 알고리즘입니다.
1. 루트(전역 객체, 스택)에서 시작
2. 루트에서 도달 가능한 모든 객체를 "표시"
3. 표시되지 않은 객체의 메모리를 해제
// 도달 가능 → GC 대상 아님
let user = { name: "정훈" }; // user → 객체 (도달 가능)
// 도달 불가 → GC 대상
user = null; // 더 이상 객체에 접근할 수 없음
V8의 세대별 GC
Minor GC (Scavenge) — Young Generation (새 객체)
→ 짧은 수명의 객체를 빠르게 수거
→ 자주 실행, 짧은 일시정지
Major GC (Mark-Compact) — Old Generation (오래된 객체)
→ 살아남은 객체가 이동하는 곳
→ 드물게 실행, 긴 일시정지
메모리 누수 패턴
1. 의도치 않은 전역 변수
function createData() {
leakedVar = new Array(1000000); // var/let/const 없으면 전역!
}
// leakedVar는 영원히 메모리에 남음
2. 해제되지 않은 이벤트 리스너
// 메모리 누수
function setup() {
const data = new Array(1000000);
window.addEventListener("resize", () => {
console.log(data.length); // data에 대한 참조 유지 → 누수
});
}
// 해결: 리스너 정리
function setup() {
const data = new Array(1000000);
const controller = new AbortController();
window.addEventListener("resize", () => {
console.log(data.length);
}, { signal: controller.signal });
return () => controller.abort(); // 정리 함수
}
3. 해제되지 않은 타이머
// 메모리 누수
function startPolling() {
const data = fetchLargeData();
setInterval(() => {
process(data); // data 참조 유지
}, 1000);
}
// 해결: 타이머 정리
function startPolling() {
const data = fetchLargeData();
const id = setInterval(() => process(data), 1000);
return () => clearInterval(id);
}
4. 클로저에 의한 참조 유지
function createClosure() {
const hugeArray = new Array(1000000).fill("data");
return function () {
// hugeArray를 사용하지 않지만, 클로저 스코프에 있으므로 유지될 수 있음
return "hello";
};
}
5. DOM 참조
// DOM에서 제거했지만 JS 변수에 참조가 남아있는 경우
const elements = [];
function addElement() {
const div = document.createElement("div");
document.body.appendChild(div);
elements.push(div); // 배열에 참조 저장
}
function removeElements() {
document.body.innerHTML = ""; // DOM에서 제거
// elements 배열에는 여전히 참조가 남아있음 → 누수
elements.length = 0; // 이것도 해야 함
}
Chrome DevTools로 메모리 프로파일링
Heap Snapshot
1. DevTools → Memory 탭
2. "Take heap snapshot" 선택
3. 스냅샷 찍기
4. 의심되는 작업 수행
5. 다시 스냅샷 찍기
6. "Comparison" 뷰에서 차이 확인
Allocation Timeline
1. Memory 탭 → "Allocation instrumentation on timeline"
2. 녹화 시작
3. 의심되는 작업 반복
4. 녹화 중지
5. 파란색 막대 = 아직 살아있는 할당 (잠재적 누수)
Performance Monitor
1. DevTools → Performance Monitor (Ctrl+Shift+P → Monitor)
2. "JS heap size" 확인
3. 계속 증가하면 → 메모리 누수 의심
WeakRef와 FinalizationRegistry
// WeakRef — GC를 방해하지 않는 약한 참조
let target = { data: "important" };
const weakRef = new WeakRef(target);
target = null; // GC 가능
// 나중에 확인
const obj = weakRef.deref();
if (obj) {
console.log(obj.data); // 아직 살아있으면 접근 가능
} else {
console.log("GC됨"); // 이미 수거됨
}
// FinalizationRegistry — GC 시 콜백
const registry = new FinalizationRegistry((value) => {
console.log(`${value}이(가) GC되었습니다.`);
});
let obj2 = { name: "test" };
registry.register(obj2, "obj2");
obj2 = null; // GC 시 "obj2이(가) GC되었습니다." 출력
메모리 최적화 팁
| 팁 | 설명 |
|---|---|
| 이벤트 리스너 정리 | AbortController 또는 removeEventListener |
| 타이머 정리 | clearInterval, clearTimeout |
| 대용량 데이터 참조 해제 | 사용 후 null 할당 |
| WeakMap/WeakSet 활용 | DOM 요소 관련 데이터 저장 |
| 클로저 범위 최소화 | 필요한 변수만 캡처 |
**기억하기 **: V8의 GC는 루트에서 도달 가능한 객체만 유지합니다. 메모리 누수는 "의도치 않게 참조를 유지하는 것"입니다. 이벤트 리스너, 타이머, DOM 참조를 정리하는 습관이 가장 중요합니다. Chrome DevTools의 Memory 탭으로 힙 스냅샷을 비교하면 누수를 찾을 수 있습니다.
댓글 로딩 중...