Devtools - 메모리 누수 찾아내기
Chrome DevTools Memory 탭으로 메모리 누수 잡기
개발 블로그를 읽다가 “개발자는 DevTools와 꼭 친숙해져야 한다”라는 말을 보았다. 성능 개선과 디버깅에 많은 도움을 받을 수 있기 때문인데, 이번 포스팅에서는 메모리 누수를 찾고 해결해보며 Dev Tools에 대해 알아보고자 한다.
메모리 누수
자바스크립트는 개발자가 변수에 값을 할당할 때 따로 메모리를 신경쓰지 않아도 되고, 해당 변수가 더 이상 사용되지 않는 경우에는 가비지 컬렉터(GC)를 통해 자동으로 메모리를 관리한다.
문제는 GC가 전부 처리하지 못하는 경우가 발생할 수도 있다는 것인데, 참조를 제거하지 않아서 GC가 메모리를 회수하지 못하는 경우 필요없는 리소스가 메모리 자리를 차지하게 되는 상황, 즉 메모리 누수가 발생하고, 이는 성능 저하를 야기할 수 있다.
점점 복잡해지는 현대 웹에서 메모리 관리는 FE 개발자로서 마주한 중요한 이슈임에 틀림없는 것이다. 🫡
메모리 누수가 주로 발생하는 상황
- eventListener를 제거하지 않는 경우
- timer를 정리하지 않는 경우
- Closure에서 불필요한 참조가 계속되는 경우
모든 메모리 누수가 위험할까?
메모리 누수에는 두 가지 종료가 있다.
1. Static Memory Leak
프로그램을 몇 번 실행하든 메모리 누수의 크기가 동일한 경우이다.
2. Dynamic/Increasing Memory Leack
프로그램을 실행할 때마다 점점 더 많은 메모리를 할당하는 경우이다. 대부분의 경우 이 경우의 메모리 누수가 프로그램에 악영향을 끼치며, 가장 먼저 해결해야 할 과제가 된다.
그럼 메모리 누수 디버깅은 어떻게 진행할까?
디버깅 준비하기
메모리 누수 디버깅을 시작하기 전 다음과 같이 디버깅 환경을 만들자.
- Chrome extension 등의 확장 프로그램이 측정값에 영향을 미치지 않도록 시크릿 모드를 사용한다.
- 항상 Application의 Production Build 모드에서 디버깅 하자. 개발 모드에서 대부분의 프레임워크는 측정 값에 영향을 줄 수 있는 다양한 작업을 내부적으로 수행하기 때문이다.
Performance 탭으로 첫 번째 확인
Memory 탭으로 가기 전에 Performance 탭에서 전체적인 메모리 상황을 먼저 확인해보자.
- Memory 체크박스를 활성화한다
- 수동으로 GC를 실행한다
- 의심되는 작업을 반복 수행한다
- JS heap 그래프를 확인한다
만약 GC를 실행했는데도 JS heap이 계속 증가하는 그래프를 보인다면? 메모리 누수를 발견한 것이다!
Memory 탭의 4가지 도구
Memory 탭에는 4가지 프로파일링 도구가 있다:
1. Heap Snapshot (힙 스냅샷)
페이지의 JavaScript 객체들과 관련 DOM 노드들 사이의 메모리 분포를 보여준다. 특정 시점의 메모리 상태를 사진 찍듯이 캡처해서 어떤 객체들이 얼마나 메모리를 차지하고 있는지 상세히 분석할 수 있다.
2. Allocation Instrumentation on Timeline
시간에 따른 JavaScript 메모리 할당을 추적해서 보여준다. 프로파일이 녹화되면 특정 시간 구간을 선택해서 그 구간 내에서 할당되었으면서 녹화 종료 시점까지 살아있는 객체들을 확인할 수 있다. 메모리 누수를 찾을 때 특히 유용하다.
3. Allocation Sampling
샘플링 방식을 사용해서 메모리 할당을 기록한다. 이 프로파일 타입은 성능 오버헤드가 최소화되어 있어서 장시간 실행되는 작업에도 사용할 수 있다. JavaScript 실행 스택별로 분류된 할당 정보의 정확한 근사치를 제공한다.
4. Detached Elements
JavaScript 참조에 의해 유지되고 있는 객체들을 보여준다. 주로 DOM에서는 제거되었지만 JavaScript에서 여전히 참조하고 있어서 가비지 컬렉션되지 않는 요소들을 찾는 데 사용한다.
디버깅 과정
그럼 이제 디버깅을 진행해보자.
방법 1: Heap Snapshot 비교하기
- 페이지 새로고침 후 첫 번째 스냅샷을 생성한다
- 메모리 누수 의심 작업을 수행한다
- 두 번째 스냅샷을 생성한다
- “Objects allocated between Snapshot 1 and Snapshot 2”를 선택한다
- Comparison 모드에서 비교 분석한다
주목할 지표들
- NEW: 새로 생성된 객체
- Deleted: 삭제된 객체
- Delta: 생성 - 삭제 차이 (양수면 메모리 누수를 의심해야 한다!)
방법 2: Allocation Instrumentation 활용
- 녹화를 시작한다
- 컴포넌트 mount/unmount를 반복한다
- 녹화 중지 후 힙 증가 패턴을 확인한다
- 계속 증가하는 객체 인스턴스 수를 체크한다
Retainers 탭을 통해 의심 객체를 참조하고 있는 최종 참조자를 확인할 수 있다.
Detached Elements 찾기
DOM에서는 사라졌지만 JavaScript에서 여전히 참조되고 있는 요소들을 찾는 방법이다.
- Memory 탭에서 “Detached elements”를 선택한다
- Heap Snapshot과 유사한 방식으로 디버깅한다
- 발견된 Detached Elements는 다시 Heap Snapshot에서 검색해서 Retainers를 확인한다