개요
클라우드 서비스 어플리케이션 개발하면서 같은 페이지를 반복해서 이동할 경우 Detached HTML 요소들이 쌓여 Heap과 node가 증가하고 줄어들지 않는 현상을 발견하였다. 한마디로 메모리 누수가 발생하여 프로파일링을 통해 개선시킨 경험을 글로 정리하게 되었습니다. 메모리 누수가 발생할 수 있는 범위가 방대하고 누수가 발생한 객체나 DOM 요소가 프로파일링과 스냅샷을 사용해도 명확히 표시되는 것이 아니라 소스 분석을 통해 많은 정보를 수집해야 했습니다. 이 글에서는 프로파일링 방법과 개선 방법에 대해 다루겠습니다.
메모리 누수
메모리 누수란 더 이상 사용하지 않는 메모리가 반환되지 않고 계속 남아 있는 현상으로 브라우저에서는 GC(가비지 컬렉터)가 동작했음에도 불구하고 남아있는 메모리가 남아있는 경우를 말합니다.
메모리 누수 개선을 진행하며 파악했던 메모리가 남아있는 경우는 다음과 같았습니다.
- Vue.js는 자체적으로 Virtual DOM을 이용해 DOM 요소를 조작하는 방식이나 jQuery로 DOM 요소를 직접 선택하고 조작하여 Vue와 충돌
- Vue의 div 요소와 애니메이션 동작을 처리하는 Gsap 라이브러리와의 참조 문제
- window.console.log와 callback 출력
메모리 누수 분석하기
개발자 도구를 이용한 프로파일링 방법으로는 다음과 같이 세가지 방법을 이용합니다.
- Memory 탭 스냅샷
- Memory 탭 타임라인
- Performance Record
위 순서대로 프로파일링 했던 방법을 소개합니다.
메모리 스냅샷 이용하기

1. Memory 탭에서 profiling type을 snapshot으로 설정한다.
2. 좌측 상단의 ● 버튼을 누르면 스냅샷을 찍는다.
3. 어플리케이션의 여러 화면을 이동하며 스냅샷을 촬영한다.

4. Comparison으로 변경 후 비교할 Snapshot을 선택하여 #Delta 및 Size Delta 값을 확인한다.
5. 증가된 Constructor를 찾아 Object로 정보를 수집하고 수집된 정보로 소스 분석을 수행한다.
메모리 타임라인 이용하기

1. Memory 탭에서 profiling type을 timeline으로 설정한다.
2. 좌측 상단의 ● 버튼을 누르면 시작한다.
3. 좌측 상단의 ● 버튼을 누르면 타임라인이 종료된다.

4. 다음 화면을 보고 결과를 분석한다. 이때 파란색 영역은 메모리가 해제되지 않은 영역으로 Object들을 분석한다.
Performance로 메모리 누수 확인하기
1. Performance탭에서 좌측 상단의 ● 버튼(Record)을 누르면 시작한다.
2. 어플리케이션의 여러 화면을 이동하고 우측 상단의 휴지통 모양인 GC(가비지 컬렉터)를 실행시켜준다.

3. 좌측 상단의 ● 버튼(Stop)을 누르면 타임라인이 종료된다.
4. 결과를 확인하고 메모리 누수가 있는지 확인한다.
개선 전 Performance

Performance 확인 결과 JS Heap, Nodes가 GC(가바지 컬렉터)가 실행되었음에도 반환되지 못하는 노드가 있기 때문에 같은 동작을 반복할 경우 메모리와 노드가 쌓여 어플리케이션이 다운될 수 있습니다.
메모리 누수를 해결하기 위해 첫번째로 Memory 탭의 타임라인을 이용하여 반환되지 못하는 부분의 Object들을 확인합니다. 확인된 Object들의 소스 상에서의 위치 및 쓰임새를 파악하고 Vue의 구조, 객체의 참조를 분석하며 문제점을 찾습니다. 그 후 해당하는 Object들을 참조하는 객체들을 초기화 시켜주거나 불필요한 요소는 지웁니다. 앞서 말한 과정을 반복하며 누수가 발생하지 않도록 개선하였습니다.
개선 후 Performance

개선 전과 비교했을때 GC 실행 후 JS Heap 및 Nodes가 원래 수치로 돌아온다.
정리
문제점 :
같은 페이지를 반복해서 이동할 경우 Detached HTML 요소들이 쌓여 어플리케이션의 메모리와 node가 증가하는 현상 발생
소스 분석 :
- Vue 인스턴스가 제대로 파괴되지 않는 경우 발생(ex. Vue 컴포넌트의 이벤트 리스너와 타이머, 비동기 작업이 있을 경우 리소스가 해제되지 않을 수 있다)
- gsap 라이브러리의 캐시
- console.log에 객체를 출력하는 경우 GC가 수거하지 않고 남겨두고 있음
- 초기화 되지 않는 불필요한 전역 변수 생성
해결 방안 :
- Vue 인스턴스를 파괴시킨다.
애니메이션 로직이 animate 요소 하위에 Vue componenet로 자식 element를 생성한 뒤 생성한 element를 animationPage 요소에 clone하여 노출
- as is : element를 제거할때 clone한 element만 제거
- to be : animate 요소의 자식 element를 제거한 뒤 clone한 element 제거 - 특정 시점에 gsap.globalTimeline.clear();를 호출하여 gsap 라이브러리의 캐시를 지워준다.
- console.log를 출력하는 debug 레벨을 설정하여 배포
- 불필요한 전역 변수 제거
결과 :
- JS Heap : 0.72MB -> 0.05MB (93% 감소)
- Nodes : 623 -> 0 (100% 감소)
* 단위 : 한번의 페이지 이동 후 복귀(및 GC 수행 후)
글 처음에 말했던대로 첫번째는 Vue 객체가 jQuery로 DOM을 직접 수정하는 코어 로직으로 인해 destroy가 되었음에도 node에 남아있어 메모리 누수가 발생했습니다. 두번째는 메모리 누수를 발생시킨 요인으로 Gsap 라이브러리를 이용하는 객체가 정상적으로 제거되지 않아 캐시가 계속 남는 현상이 있어 애니메이션 수행 후 gsap.globalTimeline.clear(); 으로 초기화 시켜주었습니다. 세번째로는 console.log에 객체를 찍을때 누수가 발생하였는데 첫번째 인자로 객체가 전달되면 해당 객체를 출력하기 위해 객체를 복사하게 되는데 이때, 메모리 누수가 발생할 수 있으므로 주의해야 합니다.
메모리 누수가 발생하는 요인은 이 밖에도 이벤트 핸들링, 객체 초기화 문제 등 여러 요소들이 존재하므로 개발 당시에 무분별한 복사와 변수를 만들지 않아야 한다는 것을 생각하는 계기가 되었으며 코어 로직까지 파고들며 내부의 동작원리와 문제점 등을 알아가는데 도움이 되었습니다. 이 경험을 토대로 다음 프로젝트 개발에서는 좀 더 성능을 염두한 코드를 작성할 것입니다.
* 참고자료
- https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=jjoommnn&logNo=221092926766
- https://greensock.com/docs/v3/GSAP/gsap.killTweensOf()
- https://engineering.linecorp.com/ko/blog/vue-memory-leak-analysis/
감사합니다.
'개발' 카테고리의 다른 글
Grafana K6를 이용하여 부하 테스트 해보기 (0) | 2024.10.27 |
---|---|
객체 지향 5원칙을 준수하여 리팩토링 하기 (with. Kafka Consumer) (3) | 2024.03.16 |
Referer Policy 적용하기 (0) | 2022.04.29 |
웹 렌더링 과정 (0) | 2021.09.13 |
SPA(Single Page Application)란? (0) | 2021.07.13 |
댓글