자바스크립트는 가비지 컬렉션(GC)이 자동으로 메모리를 관리하지만, 개발자가 메모리 누수를 만들 수 있습니다. GC의 동작 원리를 이해하면 메모리 누수를 예방하고 디버깅할 수 있습니다.

메모리 수명주기

PLAINTEXT
1. 할당 (Allocation) → 변수 선언, 객체 생성
2. 사용 (Use) → 읽기, 쓰기
3. 해제 (Release) → GC가 자동으로 처리
JS
// 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을 포함한 현대 엔진이 사용하는 기본 알고리즘입니다.

PLAINTEXT
1. 루트(전역 객체, 스택)에서 시작
2. 루트에서 도달 가능한 모든 객체를 "표시"
3. 표시되지 않은 객체의 메모리를 해제
JS
// 도달 가능 → GC 대상 아님
let user = { name: "정훈" }; // user → 객체 (도달 가능)

// 도달 불가 → GC 대상
user = null; // 더 이상 객체에 접근할 수 없음

V8의 세대별 GC

PLAINTEXT
Minor GC (Scavenge) — Young Generation (새 객체)
  → 짧은 수명의 객체를 빠르게 수거
  → 자주 실행, 짧은 일시정지

Major GC (Mark-Compact) — Old Generation (오래된 객체)
  → 살아남은 객체가 이동하는 곳
  → 드물게 실행, 긴 일시정지

메모리 누수 패턴

1. 의도치 않은 전역 변수

JS
function createData() {
  leakedVar = new Array(1000000); // var/let/const 없으면 전역!
}
// leakedVar는 영원히 메모리에 남음

2. 해제되지 않은 이벤트 리스너

JS
// 메모리 누수
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. 해제되지 않은 타이머

JS
// 메모리 누수
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. 클로저에 의한 참조 유지

JS
function createClosure() {
  const hugeArray = new Array(1000000).fill("data");

  return function () {
    // hugeArray를 사용하지 않지만, 클로저 스코프에 있으므로 유지될 수 있음
    return "hello";
  };
}

5. DOM 참조

JS
// 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

PLAINTEXT
1. DevTools → Memory 탭
2. "Take heap snapshot" 선택
3. 스냅샷 찍기
4. 의심되는 작업 수행
5. 다시 스냅샷 찍기
6. "Comparison" 뷰에서 차이 확인

Allocation Timeline

PLAINTEXT
1. Memory 탭 → "Allocation instrumentation on timeline"
2. 녹화 시작
3. 의심되는 작업 반복
4. 녹화 중지
5. 파란색 막대 = 아직 살아있는 할당 (잠재적 누수)

Performance Monitor

PLAINTEXT
1. DevTools → Performance Monitor (Ctrl+Shift+P → Monitor)
2. "JS heap size" 확인
3. 계속 증가하면 → 메모리 누수 의심

WeakRef와 FinalizationRegistry

JS
// 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 탭으로 힙 스냅샷을 비교하면 누수를 찾을 수 있습니다.

댓글 로딩 중...