홈으로 돌아가기

JS 클로저의 메모리 누수: 수정 방법

이 글은 JavaScript 클로저의 메모리 누수 메커니즘을 코드 예제와 DevTools를 통해 분석합니다. 설명된 해결책: 참조 초기화, 핸들러 제거, 약한 참조를 사용한 WeakMap. 실전 진단 지침.

JS 클로저의 메모리 누수: WeakMap 및 해결책
Advertisement 728x90

JavaScript 클로저에서 메모리 누수 방지하는 법

JavaScript 클로저는 클로저 함수에 대한 참조가 있는 한 메모리에서 객체를 계속 유지합니다. 객체가 더 이상 필요 없는데도 클로저에 캡처되어 있어 가비지 컬렉터가 이를 해제하지 못하면 누수가 발생합니다.

대형 객체가 포함된 전형적인 사례를 보죠:

let bigData = {
  id: 42,
  data: new Array(1000000).fill('Some data')
}

function handler(data) {
  return function() {
    console.log(data.id);
    debugger;
  }
}

const runHandler = handler(bigData);
bigData = null;
runHandler();

내부 함수는 data.id만 사용하지만, 클로저는 전체 bigData 객체를 캡처합니다. DevTools에서 브레이크포인트에서 객체가 여전히 메모리에 남아 있는 걸 확인할 수 있습니다.

Google AdInline article slot

클로저 내 eval()의 문제점

eval() 함수는 사용 여부와 상관없이 외부 변수 전체를 클로저에 추가해 누수를 악화시킵니다:

let hello = 'Hello';
let userName = 'Vasya';

setTimeout(function () {
  debugger;
  return eval('console.log("Hello from Vasya!")');
}, 1000);

DevTools에서 클로저에 hellouserName이 포함되어 있는 걸 볼 수 있습니다. eval() 내부 코드가 불확실하기 때문입니다. 프로덕션에서는 eval()을 피하세요.

누수 방지 방법

캡처 데이터 강제 초기화

사용 후 캡처된 데이터를 초기화하세요:

Google AdInline article slot
function runCalc(buttonId) {
  let bigData = new Array(1000000).fill('Something');
  const button = document.getElementById(buttonId);

  button.addEventListener('click', function onClick() {
    console.log('Processed items:', bigData.length);
    bigData = null;
  });
}

null 할당으로 클로저 내 참조를 해제해 가비지 컬렉터가 객체를 제거할 수 있게 합니다.

이벤트 핸들러 제대로 제거하기

DOM 요소 제거 시 이벤트 핸들러가 흔한 누수 원인입니다:

function clickSetup(btn, message) {
  const bigData = new Array(1000000).fill(message);

  function handler() {
    console.log(message, bigData.length);
    btn.remove();
    btn.removeEventListener('click', handler);
  }

  btn.addEventListener('click', handler);
}

이름이 붙은 함수 handler는 요소와 함께 제거할 수 있습니다. 익명 화살표 함수는 이렇게 제거할 수 없습니다.

Google AdInline article slot

약한 참조를 위한 WeakMap

WeakMap은 객체 키에 대한 약한 참조를 사용해 대형 객체 캡처 문제를 해결합니다.

키는 반드시 객체여야 함

const weak = new WeakMap();
const key = {};
weak.set(key, 'Vasya was here!');
console.log(weak.get(key));
const anotherKey = {};
console.log(weak.get(anotherKey)); // undefined

키는 객체 또는 비원시형이어야 합니다. 참조로 get(key) 접근.

약한 참조와 가비지 컬렉션

WeakMap의 참조는 가비지 컬렉션을 막지 않습니다:

const weak = new WeakMap();
let userKey1 = {id: 1};
weak.set(userKey1, 'Vasya');
userKey1 = null;

userKey1을 null로 만든 후 객체와 값이 가비지 컬렉트됩니다.

반복자 없음

size, forEach, keys() 없음. 기본 메서드만:

  • set(key, value) — 설정
  • get(key) — 가져오기
  • has(key) — 확인
  • delete(key) — 삭제

가비지 컬렉션 중 예측 불가능성을 피하기 위해 반복자 없음.

클로저에서 WeakMap 사용

WeakMap이 포함된 클로저는 객체를 강하게 유지하지 않습니다:

function createProcessor() {
  const weakMap = new WeakMap();

  return function process(obj) {
    weakMap.set(obj, obj.value);
    debugger;
    return weakMap.get(obj);
  }
}

const processor = createProcessor();

let bigData = {
  value: 21,
  hugeArray: new Array(1000000).fill('*')
};

console.log(processor(bigData));
bigData = null;

let bigData2 = {
  value: 42,
  hugeArray: new Array(1000000).fill('1')
};

console.log(processor(bigData2));

DevTools에서 가비지 컬렉션 후 bigData2만 남아 있습니다. WeakMap으로 자동 메모리 정리 가능.

확인 방법:

  • process()에 브레이크포인트 설정.
  • 코드 단계별 실행 (F10).
  • bigData = null 후 Memory 탭으로 이동.
  • 버튼으로 가비지 컬렉션 트리거.
  • 계속 — 현재 키만 남음.

주요 요점

  • 클로저는 한 속성만 사용해도 전체 객체를 캡처.
  • eval()은 외부 변수 전체를 클로저에 추가.
  • 이름 붙은 이벤트 핸들러는 removeEventListener로 제거 가능.
  • WeakMap은 객체 키에 약한 참조 사용.
  • WeakMap에 반복자 없어 가비지 컬렉션 예측 가능.

— Editorial Team

Advertisement 728x90

다음 읽기