react-query에서 useInfiniteQuery
훅도, useQuery
훅과 마찬가지로 다음의 옵션들이 필요하다.
다음의 옵션들은 필수로 지정하지 않으면 오류가 발생한다.
queryKey
: useQuery에서와 마찬가지로, 캐시공간에 저장하기 위한 고유한 키를 의미하며, 역시 매개변수로 쿼리파라미터를 지정할 수 있다.queryFn
: api요청 함수가 들어간다. 기본 인자로 객체가 전달되는데, 객체의 프로퍼티로 direction
, meta
, pageParam
, queryKey
, signal
등을 가진다.direction : "forward", // 쿼리의 방향. 사용자가 더 많은 데이터 요청시 forward
meta : undefined, // 쿼리의 메타데이터 포함 객체
pageParam : 0, // getNextPageParam 의 반환값, 다음페이지를 가져오기 위한 매개변수로 사용(= offset)
queryKey : ['myQueryKey', myQueryParams],
signal :, // 사용자가 쿼리를 중단하고싶을때 사용하는 신호
여기서 가장 중요하고, 가장 많이 다루는 것이 pageParam
이므로 pageParam이 offset, 페이지네이션 번호를 의미한다는 것을 꼭 기억하자.
pageParam
사용예시 )
const fetchPages = async ({ pageParam = 1 }) => {
const response = await fetch(`/api/data?page=${pageParam}`);
return response.json();
};
const fetchPages = async ({ pageParam = 0 }) => {
const response = await fetch(`/api/data?offset=${pageParam}`);
return response.json();
};
initialPageParam
: 맨 첫번째의 PageParam의 값을 지정하는 데에 사용한다.
getNextPageParam
: fetchNextPage가 호출되면 동작하는 함수로, 다음 페이지의 매개변수를 결정한다.
page단위의 페이지네이션을 사용한다면, +1
씩 증가하도록 설정할수도 있고, offset단위의 페이지네이션을 사용한다면, + limit
씩 증가하도록 설정할수도 있다.
getNextPageParam은 2개의 인자를 가진다. (v5에서는 4개의 인자를 가진다.)
lastPage = [ 데이터1, 데이터2, 데이터3 ...]
pages = [
[데이터1,데이터2,데이터3, ...], // 첫번째 요청
[데이터11, 데이터12, 데이터13, ...] // 두번째 요청
[데이터21, 데이터22, 데이터23, ...] // 세번째 요청
]
따라서 다음 pageParam를 pages 배열의 length로 결정할 수 있다.
(v5)
lastPageParam : 가장 마지막 queryFn이 실행된 후의 pageParam을 가진다. (e.g lastPageParam : 6)
allPageParams : 가장 마지막 queryFn 실행까지의 현재까지 사용된 pageParam array를 가진다. (e.g [0,6,12])
getNextPageParam
사용예시 )
const {
data,
isLoading,
fetchNextPage,
error,
} = useInfiniteQuery<Room[], Error>({
queryKey: ["rooms", params],
queryFn: ({ pageParam }) => fetchRooms(params, pageParam as number), // #3
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => { // #4
return pages.length * 9; // 9는 limit 값.
},
});
const handleButton = async () => { // #2
fetchNextPage();
};
...
return <button onClick={handleButton}>fetch버튼</button> // #1
동작 단계:
fetch버튼
을 클릭한다.
handleButton
에서 fetchNextPage()
함수가 실행된다.
queryFn
이 실행되고, API를 요청한다.
data에 값이 전달된다.
data = {
pageParams : [1번째pageParam, 2번째pageParam, 3번째pageParam ...]
pages : [ [데이터1~데이터9] , [데이터10~데이터18], [데이터19~데이터27] ... ]
}
getNextPageParam
함수가 실행되고, 다음 차례의 pageParam
값이 세팅된다,(data는 pageParams, pages를 프로퍼티로 가진다. )
fetch버튼
을 클릭한다 ... (반복)queryKey
, queryFn
, getNextPageParam
이 필수요소였다면,
다음의 요소는 선택적으로 설정할 수 있는 옵션들이다.
enabled
: useQuery에서와 마찬가지로, 쿼리의 활성화 조건을 설정한다. false이면 쿼리가 실행되지 않는다.
staleTime
: useQuery에서와 마찬가지로, staleTime이 설정된 동안은 데이터가 재요청되지 않는다.
retry
: 쿼리 실패시 재시도 횟수를 지정한다. ( default = 3, false일땐 재시도X )
getPreviousPageParam
: 이전 페이지의 매개변수를 결정
select
: 쿼리 결과를 변형
사용 예시 )
const { data, isLoading, fetchNextPage, hasNextPage, error } = useInfiniteQuery<
Room,
Error
>({
queryKey: ["rooms", params],
queryFn: ({ pageParam }) => fetchRooms(params, pageParam as number),
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => {
return pages.length * 9;
},
select: (data) => ({
...data,
pages: data.pages.flatMap((page) => page),
}),
});
원래 data는
data = {
pageParams : [1번째param, 2번째param, 3번째param],
pages : [ [데이터1~데이터9] , [데이터10~데이터18], [데이터19~데이터27] ... ]
}
형태를 가지지만, 구현한 select의 처리 후에는
data = {
pageParams : [1번째param, 2번째param, 3번째param],
pages : [ 데이터1 ~ 데이터27]
}
형태를 가지게 된다.
즉 select는 data의 최종 결과값을 가공하는 역할이다.
useInfiniteQuery를 사용하면서, fetchNextPage
함수가 호출되었을때 getNextPageParam
이 호출되기 때문에 getNextPageParam의 내용이 1번만 실행되야 한다고 생각했다.
무슨말이냐면, 아래의 작성한 코드에서
const { data, isLoading, fetchNextPage, hasNextPage, error } = useInfiniteQuery<
Room[],
Error
>({
queryKey: ["rooms", params],
queryFn: ({ pageParam }) =>
fetchRooms({ ...params, limit: limitByWidth }, pageParam as number),
initialPageParam: 0,
getNextPageParam: (lastPage, pages, lastPageParam, allPageParams) => {
console.log("getNextPageParam 호출됨");
if (lastPage.length === 0) {
return undefined;
}
return pages.flatMap((page) => page).length;
},
enabled: !!gridContainerWidthRef.current,
});
특정 버튼을 눌렀을때의 이벤트로 fetchNextPage
를 한번만 호출했는데도, 'getNextPageParam 호출됨' 이라는 텍스트가 6~7번씩 출력되었다.
pages의 데이터가 많아지면 pages.flatMap((page) => page).length;
에서 pages를 순회하면서 성능의 손실이 클 것이기 때문에 무시할 수 없는 문제라고 생각했다.
여러 state값이 변경되면서(특히 limitByWidth, gridContainerWidthRef.current), 사용된 컴포넌트가 리렌더링되는 과정에서 getNextPageParam이 호출되는건가 싶어서, JSX 코드를 대부분 지우고 state를 사용하는 값들을 제거하거나 고정시킨뒤, useEffect를 이용해서 출력해봤다.
export default function Test() {
const params: Omit<FetchRoomsParams, 'limit'> = {
search: '',
isPublic: undefined,
isPossible: undefined,
};
const { data, isLoading, fetchNextPage, hasNextPage, error } =
useInfiniteQuery<Room[], Error>({
initialPageParam: 0,
queryKey: ['rooms', params],
queryFn: ({ pageParam }) => {
return fetchRooms({ ...params, limit: 9 }, pageParam as number);
},
getNextPageParam: (lastPage, pages) => {
console.log('getNextPageParam 호출됨');
if (lastPage.length === 0) {
return undefined;
}
return pages.length * 9;
},
});
const handleButtonClick = () => {
fetchNextPage();
};
return (
<div>
<button onClick={handleButtonClick}>fetch 버튼</button>
</div>
);
위 코드처럼 완전히 기본적인 useInfiniteQuery 코드를 작성하였으나,
여전히 console.log('getNextPageParam 호출됨');
부분이 7번정도 실행됐다.
내부적으로 fetchNextPage
가 getNextPageParam
을 여러번 호출하는걸로 생각된다.
결국 중복호출에 대해서는 어쩔 수 없는 것으로 생각하고,
기존의
getNextPageParam: (lastPage, pages, lastPageParam, allPageParams) => {
console.log("getNextPageParam 호출됨");
if (lastPage.length === 0) {
return undefined;
}
return pages.flatMap((page) => page).length;
},
코드에서, getNextPageParam 내부에서 많은 연산을 수행하는 대신,
다음처럼
getNextPageParam: (lastPage) => {
if (!lastPage.hasMore) {
return undefined;
}
return lastPage.offset;
},
response된 lastPage값에 hasMore이라는 flag를 추가하여 더이상 조회할 데이터가 없을때 pageParam을 undefined로 설정하고, offset이라는 미리 계산된 다음 pageParam 값을 추가하여 최대한 간단한 연산을 수행하도록 하는 것이 최선이라고 생각된다.
근본적으로 getNextPageParam의 중복호출을 막는 방법은 좀 더 찾아봐야할 듯 하다.