페이지가 새로고침된다는 오류가 접수돼서 메모리 최적화를 진행했다.
사진이 많은 팝업 컴포넌트를 열면 페이지가 새로고침된다는 오류가 접수되었다.
가설 1
사진이 많아서 메모리를 너무 많이 사용한다.
처음에는 오류가 발생한 동작을 타임라인으로 메모리 검사를 진행했다. 그런데 메모리를 많이 차지하는 부분은 대부분 라이브러리 내부에서 발생하고 있었다.
사진의 개수를 줄이면 새로고침 현상이 발생하지 않았다. 그래서 사진과 관련된 부분을 집중적으로 살펴보았으나, 대부분 UI 라이브러리와 Swiper를 사용한 코드였다.
가설 2
이미 메모리가 많이 사용 중인 상태에서 사진이 있는 팝업까지 더해져 한계치에 도달한 것이다.
오류를 캡처한 영상을 검토한 결과, 페이지에서 스크롤을 내린 후 팝업 트리거를 클릭하자마자 새로고침이 발생했다. 이를 통해 메모리를 이미 많이 사용한 상태에서 팝업이 추가되면서 OOM(Out of Memory)이 발생한 것으로 추측했다.
페이지에서 팝업을 열지 않고 가만히 둔 상태로 메모리 검사를 진행해 보니 메모리 그래프가 꾸준히 증가하는 것을 확인했다.
특히 메모리가 급격히 증가하는 부분을 분석해 보니, 디데이를 카운트다운하는 로직에서 문제가 발생하고 있었다.
해당 로직의 일부이다. 1초마다 setInterval로 실행되며, innerHTML을 통해 DOM을 조작하는 방식이었다. 생략된 부분에선 타이머가 useEffect 내부에서 실행되며 클린업 함수도 작성되어 있다.
const timer = setInterval(() => {
if (timerElement) {
const { time, dDay } = showRemaining();
const timerNumbers = timerElement.querySelectorAll('.timer__number');
const timerInfoToArray = Object.entries(time);
const dDayElement = timerElement.querySelector('.d-day');
if (dDayElement) {
dDayElement.innerHTML = `${dDay}`;
}
Array.from(timerNumbers).forEach((item, index) => {
item.innerHTML = `${timerInfoToArray[index][1]}`;
});
}
}, 1000);
문제는 여기서 innerHTML을 사용하면서 메모리 할당이 불필요하게 반복되었다는 점이다. 😱
- 기존 내용 삭제:
innerHTML
을 사용하면 해당 요소의 모든 기존 자식 노드가 삭제됩니다.- 새로운 DOM 트리 생성: 새로운 HTML 문자열을 파싱 하여 해당 요소의 새로운 자식 노드들이 생성되고, DOM 트리에 삽입됩니다.
- 메모리 재할당: 이 과정에서 기존의 DOM 노드들은 해제되고, 새로운 DOM 노드들이 메모리에 할당됩니다.
단순 텍스트만 수정하고 싶다면 기존의 노드를 새로 만들지 않고, 기존 텍스트 노드를 업데이트하는 방식으로 동작하는 textContent
또는 innerText
를 사용하는 것이 더 효율적이라고 한다.(나는 사용하지 않았다.)
Next.js를 사용하기 때문에 DOM을 직접 조작할 필요가 없어 상태로 관리
할 수 있도록 로직을 수정했다.
또한, 매초 리렌더링이 발생하는 문제를 해결하기 위해 React.memo()
를 사용해 자주 변하지 않는 일, 시간, 분 등의 값이 매번 렌더링 되지 않도록 최적화했다.
JS 힙 메모리 그래프는 아직 개선이 필요하지만, 노드 메모리 사용량이 크게 감소하면서 문제를 많이 해결할 수 있었다.
JS 힙 메모리 최적화는 다음 글에서 해결되었다.
mui 번들 크기 줄이기로 시작했으나 없애버리고 메모리 줄이기