Vue.js 메모리 누수 완벽 방어: beforeUnmount와 생명주기 관리

SPA에서 라우터를 10번만 전환해도 브라우저 탭이 크래시되는 현상. 디버깅에 며칠을 쏟게 만든다. 원인은 윈도우 객체에 바인딩된 전역 이벤트 리스너 단 한 줄이었다. 우리가 이 OOM(Out of Memory) 문제를 어떻게 추적하고 해결했는지 그 엔지니어링 과정을 공유한다.

Vue.js 메모리 누수(Memory Leak)는 컴포넌트가 DOM에서 제거된 후에도 전역 객체나 타이머가 해당 컴포넌트를 계속 참조하여 가비지 컬렉터(GC)가 메모리를 회수하지 못하는 현상이다. 이를 방지하려면 onBeforeUnmount 생명주기 훅에서 명시적으로 참조를 끊어야 한다.

메모리 누수와 렌터카 반납의 법칙

Concept: 렌터카 반납 규칙
렌터카를 반납(Unmount)할 때, 차 안에 둔 개인 짐(Event Listener)을 치우지 않고 켜둔 내비게이션(Timer)을 끄지 않으면 렌터카 회사는 그 차를 다음 사람에게 대여할 수 없다. 패널티(Memory Leak)가 누적되는 것이다.

Vue 컴포넌트도 마찬가지다. 프레임워크가 컴포넌트를 DOM에서 제거하더라도, window.addEventListenersetInterval로 등록된 콜백 함수가 컴포넌트 내부의 상태(Ref, Reactive 데이터)를 참조하고 있다면 가비지 컬렉터는 이를 '사용 중'으로 판단한다. 결국 화면에는 보이지 않는 유령 컴포넌트가 메모리에 끝없이 쌓이게 된다.

프로덕션 환경의 생명주기 관리와 코드 구현

현재 Vue.js 3.5 (Tengen Toppa Gurren Lagann) 버전은 내부 반응성 시스템 리팩토링으로 메모리 사용량을 대폭 최적화했다. 그러나 프레임워크의 최적화도 개발자가 작성한 외부 참조 누수까지 막아주진 못한다. 아래는 실제 프로덕션 환경에서 누수를 유발하는 전형적인 안티패턴과 이를 onBeforeUnmount 훅으로 해결하는 코드다.


import { ref, onMounted, onBeforeUnmount } from 'vue';
import { Chart } from 'chart.js'; // 서드파티 라이브러리 예시

export default {
  setup() {
    const chartRef = ref(null);
    let chartInstance = null;
    let timerId = null;

    const handleResize = () => {
      // 윈도우 크기 변경에 따른 컴포넌트 로직
      console.log('Window resized');
    };

    onMounted(() => {
      // 1. 전역 이벤트 리스너 등록 (누수 원인 1)
      window.addEventListener('resize', handleResize);

      // 2. 타이머 등록 (누수 원인 2)
      timerId = setInterval(() => {
        // 백그라운드 폴링 작업 등
      }, 1000);

      // 3. 서드파티 라이브러리 인스턴스화 (누수 원인 3)
      if (chartRef.value) {
        chartInstance = new Chart(chartRef.value, { /* options */ });
      }
    });

    // 해결책: DOM에서 제거되기 직전에 모든 참조를 끊는다.
    onBeforeUnmount(() => {
      window.removeEventListener('resize', handleResize);
      
      if (timerId) {
        clearInterval(timerId);
      }
      
      if (chartInstance) {
        chartInstance.destroy();
      }
    });

    return { chartRef };
  }
}

Watch out for: 서드파티 라이브러리(Chart.js, Lottie 등)의 인스턴스를 컴포넌트 내부에 유지하는 경우, 반드시 해당 라이브러리에서 제공하는 destroy() 또는 dispose() 메서드를 호출해야 한다. Vue의 DOM 요소만 삭제된다고 메모리가 자동으로 반환되지 않는다.

Best Practices: Chrome DevTools Heap Snapshot 분석
메모리 누수가 의심된다면 크롬 개발자 도구의 Memory 탭에서 Heap Snapshot을 사용한다. 라우터 이동을 5~10회 반복한 후 스냅샷을 찍고 Detached 키워드로 검색한다. 화면에서 사라졌음에도 메모리에 남아있는 Detached HTMLElement와 VueComponent를 추적해 누수 지점을 정확히 타격할 수 있다.

자주 묻는 질문 (FAQ)

Q. SPA에서 라우터 전환 시 메모리가 해제되지 않는 이유는 무엇인가?

A. SPA(Single Page Application)는 페이지 이동 시 브라우저를 새로고침하지 않고 DOM만 교체한다. 컴포넌트가 화면에서 사라져도 윈도우 전역 객체, 이벤트 버스, 클로저 등에 의해 참조가 남아있다면 가비지 컬렉터가 수집할 수 없어 메모리에 영구적으로 남게 된다.

Q. Vue에서 beforeUnmount와 unmounted 훅의 차이는 무엇인가?

A. beforeUnmount(Vue 3 Composition API 기준 onBeforeUnmount)는 컴포넌트가 DOM에서 언마운트되기 직전에 호출된다. 아직 인스턴스와 DOM 요소에 완전히 접근할 수 있으므로 리스너나 타이머를 해제하는 데 최적이다. 반면 unmounted는 DOM 노드가 모두 제거된 후에 호출된다.

Q. Vue 컴포넌트 내에서 선언한 ref나 reactive 데이터도 직접 지워야 하는가?

A. 아니다. Vue 내부의 반응형 데이터(ref, reactive 등)는 컴포넌트가 파괴될 때 Vue의 내부 가비지 컬렉션 메커니즘과 브라우저의 GC에 의해 자동으로 정리된다. 개발자가 신경 써야 할 부분은 Vue 외부 환경(Window, Document, 외부 라이브러리 인스턴스, setInterval 등)과의 연결 고리다.

OlderNewest

Post a Comment