무한스크롤은 사용자가 스크롤 할 때마다 새로운 데이터를 가져오는 것을 말한다
이는 모든 데이터를 한 번에 가져오는 것보다 훨씬 효율적이다
리액트쿼리의 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
는 사용자가 더 많은 데이터를 요청할 때 호출하는 함수이다
스크린에서 데이터가 소진되는 지점을 누르는 경우이다
hasNextPage
는 getNextPageParam
함수의 반환값을 기반으로 하는데 이 프로퍼티를 useInfiniteQuery에 전달해서 마지막 쿼리의 데이터를 어떻게 사용할지 지시한다
undefined
일 경우 더 이상 데이터가 없다는 것이다
isFetchingNextPage
는 다음 페이지를 가져오는지 아니면 일반적인 fetching인지 구별할 수 있다
useInfiniteQuery의 첫 번째 인자는 useQuery훅과 마찬가지로 QueryKey
가 들어간다
두 번째 인자로는 QueryFn
이 들어가는데 pageParam
은 QueryFn에 전달되는 매개변수이다
pageParam은 구조분해 할당 형식으로 선언해줘야 한다
기본값은 undefined
이므로 초기값을 1로 설정해줬다
옵션에서 getNextPageParam
은 다음 API 호출에서 쿼리 함수에 넘어갈 페이지 파라미터를 리턴해주는 역할이다
lastPage, allPages
는 현재 받아온 데이터와 현재 쌓여있는 전체 데이터를 의미한다고 보면 될 것 같다
만약 response에서 페이지 정보를 받아올 수 있다면 해당 값을 사용하고, 아니라면 로직상 보관하고 있는 page param의 + 1 하면 된다
useInfiniteQuery를 SWAPI를 사용해서 구현해보자
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를 요청하면 다음과 같은 데이터를 반환한다
데이터에는 next
와 previous
가 있는데 다음 또는 이전 페이지에 대한 페이지 URL이 담겨져 있다.
다음 또는 이전 페이지가 없으면 null을 반환한다.
useInfiniteQuery 훅의 옵션 중 getNextPageParam
에서 다음페이지의 pageParam을 현재 페이지의 next로 지정하고 null이면 undefined를 반환하도록 한다
무한 스크롤 컴포넌트를 실행하기 위해 react-infinite-scroller
라이브러리의 InfiniteScroll
컴포넌트를 사용한다
InfiniteScroll
컴포넌트에는 두 개의 프로퍼티가 있다
첫 번째로 loadMore
로 이는 데이터가 더 필요할 때 불러와 useInfiniteQuery
에서 나온 fetchNextPage
함숫값을 사용한다
두 번째로는 hasMore
로 이는 hasNextPage 값을 사용한다
InfiniteScroll
컴포넌트는 스스로 페이지의 끝에 도달했음을 인식하고 fetchNextPage를 불러온다
이제 react-query와 react-infinite-scroller를 사용해서 무한스크롤을 구현해보자
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
를 실행한 값을 리턴하도록 하니 잘 실행되었다