Javascript

자바스크립트 WeakRef 완벽 가이드: 메모리 관리를 쉽게 이해해볼까요?

드리프트2 2025. 1. 8. 21:13

자바스크립트 WeakRef 완벽 가이드: 메모리 관리를 쉽게 이해해볼까요?

자바스크립트에서 WeakRef가 메모리 관리를 어떻게 도와주는지 이해해볼까요?

 

자바스크립트로 객체를 생성할 때마다 브라우저 메모리를 사용하게 되는데요, 그 객체를 더 이상 사용하지 않으면 자바스크립트가 자동으로 그 메모리를 정리해줘야 합니다.

 

하지만 때때로 예상했던 대로 메모리 정리가 이루어지지 않을 때가 있습니다.

 

강한 참조

강한 참조는 메모리 내 데이터에 "삭제하지 마라"는 태그를 붙이는 것과 같다고 생각할 수 있습니다.

 

자바스크립트에서 객체나 값에 가리키는 일반 변수를 생성하면, 이는 강한 참조를 만들게 됩니다.

// 객체에 대한 강한 참조를 생성
const user = {
  url: "www.trevorlasn.com",
  name: "Trevor Lasn"
};

 

여기서 user 변수는 객체를 강하게 유지하여 가비지 컬렉션으로부터 안전하게 보호합니다.

 

이 참조가 존재하는 한, 자바스크립트의 가비지 컬렉터는 그 데이터를 메모리에서 제거하지 않습니다, 다른 곳에서 사용되지 않더라도요.

 

약한 참조

반면 약한 참조는 메모리 내 데이터에 "필요 없으면 삭제해라"는 태그를 붙이는 것과 같습니다.

 

객체나 값을 가리키지만, 강한 참조가 존재하지 않으면 가비지 컬렉터가 그 데이터를 제거할 수 있게 해줍니다.

 

이는 메모리에 객체를 강제로 유지하지 않고 참조하는 방식입니다.

 

간단히 말해, 객체에 약한 참조만 존재한다면 가비지 컬렉터는 해당 객체를 정리할 수 있습니다.

 

이는 메모리 누수를 방지하면서 임시로 무언가를 참조하고자 할 때 유용합니다.

// 객체에 대한 약한 참조를 생성
const weakUser = new WeakRef({
    url: "www.trevorlasn.com",
    name: "Trevor Lasn"
});

// 약한 참조를 통해 객체에 접근
console.log(weakUser.deref()?.name); // "Trevor Lasn"

 

강한 참조는 가비지 컬렉션을 적극적으로 방지하는 반면, 약한 참조는 강한 참조가 남아있지 않으면 가비지 컬렉션을 허용한다는 점이 핵심 차이입니다.

 

메모리 정리가 중요한 이유

예를 들어, 채팅 앱을 만든다고 할 때, 새로운 메시지가 나타날 때마다 메시지를 포맷하고 어떻게 보여질지를 저장한다고 해봅시다.

const messageCache = new Map();

function handleNewMessage(message) {
    // 포맷된 메시지 데이터를 저장
    messageCache.set(message.id, {
        text: message.text,
        formattedTime: formatMessageTime(message.timestamp)
    });
}

 

사용자가 더 많은 메시지를 보낼수록 앱은 더 많은 메모리를 사용하게 됩니다.

 

오래된 메시지가 스크롤되어 더 이상 보이지 않더라도, messageCache가 여전히 참조를 유지하고 있기 때문에 데이터는 메모리에 남아있어요. 이렇게 되면 브라우저가 메모리를 정리하지 못하게 됩니다.

 

메모리 정리는 어떻게 이루어질까요?

기본적으로 자바스크립트는 코드가 객체에 접근할 수 있는 한 그 객체를 메모리에 유지합니다.

 

연결 고리 놀이처럼, 자바스크립트가 코드에서 객체로 선을 그릴 수 있다면 그 객체는 메모리에 남게 됩니다.

let message = { text: "Hello" };  // 메모리에 객체 생성
let copy = message;               // 같은 객체에 접근하는 또 다른 방법
message = null;                   // 'copy'가 참조하고 있으므로 객체는 여전히 존재
copy = null;                      // 이제 객체는 메모리에서 정리될 수 있음

WeakRef의 역할

WeakRef는 객체를 메모리에 유지하지 않으면서 참조할 수 있는 특별한 참조를 생성합니다.

 

실제로 그것을 잡고 있지 않으면서 어디에 있는지에 대한 메모만 남기는 것과 같다고 볼 수 있습니다.

let message = { text: "Hello" };
let weakNote = new WeakRef(message);  // 약한 참조를 생성

// 나중에...
let maybeMessage = weakNote.deref();  // 메시지를 가져오려 시도
if (maybeMessage) {
    console.log(maybeMessage.text);  // 성공할 수도 있습니다
} else {
    console.log("메시지가 사라졌습니다");   // 또는 메시지가 사라졌을 수도 있습니다
}

 

WeakRef를 사용하면 "이 객체가 아직 필요하면 사용하겠지만, 필요 없으면 정리해도 괜찮다"는 것을 자바스크립트에 알리는 것이죠.

 

WeakRef는 메모리에 강하게 유지하지 않고도 무언가를 추적하고자 할 때 유용합니다.

 

예를 들어, 페이지에 로드되는 이미지를 추적할 때 사용할 수 있습니다.

function trackImageLoad(img) {
    const ref = new WeakRef(img);
    const startTime = Date.now();

    const tracker = setInterval(() => {
        const image = ref.deref();

        // 이미지가 사라졌다면 (사용자가 페이지를 벗어났거나 제거된 경우) 추적 중지
        if (!image) {
            clearInterval(tracker);
            return;
        }

        // 이미지가 로드되었으면 소요 시간을 로그에 기록
        if (image.complete) {
            const loadTime = Date.now() - startTime;
            console.log(`Image loaded in ${loadTime}ms`);
            clearInterval(tracker);
        }
    }, 100);
}

// 모든 이미지에 사용
const img = document.createElement('img');
img.src = 'photo.jpg';
trackImageLoad(img);
document.body.appendChild(img);

 

이 코드는 이미지가 로드되는 데 걸리는 시간을 이해하는 데 도움이 되지만, 사용자가 페이지를 떠나거나 이미지가 제거될 경우 브라우저가 이미지를 정리하는 것을 방해하지 않습니다.

 

WeakRef가 없었다면, 이미지를 제거할 때마다 추적기를 수동으로 제거해야 했을 것이고, 그렇지 않으면 페이지에서 사라진 이미지가 메모리에 남아있게 되었을 겁니다.

 

FinalizationRegistry를 통한 객체 정리

때로는 객체가 메모리에서 정리될 때를 알아야 할 필요가 있습니다. 이럴 때 FinalizationRegistry가 도움이 됩니다.

 

객체가 메모리에서 제거될 때 코드를 실행할 수 있게 해줍니다.

// 정리 함수가 포함된 레지스트리 생성
const registry = new FinalizationRegistry(data => {
    console.log('Cleanup:', data);
});

function trackImage(img) {
    const ref = new WeakRef(img);

    // 식별을 돕기 위해 일부 데이터를 함께 등록
    registry.register(img, {
        src: img.src,
        trackedAt: Date.now()
    });

    return ref;
}

// 사용 예시
const img = document.createElement('img');
img.src = 'photo.jpg';
const imageRef = trackImage(img);

// 나중에 이미지가 제거되고 가비지 컬렉션되면,
// 제공한 데이터와 함께 레지스트리 함수가 실행됩니다.

 

하지만 주의해야 할 점은 정리 함수가 언제 실행될지 정확히 알 수 없다는 것입니다.

 

브라우저가 메모리를 정리할 시점을 결정하므로, 정리 함수가 예상보다 훨씬 나중에 실행될 수 있어요.

 

따라서 FinalizationRegistry는 다음과 같은 경우에 유용합니다:

  • 서버에서 파일이나 리소스를 정리할 때
  • 객체가 제거될 때를 로그로 기록할 때
  • 카운터나 통계를 업데이트할 때

다음은 WeakRef와 FinalizationRegistry를 결합하여 메모리 사용량을 추적하는 실용적인 예시입니다.

const cleanup = new FinalizationRegistry(data => {
    console.log(`Resource cleaned up: ${data.type}`);
    updateMemoryStats(data.size, 'freed');
});

function trackResource(resource, type, size) {
    const ref = new WeakRef(resource);

    // 유용한 데이터와 함께 정리 등록
    cleanup.register(resource, {
        type,
        size,
        createdAt: Date.now()
    });

    updateMemoryStats(size, 'allocated');
    return ref;
}

function updateMemoryStats(bytes, action) {
    if (action === 'allocated') {
        memoryUsed += bytes;
    } else {
        memoryUsed -= bytes;
    }
    updateMemoryDisplay();
}

 

이 코드는 객체가 생성되고 정리될 때 메모리 사용량 통계를 업데이트하여 시간이 지남에 따라 메모리 사용량을 추적하는 데 도움을 줍니다. 동시에 메모리 정리가 발생하는 것을 방해하지 않습니다.

 

WeakRef 도입 제안서에서 포함된 몇 가지 구체적인 점

가비지 컬렉터는 복잡합니다. 애플리케이션이나 라이브러리가 WeakRef의 정리나 최종화기를 예상한 시점에 실행되도록 의존한다면, 아쉬울 가능성이 큽니다.

 

정리는 예상보다 훨씬 늦게 일어나거나 전혀 일어나지 않을 수도 있습니다. 변동성의 원인은 다음과 같습니다:

  • 한 객체는 다른 객체보다 훨씬 빨리 가비지 컬렉션될 수 있습니다, 예를 들어 세대별 컬렉션 때문에.
  • 가비지 컬렉션 작업은 증분적이고 동시적인 기술을 사용하여 시간이 지남에 따라 분할될 수 있습니다.
  • 다양한 런타임 휴리스틱을 사용하여 메모리 사용량과 반응성을 균형 있게 조절할 수 있습니다.
  • 자바스크립트 엔진은 도달 불가능해 보이는 것들(예: 클로저나 인라인 캐시)에 대한 참조를 유지할 수 있습니다.
  • 서로 다른 자바스크립트 엔진은 이러한 작업을 다르게 수행할 수 있으며, 같은 엔진도 버전마다 알고리즘을 변경할 수 있습니다.
  • 특정 API와의 사용과 같은 복잡한 요인 때문에 객체가 예상치 못한 시간동안 살아 있을 수 있습니다.

WeakRef가 사용 사례에 적합한지 확신이 서지 않는다면, 사용하는 것을 피하는 것이 좋습니다.

 

강력한 도구지만 오용하기 쉽습니다. 확실하지 않다면, 아마도 필요하지 않을 것입니다.