[React] 무한 스크롤

ss_kim·2024년 11월 19일

데이터를 받아와서 리스트를 구현할 때, 처음 로딩 시간을 줄이고자 모든 데이터를 한 번에 받아오지 않고 여러 번에 나누어(스크롤 할 때마다) 가져오는 것을 구현하기 위해서 TanStack-query의 useInfiniteQuery() 훅을 사용해봤다.




useInfiniteQuery()

const {
  data,
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
  isFetchingNextPage,
  isFetchingPreviousPage,
  promise,
  ...result
} = useInfiniteQuery({
  queryKey,
  queryFn: ({ pageParam }) => fetchPage(pageParam),
  initialPageParam: 1,
  ...options,
  getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
    lastPage.nextCursor,
  getPreviousPageParam: (firstPage, allPages, firstPageParam, allPageParams) =>
    firstPage.prevCursor,
})
  • queryFn

    • 필수값
    • 데이터를 요청하는데 사용하는 함수
  • initialPageParam

    • 필수값
    • 첫 번째 페이지를 가져올 때 사용하는 매개변수
  • getNextPageParam

    • 필수값
    • 다음 페이지 값으로 전달할 매개변수를 반환해야 함
    • 다음 페이지가 없다면 undefined 또는 null을 반환해야 함
  • 훅의 return 반환값

    • data
      • queryFn의 데이터가 저장되어 있는 객체, {pages: [], pageParams: []}
    • data.pages
      • fetch한 데이터가 저장되는 배열
    • data.pageParams
      - 각 페이지를 fetch하는데 전달한 매개변수가 저장된 배열





예제

export function Component() {

  const observerRef = useRef();
  
  const fetchPosts = async (page = 1, items = 10) => {
    return await pb.collection('posts').getList(page, items);
  };
  
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam }) => fetchPosts(pageParam),
    initialPageParam: 1,
    getNextPageParam: (lastPage, allPages) => {
      if (lastPage.length < 10) return undefined;
      return allPages.length + 1;
    },
  });

  useEffect(() => {
    if (!observerRef.current) return;

    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
          fetchNextPage();
        }
      },
      { threshold: 1.0 }
    );

    observer.observe(observerRef.current);

    return () => {
      if (observerRef.current) {
        observer.unobserve(observerRef.current);
      }
    };
  }, [fetchNextPage, hasNextPage, isFetchingNextPage]);

  const allPosts = data?.pages.flat() || [];

  return (
    <div className={S.component}>
      <Outlet context={{ posts: allPosts, isLoading }} />
      <div ref={observerRef} style={{ height: '1px' }}></div>
    </div>
  );
}
  • fetchPosts 함수
    • pocketbase API를 사용해 데이터를 가져오며 page는 몇 번째 페이지인지, items는 페이지당 데이터의 갯수를 나타냄
    • page = 1, items = 10 일 경우, 0~9번 째 데이터를 가져오고, page = 2, items = 10 일 경우, 10~19번 째 데이터를 가져옴
  • getNextPageParam

    • 다음으로 전달할 pageParam 매개변수가 있는지 확인
    • items의 갯수가 10개 미만이라면 다음 페이지는 없는 것으로 생각해 undefind를 반환하고 아니라면 page + 1nextPageParam으로 반환해서 다음 페이지를 가져옴
  • fetchNextPage()

    • 다음 페이지를 가져오는 함수
    • fetchPosts(nextPageParam)을 실행
  • IntersectionObserver

    • 최하단에 주시 대상 요소를 하나 생성하고 뷰포트에 들어올 경우, fetchNextPage()를 실행
profile
프론트엔드 개발자

0개의 댓글