WeakMap과 WeakSet
약한 참조로 동작하는 WeakMap과 WeakSet의 GC 연동 메커니즘과 private 데이터 저장·순환 참조 감지·DOM 메타데이터 등 실용 패턴을 설명합니다.
지난 글에서 Map과 Set을 살펴봤습니다. 이번 글에서는 이 두 컬렉션의 “약한” 버전인 WeakMap과 WeakSet을 다룹니다. 이름에서 알 수 있듯 키(WeakMap) 또는 원소(WeakSet)에 대한 **약한 참조(weak reference)**를 유지해 가비지 컬렉터가 자유롭게 수거할 수 있도록 허용합니다.
강한 참조 vs 약한 참조
일반 Map은 키 객체에 대한 강한 참조를 유지합니다. 다른 코드에서 해당 객체를 더 이상 참조하지 않더라도 Map이 살아있는 한 객체는 GC되지 않습니다.
const map = new Map();
let el = document.querySelector('#btn');
map.set(el, { clicks: 0 });
el = null; // DOM 참조 해제
// 하지만 map이 el을 강하게 잡고 있으므로 GC 불가 → 메모리 누수!
WeakMap은 키에 대한 약한 참조만 유지합니다. 키 객체가 다른 곳에서 더 이상 참조되지 않으면 GC가 수거할 수 있고, 항목도 자동으로 제거됩니다.
const weakMap = new WeakMap();
let el = document.querySelector('#btn');
weakMap.set(el, { clicks: 0 });
el = null; // 약한 참조이므로 GC가 수거 가능
// 수거 후 weakMap 항목도 자동 제거
WeakMap API
WeakMap은 Map에 비해 제한적인 API를 제공합니다. 순회가 불가능합니다 — 약한 참조 특성상 언제 항목이 사라질지 예측할 수 없기 때문입니다.
const wm = new WeakMap();
const key = {};
wm.set(key, 'value');
wm.get(key); // 'value'
wm.has(key); // true
wm.delete(key); // true
// 불가능한 작업
// wm.size — 없음
// wm.forEach — 없음
// for...of wm — TypeError
키는 반드시 객체 또는 등록된 Symbol이어야 합니다.
const wm = new WeakMap();
wm.set(42, 'x'); // TypeError: Invalid value used as weak map key
wm.set('str', 'x'); // TypeError
WeakSet API
WeakSet은 객체만 원소로 가질 수 있는 Set입니다. 마찬가지로 순회 불가능합니다.
const ws = new WeakSet();
const obj = {};
ws.add(obj);
ws.has(obj); // true
ws.delete(obj); // true
실용 패턴
1. WeakMap으로 private 데이터 저장
ES2020+ 클래스 private 필드(#)가 도입되기 전에 WeakMap으로 클래스 내부 데이터를 숨기는 패턴이 널리 쓰였습니다.
const _private = new WeakMap();
class BankAccount {
constructor(owner, balance) {
_private.set(this, { owner, balance });
}
deposit(amount) {
const data = _private.get(this);
data.balance += amount;
}
get balance() {
return _private.get(this).balance;
}
}
const acc = new BankAccount('Alice', 1000);
acc.balance; // 1000
// acc._private — undefined, 외부 접근 불가
인스턴스가 GC되면 WeakMap 항목도 함께 정리됩니다.
2. WeakSet으로 순환 참조 감지
깊은 복사(deep clone)나 직렬화 시 순환 참조를 감지하는 데 유용합니다.
function safeStringify(obj, visited = new WeakSet()) {
if (typeof obj !== 'object' || obj === null) return JSON.stringify(obj);
if (visited.has(obj)) return '"[Circular]"';
visited.add(obj);
const pairs = Object.entries(obj).map(
([k, v]) => `"${k}":${safeStringify(v, visited)}`
);
return `{${pairs.join(',')}}`;
}
const a = { x: 1 };
a.self = a; // 순환 참조
safeStringify(a); // '{"x":1,"self":"[Circular]"}'
3. DOM 노드에 메타데이터 연결
DOM 노드의 라이프사이클에 자동으로 연동되는 메타데이터 저장소를 만들 수 있습니다.
const nodeData = new WeakMap();
function registerNode(el, meta) {
nodeData.set(el, meta);
}
function getNodeMeta(el) {
return nodeData.get(el);
}
// DOM에서 노드가 제거되고 참조가 없어지면
// nodeData 항목도 자동으로 정리됨
4. 메모이제이션 (WeakMap 기반)
객체 인자를 캐시 키로 사용하는 안전한 메모이제이션입니다.
const cache = new WeakMap();
function memoize(fn) {
return function (obj) {
if (!cache.has(obj)) cache.set(obj, fn(obj));
return cache.get(obj);
};
}
const processUser = memoize(user => {
// 비용이 큰 계산...
return { ...user, processed: true };
});
WeakRef와의 차이
WeakRef(ES2021)는 약한 참조를 명시적으로 다루지만, WeakMap/WeakSet은 약한 참조를 내부적으로 관리합니다. 대부분의 사용 사례에서는 WeakMap/WeakSet으로 충분합니다.
언제 Map 대신 WeakMap을 쓸까?
- 키가 외부 객체(DOM 노드, 클래스 인스턴스)이고
- 그 객체의 라이프사이클에 데이터를 자동으로 연동시키고 싶을 때
반면 키를 직접 관리하거나, 컬렉션을 순회하거나, 크기를 알아야 한다면 Map을 사용합니다.
다음 글에서는 Symbol을 실제 코드에 활용하는 패턴 — well-known Symbol, 커스텀 Symbol, 태깅 등을 다룹니다.
지난 글: Map과 Set
다음 글: Symbol 활용 패턴
읽어주셔서 감사합니다. 😊