WeakMap
Baseline Widely available
This feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.
WeakMap
은 키/값 쌍의 모음으로, 키는 반드시 객체 또는 등록되지 않은 심볼이며 값은 임의의 JavaScript 타입입니다. WeakMap
은 키에 대한 강력한 참조를 생성하지 않으므로, 객체가 WeakMap
의 키에 포함되더라도 가비지 컬렉션의 대상이 됩니다. 키 객체가 가비지 컬렉터에 의해 정리된 후에는, 다른 곳에 강력한 참조가 존재하지 않는 한, 키에 연결됐던 값 또한 가비지 컬렉션 대상이 됩니다. WeakMap
의 키로 사용할 수 있는 유일한 원시 값은 심볼, 정확히는 등록되지 않은 심볼인데, 등록되지 않은 심볼은 고유함이 보장되며 다시 생성할 수 없기 때문입니다.
WeakMap
을 사용하면 값이 키를 참조하더라도 키 객체의 가비지 컬렉션을 방지하지 않는 방식으로 데이터를 객체에 연결할 수 있습니다. 그러나 WeakMap
은 키의 활성 상태를 관찰하는 것을 허용하지 않으며, 이는 WeakMap
의 키를 열거할 수 없는 이유입니다. 만약 WeakMap
이 키 목록을 얻을 수 있는 어떤 메서드를 제공했다면, 그 목록은 가비지 컬렉션 상태에 따라 달라질 것이므로 비결정성이 발생합니다. 키 목록이 필요하면 WeakMap
대신 Map
을 사용해야 합니다.
키 기반 컬렉션 안내서의 WeakMap 객체 구획에서 WeakMap
에 대해 자세히 알아볼 수 있습니다.
설명
WeakMap
의 키는 반드시 가비지 컬렉션 가능해야 합니다. 대부분의 원시 값은 임의로 생성될 수 있고 생명 주기가 따로 없으므로 키로 사용할 수 없습니다. 객체와 등록되지 않은 심볼은 가비지 컬렉션 대상이므로 키로 사용할 수 있습니다.
왜 WeakMap인가?
JavaScript의 맵 API는 4개의 API 메서드와 이들이 공유하는 두 개의 배열(키용 하나, 값용 하나)으로도 구현하려면 구현할 수 있습니다. 이 맵에 요소를 추가하면 키와 값을 동시에 각 배열의 끝으로 넣는 작업을 수행할 것입니다. 결과적으로, 키와 값이 두 배열 각각에서 같은 인덱스에 위치하게 됩니다. 맵에서 값을 가져오려면 키 배열을 순회하면서 일치하는 항목을 찾고, 이 항목의 인덱스로 값 배열에서 값을 가져올 것입니다.
이 구현에는 두 가지 큰 불편함이 있습니다.
- 값의 설정과 검색이
O(n)
(n은 맵 키의 수)입니다. 두 작업 모두 일치하는 값을 찾기 위해 키 목록을 순회해야 하기 때문입니다. - 메모리 누수입니다. 배열은 각 키와 각 값에 대한 참조가 무기한 유지되도록 보장하기 때문인데, 이 때문에 다른 곳에서 객체를 참조하지 않더라도 키가 가비지 컬렉션 대상이 되지 못합니다. 값도 마찬가지입니다.
반면, WeakMap
에서 키 객체는 가비지 컬렉션 되지 않는 한 자신의 값을 강력하게 참조하고, 그 후에는 약하게 참조합니다. 따라서 WeakMap
은 다음과 같습니다.
- 가비지 컬렉션을 방지하지 않으므로 키 객체에 대한 참조가 결국 사라집니다.
- 키 객체가
WeakMap
밖의 다른 곳에서 참조되지 않으면 그 값의 가비지 컬렉션도 허용합니다.
WeakMap
은 가비지 컬렉션 되지 않아야만 유용한 키에 정보를 매핑할 때 특히 유용한 구조입니다.
그러나 WeakMap
은 키의 활성 상태를 관찰하는 것을 허용하지 않으므로 키를 열거할 수 없습니다. 키 목록을 얻을 수 있는 방법은 존재하지 않습니다. 그런 방법이 존재했다면, 키 목록은 가비지 컬렉션 상태에 따라 달라질 것이므로 비결정성이 발생합니다. 키 목록이 필요하면 Map
을 사용해야 합니다.
생성자
WeakMap()
-
새로운
WeakMap
객체를 생성합니다.
인스턴스 속성
이 속성들은 WeakMap.prototype
에 정의되며 모든 WeakMap
인스턴스가 공유합니다.
WeakMap.prototype.constructor
-
인스턴스 객체를 생성했던 생성자 함수입니다.
WeakMap
인스턴스에서의 초기 값은WeakMap
생성자입니다. WeakMap.prototype[@@toStringTag]
-
@@toStringTag
속성의 초기 값은 문자열"WeakMap"
입니다. 이 속성은Object.prototype.toString()
에서 사용합니다.
인스턴스 메서드
WeakMap.prototype.delete()
-
key
에 연결된 값을 제거합니다. 이후WeakMap.prototype.has(key)
는false
를 반환할 것입니다. WeakMap.prototype.get()
-
key
에 연결된 값을 반환합니다. 값이 없으면undefined
를 반환합니다. WeakMap.prototype.has()
-
key
에 연결된 값이WeakMap
객체에 존재하는지 나타내는 불리언 값을 반환합니다. WeakMap.prototype.set()
-
WeakMap
객체의key
에value
를 연결합니다.WeakMap
객체를 반환합니다.
예제
WeakMap 사용하기
const wm1 = new WeakMap();
const wm2 = new WeakMap();
const wm3 = new WeakMap();
const o1 = {};
const o2 = function () {};
const o3 = window;
wm1.set(o1, 37);
wm1.set(o2, "azerty");
wm2.set(o1, o2); // 값은 함수와 객체를 포함해 아무거나 가능
wm2.set(o3, undefined);
wm2.set(wm1, wm2); // 키는 아무 객체나 가능. 다른 WeakMap도 가능합니다!
wm1.get(o2); // "azerty"
wm2.get(o2); // undefined, wm2에는 o2에 연결된 값이 없음
wm2.get(o3); // undefined, o3에 undefined를 연결함
wm1.has(o2); // true
wm2.has(o2); // false
wm2.has(o3); // true (값이 undefined지만)
wm3.set(o1, 37);
wm3.get(o1); // 37
wm1.has(o1); // true
wm1.delete(o1);
wm1.has(o1); // false
WeakMap과 유사하지만 .clear() 메서드를 가진 클래스 구현
class ClearableWeakMap {
#wm;
constructor(init) {
this.#wm = new WeakMap(init);
}
clear() {
this.#wm = new WeakMap();
}
delete(k) {
return this.#wm.delete(k);
}
get(k) {
return this.#wm.get(k);
}
has(k) {
return this.#wm.has(k);
}
set(k, v) {
this.#wm.set(k, v);
return this;
}
}
비공개 멤버 흉내내기
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.showPublic = function () {
return this.someProperty;
};
Thing.prototype.showPrivate = function () {
return privateScope.get(this).hidden;
};
}
console.log(typeof privateScope);
// "undefined"
const thing = new Thing();
console.log(thing);
// Thing {someProperty: "foo"}
thing.showPublic();
// "foo"
thing.showPrivate();
// 1
위 코드는 비공개 필드를 사용하는 아래 예제와 동일하다고 볼 수 있습니다.
class Thing {
static #counter = 0;
#hidden;
constructor() {
this.someProperty = "foo";
this.#hidden = ++Thing.#counter;
}
showPublic() {
return this.someProperty;
}
showPrivate() {
return this.#hidden;
}
}
console.log(thing);
// Thing {someProperty: "foo"}
thing.showPublic();
// "foo"
thing.showPrivate();
// 1
메타데이터 연결하기
WeakMap
을 사용하면 객체의 생명 주기에 영향을 주지 않고도 메타데이터를 연결할 수 있습니다. 비공개 멤버 예제하고도 굉장히 비슷한데, 비공개 멤버 또한 프로토타입 상속에 참여하지 않는 외부 메타데이터로 설계됐기 때문입니다.
하지만 이 방법은 이미 생성된 객체들을 대상으로도 확장할 수 있습니다. 예를 들어 웹에서는, DOM 요소에 추가 데이터를 연결했다가 나중에 접근하고 싶은 경우가 생길 수 있습니다. 흔히 쓰이는 방법은 데이터를 요소의 속성으로 붙이는 것입니다.
const buttons = document.querySelectorAll(".button");
buttons.forEach((button) => {
button.clicked = false;
button.addEventListener("click", () => {
button.clicked = true;
const currentButtons = [...document.querySelectorAll(".button")];
if (currentButtons.every((button) => button.clicked)) {
console.log("모든 버튼을 클릭했습니다!");
}
});
});
위 방법도 동작하지만, 몇 가지 문제가 있습니다.
clicked
속성이 열거 가능하므로Object.keys(button)
,for...in
반복문 등에 노출됩니다.Object.defineProperty()
를 사용하면 대응할 수 있지만, 대신 코드가 더 시끄러워집니다.clicked
속성은 일반 문자열 속성이므로 다른 코드에서 접근하거나 덮어쓸 수 있습니다.Symbol
키를 사용해 대응할 수 있지만, 그래도Object.getOwnPropertySymbols()
로 접근할 수 있습니다.
WeakMap
을 사용하면 모두 고칠 수 있습니다.
const buttons = document.querySelectorAll(".button");
const clicked = new WeakMap();
buttons.forEach((button) => {
clicked.set(button, false);
button.addEventListener("click", () => {
clicked.set(button, true);
const currentButtons = [...document.querySelectorAll(".button")];
if (currentButtons.every((button) => clicked.get(button))) {
console.log("모든 버튼을 클릭했습니다!");
}
});
});
여기서는 clicked
에 접근할 수 있는 코드만 각 버튼의 클릭 상태를 알 수 있으므로 외부 코드는 이 상태를 바꿀 수 없습니다. 또한 버튼이 DOM에서 제거되면 연관됐던 메타데이터도 가비지 컬렉터가 자동으로 정리합니다.
캐싱
함수에 전달했던 매개변수와 그 함수의 결과를 연관지으면, 같은 매개변수를 다시 받았을 때 함수를 다시 실행하지 않고 캐시된 결과를 반환할 수 있습니다. 이 방법은 함수가 순수(외부 객체를 조작하거나, 기타 관측 가능한 부작용을 유발하지 않음)할 때 유용합니다.
const cache = new WeakMap();
function handleObjectValues(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}
const result = Object.values(obj).map(heavyComputation);
cache.set(obj, result);
return result;
}
하지만 함수가 한 객체를 받을 때만 사용할 수 있다는 점을 주의해야 합니다. 또한 입력했던 객체를 함수에 다시 전달할 일이 영원히 없다고 해도, 결과는 캐시에 계속 남아있게 됩니다. 더 효율적인 방법은 Map
과 WeakRef
객체를 사용해서, 임의 타입의 입력 값을 그에 대응하는 (아마도 매우 큰) 계산 결과와 연관짓는 것입니다. 자세한 내용은 WeakRef와 FinalizationRegistry 예제를 참고하세요.
명세서
Specification |
---|
ECMAScript Language Specification # sec-weakmap-objects |
브라우저 호환성
BCD tables only load in the browser