안녕하세요! 지난번 무시무시했던 Git 충돌 지옥에서는 무사히 빠져나오셨나요? 시뻘건 터미널 화면을 극복하신 여러분을 환영합니다! 오늘은 겉보기엔 아무런 에러 메시지도 없지만, 유저가 우리 서비스를 오래 켜둘수록 브라우저를 서서히 죽여가는 웹 애플리케이션의 '조용한 살인마'에 대해 이야기해 볼까 합니다.
처음 페이지에 들어갔을 때는 클릭도 빠릿빠릿하고 애니메이션도 부드럽습니다. 그런데 10분, 30분 시간이 지날수록 왠지 모르게 스크롤이 버벅거리고, 노트북 쿨러가 비명을 지르기 시작하더니... 결국 브라우저 탭이 '앗, 이런!' 화면과 함께 장렬하게 전사해 버리는 경험. 다들 한 번쯤 겪어보셨죠? 네, 맞습니다. 바로 '메모리 누수(Memory Leak)' 현상입니다.
에러 로그조차 남기지 않아 개발자를 미치게 만드는 이 조용한 살인마가 도대체 왜 생기는지, 그리고 어떻게 범인을 색출해 낼 수 있는지 실전 팁을 방출해 보겠습니다.

1. 메모리 누수, 도대체 왜 일어나는 걸까?
자바스크립트에는 '가비지 컬렉터(Garbage Collector)'라는 아주 부지런한 청소부가 살고 있습니다. 우리가 변수를 만들고 다 쓴 뒤에 방치하면, 이 청소부가 알아서 "아, 이거 이제 안 쓰는 데이터네?" 하고 메모리 쓰레기통에 버려주죠. 덕분에 우리는 메모리 관리에 크게 신경 쓰지 않고 코딩을 할 수 있습니다.
그런데 가끔 우리가 코드를 잘못 짜면, 이 청소부가 헷갈리기 시작합니다. 화면에서 이미 사라진 컴포넌트인데, 어딘가에서 계속 그 컴포넌트의 데이터를 꽉 붙잡고(참조하고) 있는 경우가 생기거든요. 청소부 입장에서는 "어? 아직 누가 쓰고 있나 보네. 버리면 안 되겠다!" 하고 쓰레기를 버리지 않고 계속 쌓아둡니다. 이 쓰레기들이 브라우저 메모리에 산더미처럼 쌓여서 결국 컴퓨터가 뻗어버리는 현상, 이것이 바로 메모리 누수입니다.
2. 단골 범인 1: 깜빡하고 안 지운 이벤트 리스너
실무에서 메모리 누수를 일으키는 가장 흔한 범인은 바로 window나 document에 달아둔 이벤트 리스너입니다.
리액트(React)를 예로 들어볼까요? 특정 페이지(컴포넌트)에 들어왔을 때, 스크롤 위치를 추적하려고 window.addEventListener('scroll', ...)을 달아두었습니다. 그런데 유저가 다른 페이지로 이동해서 해당 컴포넌트가 화면에서 사라졌음에도 불구하고, removeEventListener로 이벤트를 지워주지 않으면 어떻게 될까요? 눈에 보이지 않는 유령 리스너가 백그라운드에서 계속 스크롤 이벤트를 감지하며 메모리를 갉아먹게 됩니다.
3. 단골 범인 2: 길 잃은 타이머와 끈질긴 클로저(Closure)
두 번째 단골손님은 setInterval 같은 타이머 함수입니다. 1초마다 실시간 주식 가격을 가져오기 위해 타이머를 켜두었는데, 페이지를 벗어날 때 clearInterval로 타이머를 끄지 않았다면? 이 타이머는 브라우저 탭이 닫힐 때까지 영원히 백그라운드에서 API를 호출하며 메모리를 터뜨릴 준비를 합니다.
또한, 자바스크립트의 강력한 무기인 클로저(Closure)도 조심해야 합니다. 내부 함수가 외부 함수의 변수를 계속 참조하고 있으면, 외부 함수가 실행이 끝났음에도 가비지 컬렉터가 그 변수를 청소하지 못합니다. 잘못 설계된 클로저는 메모리 누수의 아주 흔한 원인이 되곤 하죠.

4. 실전 해결책: 크롬 개발자 도구(Memory 탭)로 범인 색출하기
그렇다면 눈에 보이지 않는 이 쓰레기들을 어떻게 찾아낼 수 있을까요? 다행히 우리에겐 최고의 탐정 도구, 크롬 개발자 도구의 'Memory' 탭이 있습니다.
가장 유용한 기능은 'Heap Snapshot(힙 스냅샷)'입니다. 메모리 누수가 의심되는 행동(예: 페이지 A와 B를 여러 번 왔다 갔다 하기)을 하기 전과 후에 스냅샷을 찍어서 두 개를 비교(Comparison)해 보는 겁니다. 만약 화면에서 지웠는데도 여전히 메모리 공간을 차지하고 있는(Delta 값이 +로 계속 늘어나는) 객체나 DOM 노드가 있다면, 바로 그 녀석이 범인입니다! 여기서 추적된 변수명이나 함수명을 단서로 코드를 수정해 주면 됩니다.
마무리하며
오늘은 겉으로는 에러를 뱉지 않아 더 무서운 조용한 살인마, '메모리 누수(Memory Leak)'의 원인과 해결 방법에 대해 알아보았습니다. 가비지 컬렉터가 아무리 똑똑해도, 우리가 붙잡고 놓아주지 않는 데이터까지 강제로 버려주지는 않습니다. 컴포넌트가 화면에서 사라질 때(unmount 될 때) 이벤트 리스너와 타이머를 깔끔하게 해제해 주는 습관 하나만 들여도, 유저의 브라우저가 버벅거리는 끔찍한 일은 대부분 막을 수 있습니다. 오늘 알아본 크롬 개발자 도구의 힙 스냅샷 기능도 꼭 한 번 실무에 적용해 보시길 바랍니다!
[다음 포스팅 예고] 메모리 누수라는 보이지 않는 적을 무사히 처치하셨나요? 다음번에는 우리를 괴롭히는 또 다른 형태의 '성능 저하' 범인을 잡아보겠습니다. 프론트엔드와 백엔드를 막론하고 모두를 고통받게 하는, "느려터진 API 응답 속도, N+1 문제 파악하고 해결한 경험담" 편으로 돌아오겠습니다. 데이터베이스에 쿼리 한 번 날렸을 뿐인데 갑자기 수백 개의 쿼리가 쏟아져 나오는 기적(?)을 경험해 보셨다면, 다음 글도 절대 놓치지 마세요! 오늘도 버그 없는 평온한 하루 보내시길 응원합니다!
[실무에서 복붙해 쓰는 에러 해결 및 트러블슈팅 가이드 #06] API 응답이 왜 이렇게 느려? 쿼리 폭
안녕하세요! 지난번 화면을 얼려버리는 조용한 살인마, 메모리 누수 추적기는 무사히 마치셨나요? 눈에 보이지 않는 쓰레기들을 청소하시느라 정말 고생 많으셨습니다. 자, 오늘 다룰 주제는 프
code-bricks.tistory.com