
WYSIWYG 에디터를 간단하게 만들어봅니다
[소소한 개발 일지] WeakRef의 약한 참조와 메모리 관리
2025-03-13
Explanation
오늘은 WeakRef의 약한 참조와 이를 활용한 메모리 관리에 대해서 알아봅니다!
WeakRef는 ES2021에서 FinalizationRegistry와 함께 추가되었는데요. 간단하게 WeakRef는 객체를 약한 참조로 만들고 FinalizationRegistry는 객체가 GC에 의해서 제거될 때의 콜백을 설정할 수 있습니다.
WeakRef는 객체를 약한 참조로 만들고 객체의 가비지 컬렉션을 방지하지 않는데요.
“javascript.info”의 예시를 들어보면,
(https://javascript.info/weakref-finalizationregistry)
1 2 3 4 5 6 7 8 9 10 |
// the user variable holds a strong reference to the object let user = { name: "John" }; // copied the strong reference to the object into the admin variable let admin = user; // let's overwrite the value of the user variable user = null; // the object is still reachable through the admin variable |
‘user’라는 변수에 강한 참조로 변수를 보유하고 이를 ‘admin’이라는 변수에 강한 참조로 복사하면 ‘user’라는 변수를 덮어써도 여전히 admin을 통해서 객체에 접근할 수 있습니다.
1 2 3 4 5 |
let user = { name: "John" }; let admin = new WeakRef(user); user = null; |
하지만 위와 같이 ‘admin’에 WeakRef를 사용해서 객체를 얕은 참조로 복사하면 ‘user’라는 값이 덮어써졌을 때 ‘admin’도 GC 될 수 있도록 할 수 있습니다.
1 2 3 4 5 6 7 |
let ref = admin.deref(); if (ref) { // the object is still accessible: we can perform any manipulations with it } else { // the object has been collected by the garbage collector } |
그리고 위와 같이 얕은 참조한 값은 ‘deref()’ 메서드를 통해 가져올 수 있으며, 조건문을 통해 GC 여부를 확인 할 수 있습니다. (가비지 컬렉터에 수집되면 ‘undefined’ 응답)
하지만 브라우저의 가비지 컬렉션은 브라우저가 필요하다 판단할 때 동작하기 때문에 위와 같이 ‘user = null’로 덮어썼다고 해서 바로 GC 되어 ‘admin.deref()’값이 ‘undefined’가 되지는 않습니다. 다만, 얕은 참조로 GC가 언제든지 해당 객체를 제거할 수 있도록 보장합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
class Counter { constructor(element) { // Remember a weak reference to the DOM element this.ref = new WeakRef(element); this.start(); } start() { if (this.timer) { return; } this.count = 0; const tick = () => { // Get the element from the weak reference, if it still exists const element = this.ref.deref(); if (element) { element.textContent = ++this.count; } else { // The element doesn't exist anymore console.log("The element is gone."); this.stop(); this.ref = null; } }; tick(); this.timer = setInterval(tick, 1000); } stop() { if (this.timer) { clearInterval(this.timer); this.timer = 0; } } } const counter = new Counter(document.getElementById("counter")); setTimeout(() => { document.getElementById("counter").remove(); }, 5000); |
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef
위 코드는 MDN에 나와 있는 예시코드 인데요. 위 코드를 실행해보면, id가 counter인 엘리먼트의 값이 1부터 5까지 증가한 후 사라지고, 일정시간 후에 약한 참조가 해제되며 “The element is gone.” 로그가 찍히는 것을 확인할 수 있습니다.
그래서 정말로 GC가 동작하는지 앞서 이야기한 ‘FinalizationRegistry’를 통해서 확인해 봤는데요.
1 2 3 4 5 6 7 8 9 10 11 12 |
class Counter { constructor(element) { // Remember a weak reference to the DOM element this.ref = new WeakRef(element) this.registry = new FinalizationRegistry((heldValue) => { console.log(`GC 발생: ${heldValue}`) }) this.registry.register(this.ref, "Counter Element") this.start() } ... |
하지만 위 코드를 실행하면 약한 참조가 해제되며 “The element is gone.” 로그는 찍히지만 FinalizationRegistry의 콜백은 호출되지 않는 걸 확인할 수 있었는데요.
그 말인즉, 정확하게는 브라우저의 WeakRef의 상태 확인과 GC의 동작은 별개로 동작한다는 것입니다. 위 예시에서는 브라우저가 DOM 요소가 삭제되고 이후 브라우저가 더 이상 해당 DOM 요소를 참조하지 않는다고 판단하면 this.ref.deref()는 ‘undefined’를 반환합니다. 그리고 GC가 실행될 때 비로소 FinalizationRegistry 콜백이 실행됩니다.
MDN에 작성되어 있는 주의해야 할 점을 간단히 살펴보면,
– WeakRef를 사용하면 GC를 직접 제어할 수 있는 것은 아닙니다.
– GC는 예측할 수 없으며, 엔진에 따라 다르게 동작할 수 있습니다.
– WeakRef를 사용한 객체는 GC될 가능성이 높지만, 즉시 GC되지는 않습니다.
– GC가 객체를 제거하지 않으면 WeakRef가 계속 객체를 참조할 수도 있습니다.
대표적인 WeakRef 사용처는 캐시 시스템인데요. 일반적으로 데이터를 캐시 하여 사용한다면 강한 참조를 유지하기 때문에 메모리 사용량이 많아질 수 있는데요. 자주 사용되는 데이터가 아니라면, 약한 참조로 사용해서 메모리가 부족하면 자동으로 사라지도록 해서 메모리를 절약할 수 있습니다.
마찬가지로 “javascript.info”의 예시를 보면,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function fetchImg() { // abstract function for downloading images... } function weakRefCache(fetchImg) { const imgCache = new Map(); return (imgName) => { const cachedImg = imgCache.get(imgName); if (cachedImg?.deref()) { return cachedImg?.deref(); } const newImg = fetchImg(imgName); imgCache.set(imgName, new WeakRef(newImg)); return newImg; }; } const getCachedImg = weakRefCache(fetchImg); |
그 밖에도 DOM 요소의 이벤트 리스너 같은 경우에도 DOM이 제거될 때 직접 수동으로 이벤트 리스너를 제거하기 어려운 상황이라면 WeakRef를 사용해서 참조를 약하게 유지하면서 자동으로 정리할 수 있습니다.