React-window 스크롤 브라우저 연동 후기

JIHUN_K·2023년 11월 8일
0

포켓몬 도감

개발 기간

2023.08.01 ~ 2023.08.12 (완성 후 지속적인 업데이트 및 배포 중)

📜 서비스 내용

포켓몬과 포켓몬 정보를 찾아볼 수 있는 도감 서비스입니다. 이 프로젝트는 대용량 렌더링의 최적화에 대해 노력했던 프로젝트입니다.

위 서비스는 3가지로 나누어 집니다.

  • 포켓몬 도감 내 등록된 포켓몬 리스트
  • 포켓몬 상세 페이지
  • 포켓몬 타입별 보기

⚙️ 기술 스택

Typescript, React, Recoil,TailwindCss

💻 핵심 개발 내용

무한 스크롤 페이지에서 React-window를 활용한 성능 최적화 경험

  • 1차 배포는 포켓몬 도감 내에 등록된 포켓몬 리스트를 무한 스크롤로 페이지로 보여주게 개발했습니다. 하지만 무한스크롤의 경우 초기 로드 속도는 좋았지만 스크롤을 내릴 수록 잦은 요청과 그에 따른 레이아웃변경으로 성능과 UX가 좋지 않은 단점이 나왔습니다.

[성능 최적화를 위해 생각했던 방안]

  1. 첫번째는 각 요청 당 데이터 limit를 높여 요청 횟수를 줄이는 것입니다. 초기에는 뷰포트 기준을 삼아 20개~100개정도의 요청하면서 초기 로드 속도는 빠르지만 갈수록 잦은 요청과 페인트때문에 성능이 저하되는게 눈에 보였습니다. 때문에 잦은 요청을 줄인다면 잦은 렌더링과 페인트 변화도 줄기 때문에 성능이 향상할 것입니다.
  2. 두번째는 가상 리스트를 활용하는 것입니다. 성능저하의 원인은 잦은 렌더링과 렌더링되는 노드량에 따른 페인트입니다. 노드가 많아지면 많아질 수록 레이아웃 계산이 많아지기 때문에 성능저하의 원인이 됩니다. 이를 가상 리스트라는 뷰포트 내에 표시되는 콘텐츠만 렌더링하여 메모리 사용과 렌더링 성능을 개선할 수 있습니다.

[React-window를 선택한 이유]

  1. 필요한 기능이 많지 않았습니다.
  2. 번들사이즈가 6.4kb로 react-virtualized의 27.3kb에 비해 가벼웠습니다.
  3. 릴리즈 또한 자주 이뤄줘 현 react 18 버전이나 다른 라이브러리와 충돌과의 문제점 또한 걱정할 필요가 없어 안전했습니다.

[React-window 스크롤 분리 개선]

윈도우와 콘텐츠 스크롤 분리 현상

  • 가상 리스트를 활용하면서 가장 큰 문제는 스크롤 분리 현상이었습니다. 초기에는 라이브러리의 콘텐츠 리스트만 렌더링될 때는 스크롤이 하나였지만, 헤더와 메뉴와 같이 여러 노드가 추가되면서 콘텐츠와 브라우저의 높이값이 달라지면서 윈도우 스크롤과 분리되었습니다. 이에 대한 직접적인 지원은 라이브러리에서 제공되지 않았습니다

react-window 스크롤 분리 개선 후 윈도우 스크롤과 연동하기

  • 스크롤은 주어진 컨테이너의 제한된 높이 값으로, 컨텐츠 내용이 화면에서 보이지 않는 부분을 볼 수 있도록 하는 기술입니다. 그 말은 즉 제한된 컨테이너의 높이를 전체 콘텐츠의 높이값에 대입한다면 스크롤이 생기지 않게됩니다.
  • 이를 적용하면 react-window의 스크롤은 사라져 윈도우 스크롤 하나만 생성 됩니다. 또한 자체 엘리먼트가 스크롤되지 않게 됩니다.
  • 여기서 중요한 점은 브라우저 스크롤로는 react-window에 숨겨진 콘텐츠를 보이게 할 수 없다는 것입니다. react-window 내부의 스크롤 동작에만 반응해 노드의 위치를 계산하여 적합한 콘텐츠를 보여주기 때문입니다. 라이브러리의 내부 동작을 살펴보면 CSS 속성 중 position: absolute를 사용하여 스크롤 위치에 맞는 항목을 조정하는 것을 확인할 수 있었습니다.

따라서 이제는 window의 스크롤을 react-window와 연동할 필요가 있었습니다.

[React-window 스크롤 연동 및 스크롤 오차값 계산]

export default function useReactWindowScroll() {
  const innerRef = useRef<HTMLElement | null>(null);
  const outerRef = useRef<HTMLElement | null>();
  const ref = useRef<FixedSizeList | null>(null);

  useEffect(() => {
	  //쓰로틀링 처리 해 스크롤 이벤트 처리량 최적화.
    const handleWindowScroll = throttle(() => {
      const { offsetTop = 0 } = outerRef.current || { offsetTop: 0 };
      const scrollMin = 0;
      let windowScrollY = window.scrollY;
			//양쪽 스크롤 높이 차이값
      let scrollOffset = windowScrollY - offsetTop;

      let isScrollRender =
        scrollOffset >= scrollMin && scrollOffset <= document.body.scrollHeight;

      //스크롤 위로 올릴 시 쓰로틀링 때문에 생기는 오차값 보정
      if (ref.current && windowScrollY === 0) {
        ref.current.scrollTo(scrollOffset);
        return;
      }

      if (ref.current && windowScrollY === document.body.scrollHeight) {
        ref.current.scrollToItem(1000);
      }

      //타겟지점 + 스크롤 가능 범위 판별
      if (ref.current && isScrollRender) {
        //브라우저 스크롤이 움직이면 react-window의 List 엘리먼트 스크롤 내리기

        ref.current.scrollTo(scrollOffset);
      }
    }, 10);

    handleWindowScroll();
    window.addEventListener("scroll", handleWindowScroll);
    window.addEventListener("resize", handleWindowScroll);

    return () => {
      window.removeEventListener("scroll", handleWindowScroll);
      window.removeEventListener("resize", handleWindowScroll);
    };
  }, []);

  return { ref, outerRef, innerRef };
}
  • 윈도우 스크롤로 react-window 스크롤을 조작하기 위해 useRef를 사용하여 react-window DOM의 스크롤 위치를 탐색했습니다.
  • 또한 scrollTo함수를 활용하여 스크롤을 조정하고 scrollOffset을 이용하여 양쪽 스크롤 높이 값의 오차를 보정했습니다
  • 이로서 스크롤 연동 이벤트를 구현 해 성공적으로 윈도우 스크롤로 react-window 콘텐츠를 조작했습니다.

[성과]

  • 가상리스트 스크롤 분리 트러블 ⇒ 브라우저 스크롤과 연동해 UX 개선.
  • JS 메모리 사용률 200% 개선 해 최적화.
  • 전체 콘텐츠 로딩 API 응답 속도 300% 향상.
  • 뷰포트당 리스트 렌더링 속도 300% 향상.
  • LCP 속도 (Largest Contentful Paint) ⇒ 7.8 초 ⇒ 2.9초 향상
  • CLS 속도 (Cumulative Layout Shift) ⇒ 0.258 ⇒ 0.093 향상
profile
한발 한발 내딛자

0개의 댓글