
기존 데이터 집합에 추가로 “더 많은 데이터를 로드” 하거나 “무한 스크롤” 할 수 있는 렌더링 목록도 매우 일반적인 UI 패턴이다. TanStack Query는 이러한 유형의 목록을 쿼리하기 위해 useInfiniteQuery 라는 유용한 useQuery 버전을 지원한다.
useInfiniteQuery 를 사용하면 몇 가지 다른 점을 알 수 있다.
data 는 무한 쿼리 데이터를 포함하는 객체다.data.pages 배열data.pageParams 배열fetchNextPage 와 fetchPreviousPage 기능을 사용할 수 있다.( fetchNextPage 는 필수 )initialPageParam 옵션을 사용할 수 있으며 필수다.getNextPageParam 와 getPreviousPageParam 옵션을 사용할 수 있다. 이 정보는 쿼리 함수의 추가 매개변수로 제공된다.hasNextPage (boolean)을 사용할 수 있으며 getNextPageParam 이 null 이나 undefined 이 외의 값을 반환하는 경우 true 다.hasPreviousPage (boolean)을 사용할 수 있으며 getPreviousPageParam 이 null 이나 undefined 이 외의 값을 반환하는 경우 true 다.isFetchingNextPage 와 isFetchingPreviousPage (boolean)을 사용하여 백그라운드 새로고침 상태와 추가 로드 상태를 구분할 수 있다.참고: initialData 또는 placeholderData 옵션은 data.pages 및 data.pageParams 속성이 있는 객체의 동일한 구조를 따라야한다.
다음 프로젝트 그룹을 가져오는 데 사용할 수 있는 cursor 인덱스를 기반으로 한 번에 3개의 projects 페이지를 반환하는 API가 있다고 가정해 보겠다.
fetch('/api/projects?cursor=0')
// { data: [...], nextCursor: 3}
fetch('/api/projects?cursor=3')
// { data: [...], nextCursor: 6}
fetch('/api/projects?cursor=6')
// { data: [...], nextCursor: 9}
fetch('/api/projects?cursor=9')
// { data: [...] }
이 정보를 사용하여 다음과 같은 방법으로 “추가로드” UI를 만들 수 있다.
useInfiniteQuery 가 첫번째 데이터 그룹을 요청하기를 기다리는 중getNextPageParam 에서 다음 쿼리에 대한 정보 반환fetchNextPage 함수 호출import { useInfiniteQuery } from '@tanstack/react-query'
function Projects() {
const fetchProjects = async ({ pageParam }) => {
const res = await fetch('/api/projects?cursor=' + pageParam)
return res.json()
}
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
return status === 'pending' ? (
<p>Loading...</p>
) : status === 'error' ? (
<p>Error: {error.message}</p>
) : (
<>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.data.map((project) => (
<p key={project.id}>{project.name}</p>
))}
</React.Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
</div>
<div>{isFetching && !isFetchingNextPage ? 'Fetching...' : null}</div>
</>
)
}
진행 중인 fetch가 진행되는 동안 fetchNextPage 를 호출하면 백그라운드에서 발생하는 데이터 새로고침을 덮어쓸 위험이 있다는 점을 이해하는 것이 중요하다. 이러한 상황은 목록을 렌더링하고 fetchNextPage 를 동시에 트리거할 때 특히 중요하다.
InfiniteQuery에 대해 진행 중인 fetch는 단 한 번만 가능하다는 점을 기억해라. 단일 캐시 항목이 모든 페이지에 공유되므로 동시에 두 번 fetch하려고 하면 데이터 덮어쓰기가 발생할 수 있다.
동시 가져오기를 활성화 하려는 경우 fetchNextPage 내에서 { cancelRefetch: false } 옵션(기본값: true)를 활용할 수 있다.
충돌 없이 원활한 쿼리 프로세스를 보장하려면 특히 사용자가 해당 호출을 직접 제어하지 않는 경우 쿼리가 isFetching 상태가 아닌지 확인하는 것이 좋다.
<List
onEndReached={() => !isFetching && fetchNextPage()}
/>
무한 쿼리가 stale되어 다시 가져와야 하는 경우 각 그룹은 첫 번째 쿼리부터 sequentially 으로 가져온다. 이렇게 하면 기본 데이터가 변경되더라도 오래된 커서를 사용하지 않고 잠재적으로 중복되거나 레코드를 건너뛰는 일이 발생하지 않는다. 무한 쿼리의 결과가 queryCache에서 제거되면 초기 그룹만 요청된 상태로 초기 상태에서 페이지 매김이 다시 시작된다.
모든 페이지의 하위 집합만 다시 가져오려면 refetchPage 함수를 전달하여 useInfiniteQuery 에서 반환된 refetch 하면 된다.
const { refetch } = useInfiniteQuery('projects', fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
// only refetch the first page
refetch({ refetchPage: (page, index) => index === 0 })
또한 이 함수에 두 번째 인수(queryFilters)의 일부로 queryClient.refetchQueries, queryClient.invalidateQueries 나 queryClient.resetQueries에 전달할 수도 있다.
Signature
refetchPage: (page: TData, index: number, allPages: TData[]) => boolean이 함수는 각 페이지에 대해 실행되며 이 함수가 true 를 반환하는 페이지만 다시 가져온다.
기본적으로 getNextPageParam 에서 반환된 변수는 쿼리 함수에 제공되지만 경우에 따라 이를 재정의할 수도 있다. 다음과 같이 기본 변수를 재정의 하는 fetchNextPage 함수에 사용자 정의 변수를 전달할 수 있다.
function Projects() {
const fetchProjects = ({ pageParam = 0 }) =>
fetch('/api/projects?cursor=' + pageParam)
const {
status,
data,
isFetching,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
} = useInfiniteQuery('projects', fetchProjects, {
getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
})
// Pass your own page param
const skipToCursor50 = () => fetchNextPage({ pageParam: 50 })
}
때로는 페이지를 역순으로 표시하고 싶을 수도 있다. 이 경우 select 옵션을 사용할 수 있다.
useInfiniteQuery('projects', fetchProjects, {
select: data => ({
pages: [...data.pages].reverse(),
pageParams: [...data.pageParams].reverse(),
}),
})
첫 번째 페이지를 수동으로 제거:
queryClient.setQueryData('projects', data => ({
pages: data.pages.slice(1),
pageParams: data.pageParams.slice(1),
}))
개별 페이지에서 단일 값을 수동으로 제거하려면:
const newPagesArray = oldPagesArray?.pages.map((page) =>
page.filter((val) => val.id !== updatedId)
) ?? []
queryClient.setQueryData('projects', data => ({
pages: newPagesArray,
pageParams: data.pageParams,
}))
페이지와 pageParams의 데이터 구조를 동일하게 유지해라.