Prefetching은 웹 애플리케이션이나 웹 페이지의 사용자 경험을 향상시키기 위해 데이터를 미리 불러오는 기술을 말한다. 주로 사용자가 페이지를 요청하기 전에 필요한 데이터를 미리 가져와서 페이지 로딩 속도를 향상시키고 사용자 경험을 부드럽게 만드는 데 사용된다.
TanStack Query에서는 prefetchQuery()
함수를 사용하여 데이터를 미리 가져오는 것이 가능하다. 이 함수는 미리 데이터를 가져올 때 사용자가 페이지로 이동하지 않았더라도 데이터를 캐시하고 유지한다. 이렇게 하면 사용자가 해당 데이터를 요청할 때 실제 요청보다 빠르게 데이터를 제공할 수 있다.
const BlogPostList = () => {
const queryClient = useQueryClient();
// 컴포넌트가 마운트되면 미리 블로그 포스트 목록 데이터를 가져온다.
useEffect(() => {
// prefetchQuery를 사용하여 'posts' 쿼리를 미리 불러온다.
queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
});
}, []);
return (
<div>
{/* 블로그 포스트 목록을 렌더링하는 코드 ... */}
</div>
);
};
페이지네이션을 구현하는 것은 데이터를 여러 페이지로 나누어 가져와 효율적으로 표시하는 방법이다. 페이지네이션은 대량의 데이터를 한 번에 로드하지 않고 필요한 만큼 조금씩 로드하여 사용자 경험을 개선하는 데 사용된다.
하지만 Pagenation 기능이 적용된 쿼리는 한가지 오류가 발생할 수 있다. 바로 UI 점프이다. 각각의 새 페이지는 완전히 새로운 쿼리처럼 취급되기 때문에 UI는 success
와 loading
상태를 왔다갔다 한다. 이러한 현상은 최적화와 거리가 멀며, TanStack Query는 이를 해결하기 위해 keepPreviousData
옵션을 지원한다. 이 옵션은 새 데이터가 올 때까지 이전 데이터의 형태를 유지하는 옵션이며 페이지네이션에 최적화된 기능이다. 페이지네이션을 사용하는 커스텀 훅을 다음과 같이 예로 들어보겠다.
export interface AxiosInterface {
projects: Project[]; // 페이지네이션을 통해 불러올 데이터의 타입
hasMore: boolean; // 더 불러올 데이터가 있는지
nextCursor: number;
}
// 인자로 받아오는 page는 컴포넌트에서 사용하는 페이지 번호이다.
export const useProjectList = (page: number) => {
const { data, isError, error, isFetching, isLoading, isPreviousData } = useQuery<AxiosInterface>({
queryKey: ['pages', page],
queryFn: () => getProjects(page), // 받아온 페이지를 바탕으로 데이터를 가져옴
keepPreviousData: true
})
return {
data, isError, error, isFetching, isLoading, isPreviousData
};
};
true
가 된다.이후 컴포넌트에선 다음과 같이 작성한다.
const ReactQueryExample3: FC = () => {
const [page, setPage] = useState<number>(0);
const { data , error, isFetching, isPreviousData, isLoading } = useProjectList(page);
return (
<div>
{isLoading ? (
<div>Loading...</div>
) : error instanceof Error ? (
<div>Error: {error.message}</div>
) : (
<div>
{data?.projects.map(project => (
<p key={project.id}>{project.name}</p>
))}
</div>
)}
<span>Current Page: {page + 1}</span>
<button
onClick={() => setPage(prev => Math.max(prev - 1, 0))}
disabled={page === 0}
>
Previous Page
</button>{' '}
<button
onClick={() => {
if (!isPreviousData && data?.hasMore) {
setPage(prev => prev + 1)
}
}}
disabled={isPreviousData || ! data?.hasMore}
>
Next Page
</button>
{isFetching ? <span> Loading...</span> : null}{' '}
</div>
);
};
해당 결과처럼 나온다.
무한 스크롤 형태의 페이지네이션을 구현하는데 사용된다. 페이지네이션을 사용하는데 있어서 데이터를 일정량씩 가져와서 스크롤을 할 때마다 추가 데이터를 불러오는 패턴을 구현할 때 유용하다. 이를 통해 사용자가 스크롤을 내리면 새로운 데이터를 자연스럽게 불러올 수 있기에 페이지네이션보다 더 최신 트렌드의 기능이다.
인피니티 스크롤을 사용하는 커스텀훅을 먼저 만든다.
export const useInfiniteExample = () => {
const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status }
= useInfiniteQuery<AxiosInterface>({
queryKey: ['projects'],
queryFn: fetchProjects,
// 다음 페이지에 필요한 파라미터를 추출하는 역할을 한다.
// 이를 통해 다음 페이지를 가져오는 데 필요한 정보를 지정할 수 있다.
getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
});
return {
data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status
};
};
커스텀 훅을 사용하는 컴포넌트를 만든다.
const ReactQueryExample4: FC = () => {
const { data, error, fetchNextPage, hasNextPage, isFetching, isFetchingNextPage, status } = useInfiniteExample();
const customError = error as CustomError;
return status === 'loading' ? (
<p>Loading...</p>
) : status === 'error' ? (
<p>Error: {customError?.message}</p>
) : (<>
{data?.pages.map((group, i) => (
<Fragment key={i}>
{group.projects.map((project) => (
<p key={project.id}>{project.name}</p>
))}
</Fragment>
))}
<div>
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ?
'Loading more...' :
hasNextPage ? 'Try Load more!' :
'Nothing to Load'}
</button>
</div>
</>)
};
결과는 아래와 같다.