어느날, 개발 중이던 페이지가 너무 버벅이는 느낌이 들었다. 이상하다고 생각하던 찰나, 메모리 사용량을 우연치않게 확인해보니 무려 700MB 가 넘어가고 있었다. 분명 페이지를 처음 로드했을 땐 200MB 정도였는데 말이다.
한번 의심이 들자 제대로 확인해보고 싶어졌다. 페이지를 켜둔 채로 6시간 후에 다시 확인해보니, 메모리 사용량은 무려 1.2GB 까지 올라가 있었다. 분명 뭔가 문제가 있다.
크롬 개발자 도구의 Performanc 탭에서 메모리 스냅샷을 찍어보았다.
회색은 가비지콜렉터가 수집해간 메모리, 파란색은 누수된 메모리이다. 슬슬 메모리 누수(Memory Leak)
가 의심되기 시작했다.
페이지에서 사용 중인 슬라이더는 Swiper.js 였고, 특히 loop
, onSlideChange
, observer
, observeParents
옵션들이 의심됐다.
버튼 클릭 핸들러는 다음과 같이 useCallback
으로 감싸주었다.
const handleNextClick = useCallback(() => {
const next = (activeIndex + 1) % champions.length;
setActiveIndex(next);
}, [activeIndex, champions.length]);
useCallback
은 컴포넌트가 리렌더링될 때마다 새로운 함수를 만드는 것이 아니라, 의존성 배열이 바뀌지 않는 이상 기존 함수를 재사용한다. 즉, 불필요한 렌더링을 막고 성능을 조금이라도 개선할 수 있다.
Swiper 의 observer
, observeParents
옵션은 DOM 변화가 있는지 계속 감시한다. 반응형 대응에는 좋지만, 계속 DOM을 지켜보고 있는 자체가 메모리 사용량을 야금야금 늘리는 원인이 될 수도 있다고 생각했다.
그래서 나는 반응형 대신 고정형 슬라이드로 구조를 바꿨다.
useEffect(() => {
const interval = setInterval(() => {
setActiveIndex((prev) => (prev + 1) % champions.length);
}, 3000);
return () => clearInterval(interval);
}, [champions.length]);
useEffect(() => {
bgSwiperRef.current?.slideTo(activeIndex);
contentSwiperRef.current?.slideTo(activeIndex);
}, [activeIndex]);
Swiper 컴포넌트도 간결하게 정리했다. 불필요한 옵션을 제거하고, loop
, autoplay
, observer
없이도 기본적인 기능만으로도 충분히 매끄러운 동작이 가능했다.
수정 후에는 슬라이드가 인터랙션을 할 때만 메모리를 사용하고, 일정 시간이 지나도 메모리 사용량이 눈에 띄게 증가하지 않았다. 메모리 누수를 어느정도 잡은 것이라 생각한다.
이번 경험을 통해 내가 얻은 교훈은 다음과 같다:
observer
, autoplay
, loop
같은 Swiper 편의기능이 때로는 독이 될 수 있다.
메모리 문제는 눈에 안 보이는 누수가 많기 때문에, 직접 확인하고 스냅샷을 찍어봐야 확실해진다.
불필요한 렌더링을 줄이는 useCallback
같은 훅도 작은 도움이 된다.
물론 아직 완벽하게 잡지는 못했다. 사실 어느정도 까지 잡아야 될지는 잘모르겠다. 그래도 어느정도 잡은거에 대해서 만족할 수 있었다.
무심코 넘어갔던 슬라이더 하나가, 1.2GB의 메모리 누수를 만들고 있었다.
앞으로 이 useCallback
이라던지 useMemo
에 대해서 좀 더 관심을 가지고 잘 활용해 봐야할 것 같다.