Vue / Nuxt / JS 대용량 메모리 사용 및 누수 방지 (Memory Leak)
대용량 메모리 사용 및 누수 방지
지금까지 Vue와 Nuxt로 개발하면서 성능 이슈에 대해 외면(?) 해오거나 문제를 삼지 않은 경우가 많았습니다.
원인 파악을 하기 어렵고, 어떠한 이유에서인지 메모리 릭 현상이 일어나 앱 또는 웹이 크래시되는 문제가 발생합니다.
메모리 누수(Memory Leak)가 무엇인가요?
프로그램이 작동하여 할당됐던 메모리가 더 이상 사용되지 않은 시점에서도 반환되지 않는 현상입니다.
이게 무슨 말이냐 하면, "에어컨 온도를 24도로 맞춰놓고 실내온도가 24도로 되었지만 계속 에어컨이 작동하여 온도를 계속 낮추는 것" 정도로 비유할 수 있습니다. 즉 더 이상 필요가 없는데도 멈추지 않고 에너지를 쓰고 있다. 라고 보셔도 됩니다.
JavaScript의 경우 가비지 컬렉터가 프로그램에 할당된 메모리를 주기적으로 관리해 더 이상 사용되지 않는 메모리는 반환하도록 설계 돼 있습니다. 따라서 개발자가 매번 메모리에 대하여 신경 쓰지 않아도 됩니다.
하지만, 가비지 컬렉터가 관리해주기 때문에 메모리 누수 현상은 왜 일어나는 것일까요?
JavaScript에는 더 이상 사용되지 않지만 가비지 컬렉터가 파악하지 못하여 반환되지 않는 메모리가 존재합니다.
가비지 컬렉터가 파악하지 못하는 메모리 사용
- 전역 변수
- 타이머와 콜백
- DOM 외부의 참조
- 클로저
메모리 누수 분석
크롬 개발자 도구를 이용하여 메모리 할당 정보를 확인 합니다.
Step 1. Performance 탭
Chrome 개발자 도구의 Performance 탭에서 라우터 전환 시 메모리 할당에 대한 전반적인 패턴을 다음과 같은 과정으로 확인합니다.
- 상단 메뉴의 Record 버튼을 누릅니다.
- 메모리 할당을 측정하기 위해 라우터 전환을 반복합니다.
- Stop 버튼을 눌러 측정된 패턴을 확인합니다.
JS Heap과 Nodes가 점진적인 증가세를 보인다면 메모리 누수를 의심할 수 있습니다.
위 사진을 볼까요?
파란 선이 JS Heap 초록색이 Nodes 입니다.
중간 중간에 감소한 부분들은 가비지 컬렉터가 메모리를 반환한 것이고, 일반적으로 메모리에 할당된 요소는 현재 라우터의 요소와 이전 라우터의 요소 두 가지가 있습니다.
현재 요소는 HTMLElement, 이전 요소는 Detached HTMLElement로 확인할 수 있습니다.
Detached HTMLElement는 다음 라우터로 전환할 때 메모리에서 반환됩니다.
만약, 현재 요소와 이전 요소가 아닌 녀석이 메모리에 할당돼 있다면 메모리 누수를 발생 시킨다고 볼 수 있습니다.
메모리 누수가 없는 프로젝트의 Performance 탭
확연한 비교가 되지 않나요?
가비지 컬렉터가 메모리를 반환해 _JS Heap_과 _Nodes_가 정상적으로 직각 상태의 플로우를 보여주고 있습니다.
Step 2. Memory 탭 - Heap 스냅샷
Chrome 개발자 도구 Memory 탭의 힙 스냅샷(heap snapshots)을 이용하면 특정 라우트에서 메모리에 할당된 요소를 확인합니다.
- 메모리 할당을 측정하고자 하는 라우트 경로(Route Path)로 이동합니다.
- 상단 메뉴의
녹화 버튼을 눌러 힙 스냅샷을 찍습니다.
상단 검색에 요약 (Summary)의 Class filter에서 엘리먼트를 검색하면 메모리에 할당된 요소를 확인할 수 있습니다.
로컬 환경에서는 Summary의 Class filter에서 Vue 컴포넌트를 검색하면 메모리에 할당된 Vue 컴포넌트를 확인할 수 있습니다.
이제 메모리 누수가 있는지 확인해보겠습니다.
1 depth.
첫 화면의 index 페이지에서 할당된 HTMLElement
2 depth.
다른 라우터로 이동
3 depth.
다시 index 페이지로 돌아옵니다.
결과 => 1 depth
에서 메모리에 할당된 HTMLElement가 2 depth
에서 1에서 할당된 메모리가 제거되어야 하지만 여전히 반환되지 않았습니다. 3 depth
에서 2 depth
에 할당된 메모리는 detached로 제거되어 메모리가 반환되었습니다. 가비지 컬렉터가 수집하지 않는 메모리 때문에 메모리 누수 현상이 발생했다는 것을 알 수 있습니다.
Step 3. Memory 탭 - 메모리 할당 타임라인
Memory 탭의 메모리 할당 타임라인으로 라우터 전환 시 메모리에 할당되는 전반적인 요소를 확인할 수 있습니다.
- 상단 메뉴의 Start Recording Heap Profile(타임라인의 할당 계측) 버튼을 누릅니다.
-
- 시작 또는 녹화를 누르면 할당 타임라인 스냅샷을 기록하면서 현재 라우터 이동을 할 때마다 실시간으로 할당되고 반환되는 메모리를 확인할 수 있습니다.
- Stop Recording Heap Profile (활성중인 녹화버튼을 한 번더 눌러서 중지) 버튼을 누릅니다.
- 특정 구간을 선택 한 뒤 Summary(요약)의 Class filter에서 요소를 검색하면 메모리에 할당된 요소를 확인할 수 있고, 마찬가지로 Vue 컴포넌트를 검색하면 메모리에 할당된 Vue 컴포넌트를 확인할 수 있습니다.
메모리 할당 타임라인을 보며 누수를 확인 해봅니다.
그래프의 파란색 영역은 반환되지 않고 할당 요소가 존재하는 메모리 (메모리 누수)
회색의 영역은 반환된 메모리를 표시합니다.
위 그래프를 보면 파란색 영영역이 그대로 남아있는것을 보아 메모리 누수가 발생했다는 것을 알 수 있습니다.
메모리 누수 개선하기
프로젝트에 메모리 누수가 있다는 것을 확인했다면 개선해야합니다. Vue 컴포넌트는 하나의 컴포넌트가 내부적으로 다른 컴포넌트를 참조($parent, $children)하기 때문에 한 컴포넌트가 메모리에 남는 문제가 발생하면 참조 관계로 얽혀 있는 다른 컴포넌트도 메모리에 남는 문제가 발생합니다.
따라서 메모리 누수를 유발한 핵심 컴포넌트를 찾아 원인을 파악해 개선하는 것이 주요 방법입니다.
그렇지만 메모리 누수를 유발한 핵심 컴포넌트를 어떻게 찾을까요?
- 위에서 메모리 분석 과정을 통해 메모리 누수가 발생하는 라우터 경로를 확인합니다.
- 라우터 최상위 컴포넌트의 내부 컴포넌트를 최소화 해가며 메모리에서 반환되지 않는 내부 컴포넌트를 찾아야 합니다.
- 메모리에서 반환되지 않는 컴포넌트를 도출해 메모리 누수의 원인이 되는 소스 코드를 찾아야합니다.
이 때 메모리에서 반환되지 않는 컴포넌트가 하나 이상일 수 있다는 점을 인지하여야 합니다.
위 과정에서 발견되는 컴포넌트가 여러 개일 수 있습니다.
- Vue 컴포넌트의 이벤트에 $on은 있으나, $off는 없을 때.
- Vue 컴포넌트를 싱글톤 클래스에 저장하고 라우터 전환 시 제거하지 않았을 때
- Nuxt에서
vue-router
프리패치(prefetch) 사용으로 링크 수에 따라 메모리 사용량이 많을 수 있습니다. 제 경우에는 많지 않으므로 허용하지만, nuxt에서 프리패치(prefetch)를 비활성화하는 옵션도 있으므로 앱이 매우 바쁘거나 단일 페이지에 수백 개의 링크가 있는 경우 prefetch를 비활성화하는 것이 좋습니다.
// locally
<nuxt-link to="/" no-prefetch>link</nuxt-link>
// globally in nuxt.config.js
router: {
prefetchLinks: false
}
- Vue / Nuxt의 라이프 사이클 훅(Hook)에서
$nuxt.on
사용할 때 (이벤트 리스너가 제거되지 않음) 경험하였습니다. 그래서 모든 리스너를 가능한 한 클라이언트 측(beforeMount
ormounted
)에서 사용하는 것이 좋습니다.
- Nuxt Docs(문서)에서는 다음과 같이 설명합니다.
- Vue.use(), Vue.component()를 사용하지 말고 전역적으로 Nuxt 주입 전용인 이 함수 내에서 Vue에 아무 것도 연결하지 마십시오. 서버 측에서 메모리 누수가 발생합니다.
Vue.use()
내부 주입 기능을 사용하면 메모리 누수가 발생할 수 있습니다.
'🖥Frontend' 카테고리의 다른 글
[Jetbrains Idea] IntelliJ 닫은 소스 탭 복구하기 (0) | 2022.10.22 |
---|---|
FontAwesome 버전 별 cdn 링크 (0) | 2022.06.15 |
동영상 HLS란? + 특징 (0) | 2022.06.05 |
댓글