상품 리스트를 보여주는 페이지에 무한 스크롤을 적용하였다.
일반적인 무한 스크롤은 스크롤이 특정 지점에 도달하면 다음 페이지의 상품 데이터를 추가로 로드하는 방식으로 동작한다.
이 과정에서 요청된 상품 페이지가 많아질수록 화면에 렌더링되는 상품 DOM 요소도 함께 증가하게 된다.
PC 웹에서는 이러한 방식으로 테스트할 때 별다른 문제가 없었다.
그러나 성능이 상대적으로 낮은 모바일 웹이나 웹뷰 앱 환경에서는 스크롤을 내릴수록 버벅임 현상이 발생하거나,
심한 경우 브라우저가 멈추는 상황까지 확인할 수 있었다.
특히 상품 상세 페이지로 진입 후 뒤로 가기 버튼을 눌렀을 때,
불러왔던 DOM 요소들이 재렌더링되면서 성능 저하가 심각하게 체감되는 문제도 발견되었다.
이러한 문제를 해결하기 위해 상품 리스트 영역을 가상화하여 최적화하는 방식을 도입하였다.
리액트 공식문서(구)를 보면 다음과 같이 나와있다.
한마디로, 사용자가 보고 있는 화면(viewport)에 해당하는 영역의 DOM만 렌더링하고,
화면 밖의 보이지 않는 영역의 DOM은 삭제하는 기법이다.
이를 그림으로 표현하면 다음과 같다.
viewport 영역 내에 있는 Product3, 4, 5만 화면에 렌더링되며,
Product1, 2, 6, 7은 viewport 영역 밖에 있기 때문에 렌더링되지 않는다.
이후 요소가 viewport 영역에 진입하면 해당 요소를 렌더링하는 방식이다.
이 방식은 수천 개의 요소를 렌더링해야 하는 상황에서도,
화면에 보이는 부분만 렌더링되기 때문에 데이터가 많아질수록
압도적인 성능 최적화 효과를 제공한다.
사실상 대규모 데이터를 다루는 경우 필수적으로 적용해야 할 방법이다.
DOM 가상화 라이브러리를 선택할때 고려한 요소는 다음과같다
react-virtuoso를 선택한 이유는 사용성이 더 직관적이라고 느껴졌기 때문이다.
처음에는 tanstack/virtual-core를 적용하려 했으나, 자식 노드의 동적 높이 계산과 관련된 이슈가 있어 react-virtuoso로 방향을 바꾸게 되었다.
npm i react-virtuoso
const products = // product 리스트
const addProducts = {
// 상품 데이터 추가하는 로직
}
const getRow = (rowIndex: number) => {
return <ProductCard index={rowIndex}/>;
};
const virtuosoIndex = sessionStorage.getItem(VIRTUOSO_INDEX)
return (
<Virtuoso
useWindowScroll
totalCount={products.length}
itemContent={getRow}
endReached={addProducts}
initialTopMostItemIndex={virtuosoIndex}
/>
)
자세한 내용은 react-virtuoso 공식문서 혹은 react-virtuoso example 예제 페이지를 참고해보면 좋을 것 같다.
이 글에서 가장 핵심적인 부분이다.
적용한 방법들이 실제로 어느 정도의 성능 개선을 이루었는지, 구체적인 수치로 확인해 보자.
성능 측정은 적용 전과 후로 나누어 진행했으며, 스크롤을 지속적으로 내려 상품 데이터를 추가할 때의 성능 변화를 분석하였다.
적용 전
적용 후
또한 모바일에서 상품 리스트를 스크롤할 때 버벅임 현상이 사라져 더욱 쾌적한 UX를 제공하는 듯한 느낌을 받았다.
많은 웹 서비스에서 사용자 경험을 개선하기 위해 페이지네이션 대신 무한 스크롤을 도입하는 사례를 쉽게 찾아볼 수 있다. 물론, 서비스의 특성과 사용자 요구에 따라 적합한 방식은 달라질 수 있다.
그렇기에 단순히 기능적인 측면에서 새로운 방식을 도입하는 것에 그치지 않고, 잠재적인 문제를 미리 예측하고 이를 해결하려는 태도가 무엇보다 중요하다. 이번 무한 스크롤 도입 사례에서도 사용자 경험을 개선하려는 의도로 시작했지만, 오히려 의도와 달리 사용자 경험을 저해하는 결과를 초래했다는 점을 알게 되었다. 이러한 경험은 새로운 기능이나 기술을 도입할 때 문제 해결 중심의 접근 방식을 더욱 상기시켜야 한다는 교훈을 남겼다.