← 글 목록

notes

WeakRef · FinalizationRegistry

WeakMap 옆의 WeakRef/FinalizationRegistry. deref는 없을 수 있음을 전제로 설계한다.

참고: MDN WeakRef / FinalizationRegistry, TC39 WeakRefs proposal 요약

Deep Dive 37장(WeakMap/WeakSet) 옆에 붙이는 약한 손잡이 메모.

“WeakMap은 알아. 근데 WeakRef는 뭐야?”

캐시 만들다가 메모리 누수 이슈를 듣고 찾아봤다.

WeakMap은 키만 약하게 붙잡는 컬렉션이고, WeakRef는 객체 하나를 약하게 가리키는 손잡이다.

이름에 Weak가 들어가니 “GC가 알아서 해주겠지” 하고 넘어갔다.

MDN을 보니 경고가 먼저다. 타이밍을 믿지 마라. deref가 갑자기 undefined일 수 있다.

이게 무슨 말이냐.

강한 참조(변수, 프로퍼티, 클로저가 붙잡은 값)가 있으면 객체는 안 죽는다.

강한 참조가 모두 사라지면 GC 후보가 된다.

WeakRef는 “아직 살아 있으면 꺼내 보고, 없으면 undefined” — 있을 수도 없을 수도다.

flowchart LR
  S[강한 참조<br/>변수/프로퍼티] --> O[대상 객체]
  W[WeakRef] -.->|deref| O
  O -->|강한 참조 0| GC[GC 가능]
  FR[FinalizationRegistry] -->|거둔 뒤 콜백| CB[정리 시도]

최소 예제

let obj = { heavy: new Array(1e5).fill(0) };
const ref = new WeakRef(obj);

console.log(ref.deref()?.heavy.length); // 살아 있으면 숫자

obj = null; // 강한 참조 해제 (즉시 GC란 보장 없음)

// 언젠가…
const again = ref.deref();
if (again) {
  // 아직 있음 — 보너스
} else {
  // 이미 거둬짐 — 정상 경로로 설계해야 함
}

이게 무슨 말이냐.

obj = null 했다고 다음 줄에 deref가 undefined라는 보장은 없다.

테스트에서 “가끔 통과”가 나오는 이유다. 그래서 WeakRef로 필수 로직을 짜면 안 된다.

FinalizationRegistry

객체가 GC로 거둬진 뒤에 콜백을 받고 싶을 때.

다만 콜백 시점·횟수·프로세스 종료 직전 동작은 구현 의존이다.

“파일 핸들을 여기서 꼭 닫아야지” 같은 필수 자원 해제에 쓰면 위험하다. 명시적 dispose가 먼저다.

const registry = new FinalizationRegistry((token) => {
  console.log('cleanup hint for', token);
});

let data = { id: 1 };
registry.register(data, data.id);
data = null;

WeakMap이랑 언제 나누냐

  • DOM 노드 → 메타데이터: 대개 WeakMap (키 수명 = 노드 수명, 열거 불필요)
  • “객체 하나”를 캐시 키처럼 약하게 붙잡고 가끔 꺼내 보기: WeakRef 검토
  • 값이 객체이고 키도 약해야 하면 WeakMap / WeakSet이 더 직관적

실무에서 내가 정한 선

  • 먼저 강한 참조 그래프를 줄인다 (클로저·전역·배열에 쌓기).
  • 그래도 캐시가 필요하면 WeakMap을 먼저 본다.
  • WeakRef/FinalizationRegistry는 힌트·통계·옵션 캐시 수준.
  • deref 결과는 항상 optional로 본다.

질문 → 자답

Q. WeakRef로 연결 리스트 짜면?

A. 하지 않는다. 노드가 언제 사라질지 모르면 자료구조가 아니다.

Q. 테스트에서 GC를 강제할 수 있나?

A. 환경마다 다르고, 스펙이 보장하지 않는다. 테스트 설계를 GC에 기대지 말 것.

여기서 잠깐 💁‍♂️

약한 참조는 “메모리 마법”이 아니라 수명에 기생하지 않기 위한 도구다.

없을 수 있음을 전제하지 않는 코드에 꽂으면, 그게 바로 버그 생산기다.