무한스크롤(infinite scroll) 구현 (Intersection Observer API)

프로젝트를 진행하면서 페이징 기능이 필요하여 무한 스크롤로 기능을 구현해보았습니다.

무한 스크롤 이란?

무한 스크롤은 많은 양의 콘텐츠를 스크롤 해서 볼 수 있게 만든 기술입니다.
한 페이지를 아래로 스크롤 하면 새로운 화면을 보여줍니다.

무한스크롤 장점

  • 사용자 참여 및 콘텐츠 탐색이 쉽습니다.
  • 무한 스크롤이 클릭하는 것보다 더 나은 사용자 경험을 제공합니다.
  • 모바일에 적합합니다.

무한스크롤 단점

  • 페이지 성능이 느려집니다.
  • 특정 항목 검색 및 원래 위치로 되돌아오기 힘듭니다.
  • 스크롤 막대가 실제 데이터의 양을 반영하지 못합니다.
  • 푸터를 찾기 어려워집니다.

무한 스크롤 구현하기

JS에서 무한스크롤 구현방법이 여러가지 있지만, 저는 Intersection Observer API를 활용해서 구현했습니다.
Intersection Observer API를 사용한 이유입니다.

  • 호출 수 제한 방법 debounce, throttle을 사용하지 않아도 됩니다.
  • reflow를 하지 않아도 됩니다.

Intersection Observer API 란?

Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document의 viewport 사이의
intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.

intersection 사용 용도

  • 페이지가 스크롤 되는 도중에 발생하는 이미지나 다른 컨텐츠의 지연 로딩.
  • 스크롤 시에, 더 많은 컨텐츠가 로드 및 렌더링되어 사용자가 페이지를 이동하지 않아도 되게 하는 infinite-scroll 을 구현.
  • 광고 수익을 계산하기 위한 용도로 광고의 가시성 보고.
  • 사용자에게 결과가 표시되는 여부에 따라 작업이나 애니메이션을 수행할 지 여부를 결정

Intersection Observer Options

Intersection Observer를 생성할 때는 옵션을 설정할 수 있습니다.
옵션에는 root, rootMargin, threshold가 있습니다.

  • root: 이 옵션에 정의된 Element를 기준으로 Target Element가 노출되었는지 노출 되지 않았는지를 판단합니다.
  • rootMargin: root에 정의된 Element가 가진 마진값을 의미합니다.
  • threshold : Target Element가 root에 정의된 Element에 얼만큼 노출되었을 때 Callback함수를 실행시킬지 정의하는 옵션입니다.

mdn 자료 설명

  • root : 대상 객체의 가시성을 확인할 때 사용되는 뷰포트 요소입니다. 이는 대상 객체의 조상 요소여야 합니다. 기본값은 브라우저 뷰포트이며, root 값이 null 이거나 지정되지 않을 때 기본값으로 설정됩니다.
  • rootMargin : root 가 가진 여백입니다. 이 속성의 값은 CSS의 margin 속성과 유사합니다. e.g. "10px 20px 30px 40px" (top, right, bottom, left). 이 값은 퍼센티지가 될 수 있습니다. 이것은 root 요소의 각 측면의 bounding box를 수축시키거나 증가시키며, 교차성을 계산하기 전에 적용됩니다. 기본값은 0입니다.
  • threshold : observer의 콜백이 실행될 대상 요소의 가시성 퍼센티지를 나타내는 단일 숫자 혹은 숫자 배열입니다. 만일 50%만큼 요소가 보여졌을 때를 탐지하고 싶다면, 값을 0.5로 설정하면 됩니다. 혹은 25% 단위로 요소의 가시성이 변경될 때마다 콜백이 실행되게 하고 싶다면 [0, 0.25, 0.5, 0.75, 1] 과 같은 배열을 설정하세요.
    기본값은 0이며(이는 요소가 1픽셀이라도 보이자 마자 콜백이 실행됨을 의미합니다). 1.0은 요소의 모든 픽셀이 화면에 노출되기 전에는 콜백을 실행시키지 않음을 의미합니다.

적용 코드


import React, { useState, useEffect, useRef, useCallback } from "react";
// ...

// data 배열을 받아 옵니다.
function GridView({ data }) {
  const [list, setList] = useState(data.slice(0, 20));
  const [isScroll, setIsScroll] = useState(false);
  const loadRef = useRef(null);
  const observerRef = useRef(null);

  const onIntersect = useCallback(
    async (entry, observer) => {
      if (entry[0].isIntersecting) {
        observer.unobserve(entry[0].target);
        await new Promise((resolve) => setTimeout(resolve, 1000));
        setList((list) =>
          list.concat(data.slice(list.length, list.length + 10))
        );
        observer.observe(entry[0].target);
      }
    },
    [data]
  );

  useEffect(() => {
    if (loadRef.current &&  list.length !== data.length) {
      setIsScroll(true);
      observerRef.current = new IntersectionObserver(onIntersect, {
        threshold: 0.5,
      });
      if (isScroll) {
        observerRef.current.observe(loadRef.current);
      }
    }
    return () => {
      setIsScroll(false);
      observerRef.current && observerRef.current.disconnect();
    };
  }, [list, data , onIntersect]);

  return (
    <>
  		// ...
      <Load ref={loadRef}>
        {isScroll && <InfiniteLoading type="spin" color="#2f3640" />}
      </Load>
    </>
  );
}

export default GridView;

const Load = styled.div`
  ${({ theme }) => theme.common.flexRow};
  width: 100%;
  background-color: ${({ theme }) => theme.colors.white};
`;
const InfiniteLoading = styled(ReactLoading)`
  width: 3rem;
  height: 3rem;
  z-index: 999;
`;

설명

useEffect 부분

  1. useRef를 활용해 observerRef와 loadRef를 만들어 줍니다.
  • observerRef: intersection Observer 담아줌
  • loadRef: 관찰할 대상(target)
  1. loadRef가 보이면 isScrollintersection Observer를 생성하여 observerRef에 담아 주어 observer가 관찰할 대상(loadRef)을 observer.observe함수로 지정합니다.
    조건문에 list.length !== data.length는 데이터를 모두 불러와 스크롤 마지막이면 더이상 작동하지 말라고 작성해 놓은 코드입니다.
  2. 스크롤을 내려 새로운 목록을 받아오게 되므로 관찰할 대상이 바뀌기 때문에 observer.disconnect 함수로 관찰요소를 없애고 새로 지정하게 됩니다.
  • isScroll useState를 만든 이유는 해당 프로젝트에서 router 이동시 메모리 lack 에러가 발생하여 useEffect의 cleanup을 해주기 위함입니다.

onIntersect 부분

무한 스크롤을 비동기 처럼 보이게 해주기 위해 SetTimeout함수를 이용하여 1초 후 데이터를 불러옵니다.
list.length를 활용해 불러온 데이터의 이후 데이터 10개씩을 concat 메소드를 활용해 list state에 할당해주었습니다.

Load

스크롤할 때 isScroll state를 활용해 스크롤 중일 땐 로딩 중이라는 표시를 주어 사용자가 데이터를 불러올 동안 기다릴 수 있게 해주었습니다.

결과

좋은 웹페이지 즐겨찾기