WeakMap
WeakMap
WeakMap은 키가 객체 또는 비등록 심볼(non-registered symbol)이어야 하고 값은 임의의 JavaScript 타입이 될 수 있는 키-값 쌍의 컬렉션입니다. 키에 강한 참조를 만들지 않아 키 객체가 가비지 컬렉션의 대상이 될 수 있습니다.
본문
WeakMap은 키가 객체 또는 비등록 심볼이어야 하고 값이 임의의 JavaScript 타입이 될 수 있는 키-값 쌍의 컬렉션으로, 키에 강한 참조를 만들지 않습니다. 즉, 객체가 WeakMap의 키로 존재한다고 해서 그 객체가 가비지 컬렉션되는 것을 막지 못합니다. 키로 사용된 객체가 수집되면, 약하게 참조된 해당 값들도 다른 곳에서 강하게 참조되지 않는 한 가비지 컬렉션의 후보가 됩니다.
WeakMap은 값이 키를 참조하더라도 키 객체가 수집되는 것을 막지 않는 방식으로 데이터를 객체에 연결할 수 있게 해줍니다. 그러나 WeakMap은 키의 살아있는지 여부(liveness)를 관찰하는 것을 허용하지 않으므로 열거(enumeration)를 허용하지 않습니다. 만약 WeakMap이 키 목록을 얻는 메서드를 노출한다면, 그 목록은 가비지 컬렉션 상태에 의존하게 되어 비결정적(non-determinism)인 동작을 만들기 때문입니다. 키 목록이 필요하다면 Map을 사용해야 합니다.
WeakMap 키
WeakMap의 키는 가비지 컬렉팅 가능해야 합니다. 대부분의 프리미티브 데이터 타입은 임의로 생성될 수 있고 수명(lifetime)이 없으므로 키로 사용할 수 없습니다. 객체와 비등록 심볼은 가비지 컬렉팅 가능하므로 키로 사용할 수 있습니다.
일반 Map처럼 값 동등성은 SameValueZero 알고리즘에 기반합니다. WeakMap은 객체와 심볼 키만 담을 수 있으므로 === 연산자와 같습니다. 객체 키의 경우 동등성은 객체 정체성에 기반하며, 값이 아니라 참조로 비교합니다.
왜 WeakMap이 유용한가
Map API를 두 개의 배열(키용 하나, 값용 하나)로 JavaScript에서 구현할 수 있습니다. 하지만 이 방식은 두 가지 불편함이 있습니다. 첫째, 매칭을 찾기 위해 키 목록을 반복해야 해서 O(n)의 집합·검색 비용이 듭니다. 둘째, 배열이 각 키와 값에 대한 참조를 무기한 유지하므로 메모리 누수가 발생합니다. 다른 참조가 없어도 키가 가비지 컬렉션되지 않게 막고, 해당 값들이 수집되는 것도 막습니다.
반면 WeakMap에서는 키 객체가 수집되지 않는 한 그 내용을 강하게 참조하지만, 그 이후에는 약하게 참조합니다. 따라서 WeakMap은:
- 키 객체에 대한 참조를 결국 제거하는 가비지 컬렉션을 막지 않고,
- 키 객체가
WeakMap이외의 곳에서 참조되지 않는다면 해당 값들의 가비지 컬렉션을 허용합니다.
WeakMap은 키가 가비지 컬렉팅되지 않은 경우에만 유용한 키에 대한 정보를 매핑할 때 특히 유용한 구조입니다.
생성자
WeakMap()— 새WeakMap객체를 만듭니다.
인스턴스 속성
WeakMap.prototype에 정의되어 모든 인스턴스가 공유하는 속성: constructor, [Symbol.toStringTag](초기 값은 "WeakMap").
인스턴스 메서드
WeakMap.prototype.delete()— 키로 지정된 항목을 이WeakMap에서 제거합니다.WeakMap.prototype.get()— 이WeakMap에서 키에 해당하는 값을 반환하거나, 없으면undefined를 반환합니다.WeakMap.prototype.getOrInsert()— 지정된 키에 해당하는 값을 반환합니다. 키가 없으면 주어진 기본 값으로 새 항목을 삽입하고 삽입된 값을 반환합니다.WeakMap.prototype.getOrInsertComputed()— 지정된 키에 해당하는 값을 반환합니다. 키가 없으면 주어진 콜백으로 계산한 기본 값으로 새 항목을 삽입하고 그 값을 반환합니다.WeakMap.prototype.has()— 지정된 키의 항목이 존재하는지 여부를 나타내는 불리언을 반환합니다.WeakMap.prototype.set()— 지정된 키와 값으로 새 항목을 이WeakMap에 추가하거나, 키가 이미 존재하면 기존 항목을 갱신합니다.
예제
WeakMap 사용 — 키로 객체, 함수, WeakMap 자체까지 모든 객체를 사용할 수 있고, 값으로도 객체·함수·undefined 등 무엇이든 될 수 있습니다.
const wm1 = new WeakMap();
const wm2 = new WeakMap();
const o1 = {};
const o2 = () => {};
wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // 값은 무엇이든 될 수 있음 (객체, 함수 포함)
wm2.set(wm1, wm2); // 키와 값은 어떤 객체든 될 수 있음. 심지어 WeakMap도!
wm1.get(o2); // "azerty"
wm2.has(o2); // true
wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false
비공개 멤버(private members) 흉내 내기 — 개발자는 WeakMap으로 프라이빗 데이터를 객체에 연결할 수 있으며 다음과 같은 장점이 있습니다.
Map과 비교해WeakMap은 키로 사용된 객체에 강한 참조를 유지하지 않으므로 메타데이터가 객체 자체와 같은 수명을 공유하여 메모리 누수를 피합니다.- 비열거형 및/또는
Symbol속성 사용과 비교해WeakMap은 객체 외부에 존재하므로Object.getOwnPropertySymbols같은 반영 메서드로 메타데이터를 검색할 방법이 없습니다. - 클로저와 비교해 같은
WeakMap을 생성자에서 만든 모든 인스턴스에 재사용할 수 있어 메모리 효율적이며, 같은 클래스의 서로 다른 인스턴스가 서로의 비공개 멤버를 읽을 수 있습니다.
let Thing;
{
const privateScope = new WeakMap();
let counter = 0;
Thing = function () {
this.someProperty = "foo";
privateScope.set(this, { hidden: ++counter });
};
Thing.prototype.showPrivate = function () {
return privateScope.get(this).hidden;
};
}
const thing = new Thing();
thing.showPrivate(); // 1
이는 프라이빗 필드를 사용한 아래 방식과 거의 동일합니다.
class Thing {
static #counter = 0;
#hidden;
constructor() {
this.someProperty = "foo";
this.#hidden = ++Thing.#counter;
}
showPrivate() {
return this.#hidden;
}
}
또한 WeakMap은 객체에 메타데이터를 연결하되 객체 자체의 수명에는 영향을 주지 않는 데 사용됩니다. 버튼이 클릭되었는지 여부를 WeakMap에 저장하면, 버튼이 DOM에서 제거될 때 관련 메타데이터가 자동으로 가비지 컬렉팅됩니다.
더 알아보기
Map— 키-값 쌍 컬렉션 (강한 참조, 열거 가능)WeakSet— 약한 참조 값을 가진 집합WeakRef— 객체의 약한 참조- JavaScript 가비지 컬렉션 안내서