[React Query] 무한스크롤 useInfiniteQuery

상민·2022년 8월 2일
1

React Query

목록 보기
6/6
post-thumbnail

무한스크롤은 사용자가 스크롤 할 때마다 새로운 데이터를 가져오는 것을 말한다
이는 모든 데이터를 한 번에 가져오는 것보다 훨씬 효율적이다

리액트쿼리의 useInfiniteQuery 훅을 사용해서 구현해보자


useInfiniteQuery

useInfiniteQuery는 useQuery의 무한스크롤 버전이다
이를 통해 추가로 데이터를 로딩하고 리스트로 관리할 수 있다

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

useInfiniteQuery의 data는 useQuery의 data와 다르게 두 개의 프로퍼티가 들어있다.

하나는 pages로 각 페이지의 정보가 pages 배열안에 담겨 있다
또 하나는 pageParams로 각 페이지의 매개변수가 기록되어 있다

useQuery와 다른 반환 객체의 몇 가지 프로퍼티를 살펴보자

fetchNextPage는 사용자가 더 많은 데이터를 요청할 때 호출하는 함수이다
스크린에서 데이터가 소진되는 지점을 누르는 경우이다

hasNextPagegetNextPageParam 함수의 반환값을 기반으로 하는데 이 프로퍼티를 useInfiniteQuery에 전달해서 마지막 쿼리의 데이터를 어떻게 사용할지 지시한다
undefined일 경우 더 이상 데이터가 없다는 것이다

isFetchingNextPage는 다음 페이지를 가져오는지 아니면 일반적인 fetching인지 구별할 수 있다

useInfiniteQuery의 첫 번째 인자는 useQuery훅과 마찬가지로 QueryKey가 들어간다

두 번째 인자로는 QueryFn이 들어가는데 pageParam은 QueryFn에 전달되는 매개변수이다
pageParam은 구조분해 할당 형식으로 선언해줘야 한다
기본값은 undefined이므로 초기값을 1로 설정해줬다

옵션에서 getNextPageParam은 다음 API 호출에서 쿼리 함수에 넘어갈 페이지 파라미터를 리턴해주는 역할이다
lastPage, allPages는 현재 받아온 데이터와 현재 쌓여있는 전체 데이터를 의미한다고 보면 될 것 같다

만약 response에서 페이지 정보를 받아올 수 있다면 해당 값을 사용하고, 아니라면 로직상 보관하고 있는 page param의 + 1 하면 된다


useInfiniteQuery를 SWAPI를 사용해서 구현해보자

  • src/people/InfinitePeople.tsx

import InfiniteScroll from "react-infinite-scroller";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Person } from "./Person";

export interface SWData {
  count: number;
  next: string;
  previous: string;
  results: Result[];
}

export interface Result {
  name: string;
  height: string;
  mass: string;
  hair_color: string;
  skin_color: string;
  eye_color: string;
  birth_year: string;
  gender: Gender;
  homeworld: string;
  films: string[];
  species: string[];
  vehicles: string[];
  starships: string[];
  created: Date;
  edited: Date;
  url: string;
}

export enum Gender {
  Hermaphrodite = "hermaphrodite",
  Male = "male",
}

const initialUrl = "https://swapi.dev/api/people/";
const fetchUrl = async (url: string) => {
  const response = await fetch(url);
  const json = await response.json();
  return json as SWData;
};

export function InfinitePeople() {
  // TODO: get data for InfiniteScroll via React Query
  // fetchNextPage: 더 많은 데이터가 필요할 때 어느 함수를 실행할지를 InfiniteScroll에 지시
  // hasNextPage: 수집할 데이터가 더 있는지 결정하는 boolean
  const { data, fetchNextPage, hasNextPage } = useInfiniteQuery<SWData, Error>(
    ["sw-people"], // 쿼리 키
    // pageParam은 fetchNextPage가 어떻게 보일지 결정하고 다음페이지가 있는지 결정
    ({ pageParam = initialUrl }) => fetchUrl(pageParam), // url인 pageParam을 가져와서 json을 반환
    {
      getNextPageParam: (lastPage) => lastPage.next || undefined,
      // lastPage: 쿼리 함수를 마지막으로 실행한 시점의 데이터
      // pageParam을 lastPage.next로 작성
      // fetchNextPage를 실행하면 next 프로퍼티가 무엇인지에 따라 마지막 페이지에 도착한 다음 pageParam을 사용
      // lastPage가 거짓이면 undefined를 반환
    }
  );
  return <InfiniteScroll loadMore={() => {}} />;
}

SWAPI를 요청하면 다음과 같은 데이터를 반환한다

데이터에는 nextprevious가 있는데 다음 또는 이전 페이지에 대한 페이지 URL이 담겨져 있다.
다음 또는 이전 페이지가 없으면 null을 반환한다.

useInfiniteQuery 훅의 옵션 중 getNextPageParam에서 다음페이지의 pageParam을 현재 페이지의 next로 지정하고 null이면 undefined를 반환하도록 한다


react-infinite-scroller

무한 스크롤 컴포넌트를 실행하기 위해 react-infinite-scroller 라이브러리의 InfiniteScroll 컴포넌트를 사용한다
InfiniteScroll 컴포넌트에는 두 개의 프로퍼티가 있다

첫 번째로 loadMore로 이는 데이터가 더 필요할 때 불러와 useInfiniteQuery에서 나온 fetchNextPage 함숫값을 사용한다

두 번째로는 hasMore로 이는 hasNextPage 값을 사용한다

  • loadMore={fetchNextPage}
  • hasMore={hasNextPage}

InfiniteScroll 컴포넌트는 스스로 페이지의 끝에 도달했음을 인식하고 fetchNextPage를 불러온다


무한 스크롤 구현

이제 react-query와 react-infinite-scroller를 사용해서 무한스크롤을 구현해보자

  • src/people/InfinitePeople.tsx

import InfiniteScroll from "react-infinite-scroller";
import { useInfiniteQuery } from "@tanstack/react-query";
import { Person } from "./Person";

export interface SWData {
  count: number;
  next: string;
  previous: string;
  results: Result[];
}

export interface Result {
  name: string;
  height: string;
  mass: string;
  hair_color: string;
  skin_color: string;
  eye_color: string;
  birth_year: string;
  gender: Gender;
  homeworld: string;
  films: string[];
  species: string[];
  vehicles: string[];
  starships: string[];
  created: Date;
  edited: Date;
  url: string;
}

export enum Gender {
  Hermaphrodite = "hermaphrodite",
  Male = "male",
}

const initialUrl = "https://swapi.dev/api/people/";
const fetchUrl = async (url: string) => {
  const response = await fetch(url);
  const json = await response.json();
  return json as SWData;
};

export function InfinitePeople() {
  // TODO: get data for InfiniteScroll via React Query
  // fetchNextPage: 더 많은 데이터가 필요할 때 어느 함수를 실행할지를 InfiniteScroll에 지시
  // hasNextPage: 수집할 데이터가 더 있는지 결정하는 boolean
  const {
    data,
    fetchNextPage,
    hasNextPage,
    isLoading,
    isFetching,
    isError,
    error,
  } = useInfiniteQuery<SWData, Error>(
    ["sw-people"], // 쿼리 키
    // pageParam은 fetchNextPage가 어떻게 보일지 결정하고 다음페이지가 있는지 결정
    ({ pageParam = initialUrl }) => fetchUrl(pageParam), // url인 pageParam을 가져와서 json을 반환
    {
      getNextPageParam: (lastPage) => lastPage.next || undefined,
      // lastPage: 쿼리 함수를 마지막으로 실행한 시점의 데이터
      // pageParam을 lastPage.next로 작성
      // fetchNextPage를 실행하면 next 프로퍼티가 무엇인지에 따라 마지막 페이지에 도착한 다음 pageParam을 사용
      // lastPage가 거짓이면 undefined를 반환
    }
  );
  if (isLoading) return <div className="loading">Loading..</div>; // 로딩중
  if (isError) return <div>Error! {error.toString()}</div>; // 에러
  return (
    <>
      {isFetching && <div className="loading">Loading..</div>}{" "}
      {/** 데이터를 fetching하는 동안의 로딩컴포넌트 */}
      <InfiniteScroll loadMore={() => fetchNextPage()} hasMore={hasNextPage}>
        {data?.pages.map((pageData) => {
          return pageData.results.map((person) => {
            return (
              <Person
                key={person.name}
                name={person.name}
                hairColor={person.hair_color}
                eyeColor={person.eye_color}
              />
            );
          });
        })}
      </InfiniteScroll>
    </>
  );
}

InfiniteScroll 컴포넌트에서 loadMore에 fetchNextPage 함숫값을 사용한다
강의에서는 loadMore={fetchNextPage} 이렇게 바로 사용했지만
타입스크립트에서 그대로 사용하니 타입에러가 났다
그래서 익명함수로 감싸서 fetchNextPage를 실행한 값을 리턴하도록 하니 잘 실행되었다

profile
FE Developer

0개의 댓글