무한스크롤 리스트 DOM 가상화 기법으로 성능 최적화 하기

woohyuk·2025년 1월 2일
0
post-thumbnail

개요

상품 리스트를 보여주는 페이지에 무한 스크롤을 적용하였다.
일반적인 무한 스크롤은 스크롤이 특정 지점에 도달하면 다음 페이지의 상품 데이터를 추가로 로드하는 방식으로 동작한다.
이 과정에서 요청된 상품 페이지가 많아질수록 화면에 렌더링되는 상품 DOM 요소도 함께 증가하게 된다.

PC 웹에서는 이러한 방식으로 테스트할 때 별다른 문제가 없었다.
그러나 성능이 상대적으로 낮은 모바일 웹이나 웹뷰 앱 환경에서는 스크롤을 내릴수록 버벅임 현상이 발생하거나,
심한 경우 브라우저가 멈추는 상황까지 확인할 수 있었다.

특히 상품 상세 페이지로 진입 후 뒤로 가기 버튼을 눌렀을 때,
불러왔던 DOM 요소들이 재렌더링되면서 성능 저하가 심각하게 체감되는 문제도 발견되었다.

이러한 문제를 해결하기 위해 상품 리스트 영역을 가상화하여 최적화하는 방식을 도입하였다.

DOM 가상화란?

리액트 공식문서(구)를 보면 다음과 같이 나와있다.

한마디로, 사용자가 보고 있는 화면(viewport)에 해당하는 영역의 DOM만 렌더링하고,
화면 밖의 보이지 않는 영역의 DOM은 삭제하는 기법이다.
이를 그림으로 표현하면 다음과 같다.

viewport 영역 내에 있는 Product3, 4, 5만 화면에 렌더링되며,
Product1, 2, 6, 7은 viewport 영역 밖에 있기 때문에 렌더링되지 않는다.
이후 요소가 viewport 영역에 진입하면 해당 요소를 렌더링하는 방식이다.

이 방식은 수천 개의 요소를 렌더링해야 하는 상황에서도,
화면에 보이는 부분만 렌더링되기 때문에 데이터가 많아질수록
압도적인 성능 최적화 효과를 제공한다.
사실상 대규모 데이터를 다루는 경우 필수적으로 적용해야 할 방법이다.

라이브러리 선택

DOM 가상화 라이브러리를 선택할때 고려한 요소는 다음과같다

  • 원하는 기능
  • 편리한 interface 제공
  • 라이브러리 업데이트 주기


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}
  />
  )
  • useWindowScroll : 요소 스크롤이 아닌 브라우저 스크롤 사용
  • totalCount : 노출이될 products의 갯수
  • itemContent : 상품 Element (totalCount를 기반으로 생성됨)
  • endReached : 스크롤이 가장 아랫부분에 닿을 때 실행되는 함수 (이때 상품을 추가해주는 로직을 작성하여 무한스크롤 기능 구현 가능)
  • initialTopMostItemIndex : 초기 렌더링 시 표시할 가장 상단 아이템의 인덱스를 설정하는 속성, 상품 상세페이지 진입 후 뒤로가기로 스크롤 위치를 복원하고 싶을때 사용하면된다. (단, 상품데이터가 캐싱이 되어있다는 가정하에)

자세한 내용은 react-virtuoso 공식문서 혹은 react-virtuoso example 예제 페이지를 참고해보면 좋을 것 같다.

성능 분석

이 글에서 가장 핵심적인 부분이다.
적용한 방법들이 실제로 어느 정도의 성능 개선을 이루었는지, 구체적인 수치로 확인해 보자.

성능 측정은 적용 전과 후로 나누어 진행했으며, 스크롤을 지속적으로 내려 상품 데이터를 추가할 때의 성능 변화를 분석하였다.

Lighthouse (모드: 기간)

  • 적용 전
  • 적용 후

성능 탭

  • 적용 전
  • 적용 후

React-Devtools Profiler

  • 적용 전

  • 적용 후

최종 결과

  • Total Blocking Time : 2,880ms => 120ms (95.83% 개선)
  • 렌더링 : 1093ms => 507ms (53.61% 개선)
  • 페인팅 : 658ms => 320ms (51.37% 개선)
  • Product List 컴포넌트 렌더링 : 232.7ms => 1.5ms (99.36% 개선)

또한 모바일에서 상품 리스트를 스크롤할 때 버벅임 현상이 사라져 더욱 쾌적한 UX를 제공하는 듯한 느낌을 받았다.

정리

많은 웹 서비스에서 사용자 경험을 개선하기 위해 페이지네이션 대신 무한 스크롤을 도입하는 사례를 쉽게 찾아볼 수 있다. 물론, 서비스의 특성과 사용자 요구에 따라 적합한 방식은 달라질 수 있다.

그렇기에 단순히 기능적인 측면에서 새로운 방식을 도입하는 것에 그치지 않고, 잠재적인 문제를 미리 예측하고 이를 해결하려는 태도가 무엇보다 중요하다. 이번 무한 스크롤 도입 사례에서도 사용자 경험을 개선하려는 의도로 시작했지만, 오히려 의도와 달리 사용자 경험을 저해하는 결과를 초래했다는 점을 알게 되었다. 이러한 경험은 새로운 기능이나 기술을 도입할 때 문제 해결 중심의 접근 방식을 더욱 상기시켜야 한다는 교훈을 남겼다.

profile
기록하는 습관을 기르자

0개의 댓글

관련 채용 정보