useInfinityQuery로 무한스크롤 만들기

useQuery와 useInfinityQuery의 차이점

react-query를 사용해서 서버로부터 가져온 데이터를 관리할 때 가장 흔하게 useQuery가 사용된다.
그런데 useQuery 같은 경우에는 고유한 query-key에 해당하는 데이터가 만료되면 새로운 데이터를 패칭해서 기존의 데이터를 덮어버리는 방식이기에 무한 스크롤 기능처럼 기존의 데이터는 그대로 두고 새로운 데이터를 가져와야 할 때 사용하기에는 어려움이 있는데 이런 경우에 useInfinityQuery를 사용하면 좋다.
useInfinityQuery의 결과에 담긴 data는 useQuery와 다르게 data.pagesdata.pageParams가 존재하는데, 다음 페이지나 이전 페이지의 데이터를 가져오면 기존의 data.pages에 새로운 데이터가 맨 앞이나 맨 뒤에 덧붙혀지는 방식으로 작동한다.

기본 형태

const {
  fetchNextPage,
  fetchPreviousPage,
  hasNextPage,
  hasPreviousPage,
  isFetchingNextPage,
  isFetchingPreviousPage,
  ...result
} = useInfiniteQuery(queryKey, ({ pageParam = 1 }) => fetchPage(pageParam), {
  ...options,
  getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
  getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
})

옵션

queryKey: unknown[]

  • 가져온 데이터를 인식할 고유한 키이다.

queryFn: (context: QueryFunctionContext) => Promise

  • 데이터를 가져오는 Promise 함수로, useInfinityQuery의 두 번째 인자로 부여한다.
  • 매개변수 context 객체가 갖고 있는 pageParam은 서버에 요청할 때 어떤 페이지의 데이터를 가져올 지 표현할 수 있으며 시작 페이지를 줄 수 있다.

getNextPageParam: (lastPage, allPages) => unknown | undefined

  • pageParam을 다음 페이지의 값으로 변경하는 함수로, useInfinityQuery의 세 번째 인자로 부여한다.
  • 현재 페이지를 기준으로 다음 페이지의 pageParam 값을 반환하면 된다.
  • 다음 페이지가 없다면 undefined 를 반환하면 된다.

getPreviousPageParam: (firstPage, allPages) => unknown | undefined

  • pageParam을 이전 페이지의 값으로 변경하는 함수로, useInfinityQuery의 세 번째 인자로 부여한다.
  • 현재 페이지를 기준으로 이전 페이지의 pageParam 값을 반환하면 된다.
  • 이전 페이지가 없다면 undefined 를 반환하면 된다.

반환되는 값

data.pages: TData[]

  • 각 페이지 별 데이터 내용이 들어있다.

data.pageParams: unknown[]

  • 각 페이지 별 pageParam 값이 들어있다

fetchNextPage: (options?: FetchNextPageOptions) => Promise

  • 다음 페이지 내용을 가져오는 함수이다.

fetchPreviousPage: (options?: FetchPreviousPageOptions) => Promise

  • 이전 페이지 내용을 가져오는 함수이다.

isFetchingNextPage: boolean

  • 다음 페이지 내용을 가져오는 중인지의 여부이다.

isFetchingPreviousPage: boolean

  • 이전 페이지 내용을 가져오는 중인지의 여부이다.

hasNextPage: boolean

  • 다음 페이지가 있는지의 여부를 나타낸다.

hasPreviousPage: boolean

  • 이전 페이지가 있는지의 여부를 나타낸다.

소스코드 예시

import React, { Fragment, useEffect } from 'react';
import { useInfiniteQuery } from 'react-query';
import { useInView } from 'react-intersection-observer';
import { AxiosError } from 'axios';
import PhotoCard from './PhotoCard';
import SkeletonPhotoCard from './SkeletonPhotoCard';

interface PhotoListProps {
  children?: React.ReactNode;
}
const PhotoListDefaultProps = {};

const fetchData = (limit: number, pageParam: number) => {
  const url = `/api/photo`;
  const params = { limit, pageParam }
  const res = await client.get(url, { params });
  return res.data;
}

function PhotoList({ children }: PhotoListProps & typeof PhotoListDefaultProps) {
  const limit = 20; // 한 페이지에 보여줄 아이템 갯수
  const [viewRef, inView] = useInView();

  // 데이터 가져오기
  const { data: photos, error, isFetching, fetchNextPage, hasNextPage } = 
  useInfiniteQuery(['photos'], ({ pageParam = 0 }) => fetchData(limit, pageParam),
  {
    getNextPageParam: (lastPage, pages) => {
      return lastPage?.photos.length === limit && lastPage.pageParam + limit;
    }
  });

  // 다음 페이지 가져오기
  useEffect(() => {
    if (!inView) return;
    if (!photos) return;
    if (!hasNextPage) return;

    fetchNextPage();
    console.log(inView);
  }, [inView]);

  return (
    <>
      <section className="photo-section">
        {photos?.pages.map((page, pageIdx) => 
          <Fragment key={pageIdx}>
            {page?.photos.map((item) => (
              <PhotoCard key={item.photocard_id} photo={item} />
            ))}
          </Fragment>
        )}

        {isFetching && Array.from({length: limit}).map((_, idx) => (
          <SkeletonPhotoCard key={idx} />
        ))}
      </section>
      
      <div ref={viewRef} />
    </>
  );
}

PhotoList.defaultProps = PhotoListDefaultProps;
export default PhotoList;

Written by@bbearcookie
Frontend 개발자가 되려고 하는 컴퓨터공학과 학생입니다. React 와 Express 같은 JS, TS 기반의 기술에 관심이 있습니다.