[React] 무한스크롤 구현하기

이지·2023년 9월 13일
0

무한스크롤이란?

무한스크롤이란 화면을 아래로 스크롤 하였을 때, 콘텐츠가 끊이지 않고 계속해서 로드되는 것을 의미합니다. 다음 버튼을 눌러 페이지가 로딩되는 것(페이지네이션)을 기다리지 않아도 되어 사용자 경험을 높일 수 있습니다.
인스타그램이나 유튜브에서도 무한스크롤을 사용하고 있는데요. 끊임없이 새로운 콘텐츠가 아래에서 로드되는 것을 알 수 있습니다.

무한스크롤을 구현하는 방식에는 여러 가지가 있지만, 그중에서도 저는 intersection observer API 를 이용하여 구현하였습니다.

Intersection Observer API

뷰포트와 대상 요소 사이의 교차 상태를 감지하는 기능을 제공하여 무한 스크롤과 같은 기능을 구현하는 데 유용합니다.

  • IntersectionObserver(callback, options)
    교차 상태 변화를 관찰하기 위한 Observer 객체입니다. 생성자로 콜백 함수와 옵션을 전달하여 생성됩니다.
  • observe()
    대상 요소를 관찰하도록 설정합니다. 대상 요소가 교차 상태에 들어오거나 나갈 때 콜백 함수가 호출됩니다.
  • disconnect()
    Intersection Observer의 관찰을 중지하고 모든 리소스를 해제합니다.

options

  • root
    교차 상태를 관찰할 루트 요소
    기본 값은 null 이며, 이 경우 뷰포트를 기준으로 교차상태를 감지합니다.
    다른 요소를 지정하면 해당 요소의 경계 내에서 교차 상태를 감지합니다.
  • threshold
    교차 상태가 변경되기 위한 대상 요소와 루트 요소 사이의 비율을 나타냅니다.
    0~1의 숫자나 숫자배열로 설정가능합니다.
    예를 들어, 0.5는 대상 요소가 최소한 반 이상이 뷰포트 안에 들어와야 교차 상태로 판단합니다.
  • rootMargin
    루트와 대상 요소 사이의 여백(마진)을 설정합니다. 여백을 통해 실제 보여지는 영역보다 약간 더 크거나 작게 설정하여 교차 상태 변화 이벤트가 발생하는 시점을 조절할 수 있습니다.

code

일단 저는 무한스크롤은 여러 페이지에서 재사용될 수 있기 때문에 커스텀 훅으로 작성하였습니다.

// useIntersectionObserver 커스텀 훅
const defaultOption = {
  root: null,
  threshold: 0.5,
  rootMargin: "0px",
};

export default function useIntersectionObserver(onIntersect, options, isLoading) {
  const targetRef = useRef(null);
  const checkIntersect = useCallback(([entry]) => {
    if (entry.isIntersecting) {
      onIntersect();
    }
  }, []);

  useEffect(() => {
    let io;
    if (targetRef.current && isLoading) {
      io = new IntersectionObserver(checkIntersect, {
        ...defaultOption,
        ...options,
      }); // 관찰자 생성
      io.observe(targetRef.current); // 관찰할 대상 등록
    }

    return () => {
      io && io.disconnect();
    };
  }, [targetRef, isLoading]);

  return targetRef;
}
// Home.jsx
export default function Home() {
  const [productListData, setProductListData] = useState([]);
  const [page, setPage] = useState(0);
  const [isLoading, setIsLoading] = useState(true);
  const getProductList = async (page) => {
    try {
      const res = await getProductList(page); // 상품리스트 불러오는 API
      setProductListData((prev) => [...prev, ...res.data.results]);
    } catch (err) {
      if (err.response.data.detail === "페이지가 유효하지 않습니다.") // 더이상 불러올 데이터가 없는 경우
        setIsLoading(false);
    }
  };
  const targetRef = useIntersectionObserver(
    () => {
      setPage((prev) => prev + 1); 
    },
    { threshold: 0.5 },
    isLoading
  );

  useEffect(() => {
    if (page === 0) return;
    getProductList(page);
  }, [page]);

  return (
    <MainLayout>
      <ProductList productListData={productListData} />
      <div ref={targetRef} />
    </MainLayout>
  );
}
  • <div ref={targetRef} 가 화면에 감지되면 observer가 setPage((prev) => prev + 1) 을 실행하여 다음 페이지의 데이터를 가져옵니다.
  • isLoading을 통해 모든 데이터가 다 불러와지면 관찰자는 참조하는 대상과의 연결을 끊습니다.
// ProductList.jsx
export default function ProductList({ productListData }) {
  return (
    <ProductUl>
      {productListData.map((product) => (
        <li key={product.product_id}>
           <ProductImg src={product.image} alt="상품이미지" />
           <ProductName>{product.product_name}</ProductName>
           <ProductPrice>{product.price}</ProductPrice>
        </li>
      ))}
    </ProductUl>
  );
}

0개의 댓글