자바스크립트의 가비지 컬렉터는 도달 가능한(reachable) 값만은 메모리에 유지한다. 도달 가능하지 않다고 판단되면 메모리에서 해당 데이터는 해제된다.
let john = { name: "John" };
// 위 객체는 john이라는 참조를 통해 접근할 수 있습니다.
// 그런데 참조를 null로 덮어쓰면 위 객체에 더 이상 도달이 가능하지 않게 되어
john = null;
// 객체가 메모리에서 삭제됩니다.
"객체의 프로퍼티, 배열의 요소, 맵이나 셋을 구성하는 요소"들은,
메모리에 남아 있는 동안 도달 가능한 값(reachable)
으로 취급되기에 메모리에서 삭제되지 않는다.
let john = { name: "John" };
let array = [ john ];
john = null; // 참조를 null로 덮어씀
// john을 나타내는 객체는 배열의 요소이기 때문에 가비지 컬렉터의 대상이 되지 않습니다.
// array[0]을 이용하면 해당 객체를 얻는 것도 가능합니다.
alert(JSON.stringify(array[0]));
위 코드에서 john에 null을 할당했지만, 그 전에 앞서 배열([])
이 메모리에 할당되어 있고 john은 배열의 요소이므로 메모리 상에서 해제되지 않는다.
let john = { name: "John" };
let map = new Map();
map.set(john, "...");
john = null; // 참조를 null로 덮어씀
// john을 나타내는 객체는 맵 안에 저장되어있습니다.
// map.keys()를 이용하면 해당 객체를 얻는 것도 가능합니다.
for(let obj of map.keys()){
alert(JSON.stringify(obj));
}
alert(map.size);
예제를 변경해도 john이 맵을 구성하는 요소로 존재하므로 가비지 컬렉터의 대상이 되지 않는다.
map와 weakMap의 차이점은 다음과 같다.
WeakMap의 key는 원시값이 불가능하며, 반드시 객체여야 한다.
그리고 map과 달리, key로 사용된 객체를 참조하는 게 아무것도 없다면 해당 객체는 메모리와 WeakMap에서 모두 자동으로 삭제된다.
let john = { name: "John" };
let weakMap = new WeakMap();
weakMap.set(john, "...");
john = null; // 참조를 덮어씀
// john을 나타내는 객체는 이제 메모리에서 지워집니다!
WeakMap은 iterable하지 않으므로 반복작업을 할 수 없으며,
keys(), values(), entries()
와 같은 메서드를 지원하지 않는다.
WeakMap 메서드
weakMap.get(key)
weakMap.set(key, value)
weakMap.delete(key)
weakMap.has(key)
이렇게 적은 메서드를 지원하는 이유는, 가비지 컬렉션의 동작 방식 때문이다.
weakMap의 key 값으로 존재할 수 있는 객체는 모든 참조를 잃으면 자동으로 GC의 대상이 되는데, GC의 동작 시점은 정확하게 알 수 없다.
가비지 컬렉션이 일어나는 시점은 자바스크립트 엔진이 결정한다. 객체는 모든 참조를 잃었을 때, 그 즉시 메모리에서 삭제될 수도 있고, 다른 삭제 작업이 있을 때까지 대기하다가 함께 삭제될 수도 있다. 현재 위크맵에 요소가 몇 개 있는지 정확히 파악하는 것 자체가 불가능하다. 가비지 컬렉터가 한 번에 메모리를 청소할 수도 있고, 부분 부분 메모리를 청소할 수도 있으므로 위크맵의 요소(키/값) 전체를 대상으로 무언가를 하는 메서드는 동작 자체가 불가능한 것이다.
이러한 WeakMap은 메모리 누수를 막는 데 유용하게 쓰일 수 있다.
예를 들어 WeakMap은 부가적인 데이터를 저장할 곳이 필요할 때 사용할 수 있다. 서드파티 라이브러와 같은 외부 코드에 속한 객체를 가지고 작업해야 할 때 WeakMap을 사용하면 된다.
원하는 데이터를 weakMap의 값으로 저장하고, key로 객체를 저장하면 key인 객체가 GC의 대상이 될 때 해당 데이터도 함께 사라진다.
예를 들어 사용자의 방문 횟수를 세어주는 코드가 있다고 가정할 때
// visitsCount.js
let visitsCountMap = new Map(); // 맵에 사용자의 방문 횟수를 저장함
// 사용자가 방문하면 방문 횟수를 늘려줍니다.
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
// main.js
let john = { name: "John" };
countUser(john); // John의 방문 횟수를 증가시킵니다.
// John의 방문 횟수를 셀 필요가 없어지면 아래와 같이 john을 null로 덮어씁니다.
john = null;
더 이상 필요가 없을 때 john을 나타내는 객체가 GC의 대상이 되어야 한다.
하지만 visitsCountMap의 key로 사용되고 있기에 메모리에서 삭제되지 않는다.
이때 weakMap을 사용하면 된다. 그럼 객체에 대한 정보인 방문 횟수를 일일이 지우지 않아도 자동으로 메모리 상에서 해제가 된다.
john을 나타내는 객체가 도달 가능하지 않은 상태가 되면 자동으로 메모리에서 삭제되기 때문이다. 위크맵의 키(john)에 대응하는 값(john의 방문 횟수)도 자동으로 가비지 컬렉션의 대상이 된다.
// visitsCount.js
let visitsCountMap = new WeakMap(); // 위크맵에 사용자의 방문 횟수를 저장함
// 사용자가 방문하면 방문 횟수를 늘려줍니다.
function countUser(user) {
let count = visitsCountMap.get(user) || 0;
visitsCountMap.set(user, count + 1);
}
캐싱(caching) 작업에 유용하다. 캐싱(caching)은 시간이 오래 소요되는 작업의 결과를 어딘가에 저장해서 연산 시간과 비용을 절약해주는 기법이다.
WeakMap은 메모이제이션에 유용하다.
예를 들어 동일한 함수를 매번 호출해야 한다면,
매번 호출하는 게 아니라 1번만 호출한 다음 저장해두고 그 다음부터는 일일이 호출하는 게 아니라 저장된 값을 사용하는 식의 접근이다.
이때 단순히 map을 사용하면 해당 데이터가 필요없어져도 수동으로 청소를 해줘야 한다. 반면 weakMap을 사용하면 자동으로 청소가 되므로, 이런 상황에 최적인 셈이다.
// cache.js
let cache = new WeakMap();
// 연산을 수행하고 그 결과를 위크맵에 저장합니다.
function process(obj) {
if (!cache.has(obj)) {
let result = /* 연산 수행 */ obj;
cache.set(obj, result);
}
return cache.get(obj);
}
// 📁 main.js
let obj = {/* ... 객체 ... */};
let result1 = process(obj);
let result2 = process(obj);
// 객체가 쓸모없어지면 아래와 같이 null로 덮어씁니다.
obj = null;
// 이 예시에선 맵을 사용한 예시처럼 cache.size를 사용할 수 없습니다.
// 하지만 obj가 가비지 컬렉션의 대상이 되므로, 캐싱된 데이터 역시 메모리에서 삭제될 겁니다.
// 삭제가 진행되면 cache엔 그 어떤 요소도 남아있지 않을겁니다.
WeakSet은 WeakMap과 유사한 맥락이다.
WeakMap과 유사하게 WeakSet도 부가적인 데이터를 저장할 때 사용할 수 있지만, 복잡한 데이터를 처리하기에는 애매하다. 중복이 불가능하기 때문이다.
let visitedSet = new WeakSet();
let john = { name: "John" };
let pete = { name: "Pete" };
let mary = { name: "Mary" };
visitedSet.add(john); // John이 사이트를 방문합니다.
visitedSet.add(pete); // 이어서 Pete가 사이트를 방문합니다.
visitedSet.add(john); // 이어서 John이 다시 사이트를 방문합니다.
// visitedSet엔 두 명의 사용자가 저장될 겁니다.
// John의 방문 여부를 확인해보겠습니다.
alert(visitedSet.has(john)); // true
// Mary의 방문 여부를 확인해보겠습니다.
alert(visitedSet.has(mary)); // false
john = null;
// visitedSet에서 john을 나타내는 객체가 자동으로 삭제됩니다.
위크맵과 위크셋의 가장 큰 단점은 반복 작업이 불가능하다는 점이다. 위크맵이나 위크셋에 저장된 자료를 한 번에 얻는 게 불가능하다.
하지만 이런 단점은 불편함을 초래하는 것 같아 보이지만, 위크맵과 위크셋을 이용해 할 수 있는 주요 작업을 방해하진 않는다. WeakMap과 WeakSet은 메모리 누수 관리를 쉽게 할 수 있는 자료구조이기 때문이다.