weakmap 약한 참조 알아보기
WeakMap
이란 객체를 키로 사용하며, 키로 사용된 객체가 다른 곳에서 참조되지 않을 때 자동으로 가비지 컬렉션 될 수 있는 자바스크립트의 자료구조 입니다.
객체와 연결된 데이터를 메모리에 안전하게 저장하고, 객체가 더 이상 필요하지 않을 때 자동으로 데이터를 정리하는 용도로 사용됩니다.
Map과 차이점
-
키는 객체만 사용할 수 있습니다. 문자열이나 숫자는 사용할 수 없습니다.
-
기존
Map
과는 다르게get
,set
,delete
,has
4개의 메소드만 사용할 수 있습니다.size
로 몇 개의 데이터를 가지고 있는지 확인하거나 키를 순회하는 등을 위해서는 객체가 계속 메모리를 차지(강한 참조)하고 있어야 하지만Weakmap
은 약한 참조를 사용하기 때문입니다.
const weakMap = new WeakMap();
let obj1 = { key: "value" };
let obj2 = { key: "value" };
weakMap.set(obj1, "value1");
weakMap.set(obj2, "value2");
console.log(weakMap.get(obj1)); // "value1"
console.log(weakMap.has(obj2)); // true
obj2 = null;
console.log(weakMap.has(obj2)); // false
weakMap.delete(obj1);
weakMap.has(obj1); // false
console.log(obj1); //{key: 'value'}
약한 참조
약한 참조란, 해당 객체가 다른 곳에서 더 이상 사용되지 않는다면 가비지 컬렉터에 의해 자동으로 메모리에서 제거될 수 있도록 하는 참조입니다.
일반적인 객체나 Map
에 저장한 경우, 더 이상 데이터에 접근할 수 없다고 해도 가비지 컬렉션 대상이 되지 않고 메모리에 남아있게 됩니다.
let data = { name: "hello" };
const map = new Map();
map.set(data, 1);
data = null;
console.log(data); // null
console.log(map); // Map(1) { { name: 'hello' } => 1 } (데이터가 남아있음)
이는 사용하지 않는 데이터이지만, 메모리에는 존재하게 되어 메모리 누수 문제를 일으킬 수 있습니다.
언제 WeakMap
을 사용할까?
실제로 WeakMap
을 사용하는 사례를 보면서, 언제/왜 사용하는지 알아보겠습니다.
메모리 누수 방지
카드 UI를 DOM에 추가하고, 추가 데이터를 Map
에 저장하는 예제 입니다.
DOM에서는 제거 되었지만, Map
에는 데이터가 남아 메모리 누수 문제가 있습니다.
직접 돔을 추가하고, 제거하면서 크롬 개발자 도구를 통해 메모리 사용량을 확인해보겠습니다.
메모리 사용량을 직관적으로 볼 수 있도록, 카드 만들 때 10MB 크기의 Uint8Array
를 같이 사용했습니다.
- “Create Profile Card” 버튼을 클릭하면 카드를 추가하고, Map에 객체 정보를 추가합니다.
- “Close” 버튼을 누르면 카드를 제거합니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Profile Card Generator</title>
</head>
<body>
<button id="createCard">Create Profile Card</button>
<script>
const profileDataMap = new Map();
function createProfileCard(userData) {
const card = document.createElement("div");
card.className = "profile-card";
card.innerHTML = `
<h3>${userData.name}</h3>
<p>${userData.description}</p>
<button class="close-btn">Close</button>
`;
profileDataMap.set(card, userData);
card.querySelector(".close-btn").addEventListener("click", function () {
card.remove();
});
document.body.appendChild(card);
}
document
.getElementById("createCard")
.addEventListener("click", function () {
const names = ["John Doe", "Jane Smith"];
const descriptions = ["Front-end Developer", "Back-end Specialist"];
const index = Math.floor(Math.random() * names.length);
const buffer = new Uint8Array(10 * 1024 * 1024); // 10MB
createProfileCard({
name: names[index],
description: descriptions[index],
buffer,
});
});
</script>
</body>
</html>
Map 메모리 사용량 확인해보기
카드를 1개 만들었을 때, 예상한대로 Map의 크기는 약 10MB입니다.
추가로 카드를 1개 더 만들었을 떄 Map의 크기는 약 21MB입니다.
정상적으로 메모리에 할당 된 것 같으니, 이제 DOM을 삭제해보겠습니다.
화면상으로는 카드가 사라지고, DOM에서 해제 되었지만, profileDataMap에서 데이터를 삭제하는 코드가 없기 때문에 profileDataMap에는 userData가 남아있어 메모리 사용량이 변하지 않았습니다.
즉, Map은 강한 참조를 가지고 있어서 key
인 card
가 삭제되었어도 profileDataMap
에는 데이터가 그대로 남아 있습니다.
WeakMap 메모리 사용량 확인해보기
이번에는 WeakMap
으로 동일하게 테스트 해보겠습니다. 11번 라인에 있는 Map()
만 WeakMap()
으로 바꿔주었습니다. 이번에는 메모리 사용량만 바로 확인해보겠습니다.
- 카드 1개 추가 (WeakMap 크기 약 10MB)
- 카드 1개 추가 (WeakMap 크기 약 21MB)
- 카드 1개 추가 (WeakMap 크기 약 31MB)
- 카드 1개 제거 (WeakMap 크기 약 21MB)
- 카드 1개 제거 (WeakMap 크기 약 10MB)
WeakMap
은 약한 참조를 가지고 있어서, 데이터를 삭제하는 코드가 없어도 key
인 card
가 삭제되면 WeakMap
의 데이토 같이 회수되는 것을 확인할 수 있습니다.
Dom
에 추가적인 데이터를 저장할 때 Dom
이 삭제되면 데이터가 같이 삭제되기를 원한다면 WeakMap
을 사용할 수 있습니다.
Map 메모리 확인해보기
메모리 사용량 뿐 아니라, 개발자 도구의 메모리 탭에서 실제로 변수가 할당되는 것도 확인할 수 있습니다.
카드 UI를 생성해 총 5개의 변수를 할당했습니다. 이제 삭제해보면서 메모리를 확인해보면,
UI 자체는 돔에서 제거되었지만 Map
에 강한 참조로 연결되어 있기 때문에, 카드 UI를 삭제했음에도 메모리에 변수가 남아있는것을 볼 수 있습니다.
WeakMap 메모리 확인해보기
먼저 카드 UI를 5개 생성했을 때의 메모리를 보면,
이후 3개 카드 UI를 삭제해보면 그만큼 삭제되어 메모리에는 2개의 값만 남아 있습니다.
반면 WeakMap
의 경우에는 약한 참조를 사용하기 때문에, 돔에서 카드 UI가 제거되면 더 이상 메모리에 값이 남아있자 않습니다.
캐시
key
로 지정한 객체가 더 이상 참조되지 않게 되면 메모리가 해제 되는 특성으로 캐싱시에 유용합니다.
let cache = new WeakMap();
function computeSumWithCache(obj) {
if (!cache.has(obj)) {
console.log("계산");
const sum = obj.a + obj.b + obj.c;
cache.set(obj, sum);
}
console.log("캐시 반환");
return cache.get(obj);
}
let obj = {
a: 1,
b: 2,
c: 3,
};
// 계산
// 캐시 반환
let result1 = computeSumWithCache(obj);
// 캐시 반환
let result2 = computeSumWithCache(obj);
obj = null;
// obj가 null 처리되면 GC에 의해 WeakMap에서 해당 엔트리 자동 제거됨
lodash의 memorize 함수에서도 WeakMap
을 사용할 수 있습니다.
_.memoize.Cache = WeakMap;
Vue
DOM이 삭제되었을 때 참조를 해제하지 않아도 메모리 누수를 방지할 수 있다는 점에서 Vue 구현에서도 사용된다고 합니다.
Vue에서 반응형 작동 방식 에서는 반응형 객체의 속성 접근을 추적하기 위해 전역 WeakMap을 사용해 객체별 의존성 맵을 관리한다고 설명합니다.
이 WeakMap은 객체를 키로 사용하므로 참조가 끊기면 자동으로 GC 대상이 되어 메모리 누수를 방지할 수 있기 때문에, Proxy로 감지하고 WeakMap으로 추적해 메모리 효율성과 정확한 이펙트 실행이 가능하다고 합니다.