
이번 포스팅에서는 pagination을 구현해보도록 하자.
다음 API 함수는 쿼리 파라미터로 page와 limit을 넘겨주면 해당하는 page의 데이터를 limit 개수만큼 보내주도록 설계되어 있다.
export async function getPosts(page = 0, limit = 10) {
const response = await fetch(`${BASE_URL}/posts?page=${page}&limit=${limit}`);
return await response.json();
}
다음으로, useState()를 사용하여 page 변수를 만들어준 후, useQuery()에서 queryKey에 page를 추가하여 page 별로 데이터를 캐싱해주었다.
그리고 쿼리 함수 호출 부분에 page를 추가해주었다.
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getPosts, uploadPost, getUserInfo } from './api';
const PAGE_LIMIT = 3;
function HomePage() {
// ...
const [page, setPage] = useState(0);
const {
data: postsData,
isPending,
isError,
} = useQuery({
queryKey: ['posts', page],
queryFn: () => getPosts(page, PAGE_LIMIT),
});
// ...
const posts = postsData?.results ?? [];
return (
<>
<div>
{currentUsername ? (
loginMessage
) : (
<button onClick={handleLoginButtonClick}>codeit으로 로그인</button>
)}
<form onSubmit={handleSubmit}>
<textarea
name="content"
value={content}
onChange={handleInputChange}
/>
<button disabled={!content} type="submit">
업로드
</button>
</form>
</div>
<div>
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.user.name}: {post.content}
</li>
))}
</ul>
</div>
</>
);
}
export default HomePage;
위 코드를 실행하여 결과를 확인해보면 첫 페이지(page = 0)에 해당하는 세 개의 데이터가 보여질 것이다.
이제 페이지를 변경할 수 있는 버튼을 추가해보자.
React-query 개발자 도구로 posts 데이터를 살펴보면 hasMore라는 값이 있는데, 백엔드에서 그 다음 페이지가 있을 때 hasMore 값을 true로 보내주게 된다.
즉, 이 hasMore 값으로 다음 페이지 버튼의 활성화 여부를 결정할 수 있는 것이다.
import { useState } from 'react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { getPosts, uploadPost, getUserInfo } from './api';
const PAGE_LIMIT = 3;
function HomePage() {
// ...
const [page, setPage] = useState(0);
const {
data: postsData,
isPending,
isError,
} = useQuery({
queryKey: ['posts', page],
queryFn: () => getPosts(page, PAGE_LIMIT),
});
// ...
const posts = postsData?.results ?? [];
return (
<>
<div>
{currentUsername ? (
loginMessage
) : (
<button onClick={handleLoginButtonClick}>codeit으로 로그인</button>
)}
<form onSubmit={handleSubmit}>
<textarea
name="content"
value={content}
onChange={handleInputChange}
/>
<button disabled={!content} type="submit">
업로드
</button>
</form>
</div>
<div>
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.user.name}: {post.content}
</li>
))}
</ul>
<div>
<button
disabled={page === 0}
onClick={() => setPage((old) => Math.max(old - 1, 0))}
>
<
</button>
<button
disabled={!postsData?.hasMore} // hasMore 사용 부분
onClick={() => setPage((old) => old + 1)}
>
>
</button>
</div>
</div>
</>
);
}
export default HomePage;
위와 같이 코드를 작성해주면, 마지막 페이지로 갔을 때 다음 페이지 버튼이 성공적으로 비활성화되는 것을 확인할 수 있다.
그런데, 지금 상태에서는, 다음 페이지로 넘어갈 때마다 매번 로딩 메세지가 뜨는 오류가 존재하는데,
이는 새로운 페이지에 해당하는 쿼리를 보낼 때마다 완전히 새로운 쿼리로 인식하기 때문에, 계속 pending 상태가 되기 때문이다.
이를 해결하기 위해, React-query에서는 조금 더 부드러운 UI 전환을 위한 placeholderData를 설정해줄 수 있다.
useQuery()에서 placeholderData 옵션에 keepPreviousData 혹은 (prevData) => prevData를 넣어주면, 페이지가 새로 바뀌더라도 매번 pending 상태가 되지 않고, 이전의 데이터를 유지해서 보여주다가, 새로운 데이터 fetch가 완료되면 자연스럽게 새로운 데이터로 바꿔서 보여주게 된다.
import {
// ...
keepPreviousData,
} from '@tanstack/react-query';
const {
data: postsData,
isPending,
isError,
} = useQuery({
queryKey: ['posts', page],
queryFn: () => getPosts(page, PAGE_LIMIT),
placeholderData: keepPreviousData,
});
이제 코드를 실행해보면, 다음 버튼을 눌렀을 때 로딩 메세지가 보이지 않고 이전 페이지의 내용이 그대로 보이다가, 데이터를 모두 받아오면 다음 페이지의 내용으로 바뀌는 것을 확인할 수 있다.
그런데, 여기서도 한 가지 보완할 점이 존재하는데, 다음 버튼을 누르고 새로운 데이터를 받아오는 중간 과정에서 다음 페이지 버튼이 활성화되어 있다는 것이다.
이 때, useQuery()의 리턴 값에서 isPlaceholderData 값을 활용하면, 현재 보이는 데이터가 이전 데이터(placeholderData) 일 때, 다음 페이지 버튼을 비활성화시킬 수 있다.
const {
data: postsData,
isPending,
isError,
isPlaceholderData,
} = useQuery({
queryKey: ['posts', page],
queryFn: () => getPosts(page, PAGE_LIMIT),
placeholderData: keepPreviousData,
});
...
return (
...
<div>
<ul>
{posts.map((post) => (
<li key={post.id}>{`${post.user.name}: ${post.content}`}</li>
))}
</ul>
<div>
<button
disabled={page === 0}
onClick={() => setPage((old) => Math.max(old - 1, 0))}
>
<
</button>
<button
disabled={isPlaceholderData || !postsData?.hasMore}
onClick={() => setPage((old) => old + 1)}
>
>
</button>
</div>
</div>
);
이제 pagination 구현이 성공적으로 완료되었다.
여기서 딱 한 가지만 더 보완을 해보자면, 데이터를 prefetch하여 다음 페이지로의 전환을 끊김없이 만들어보는 것이다.
이는 쿼리 클라이언트의 prefetchQuery 함수를 이용하여 구현할 수 있다.
...
useEffect(() => {
if (!isPlaceholderData && postsData?.hasMore) {
queryClient.prefetchQuery({
queryKey: ['posts', page + 1],
queryFn: () => getPosts(page + 1, PAGE_LIMIT),
});
}
}, [isPlaceholderData, postsData, queryClient, page]);
...