문제점
가게 상세 페이지에서 해당 가게에 대한 댓글을 렌더링할 때 페이지네이션을 통해 5개씩 가져오도록 구현해야 했다. 댓글 페이지네이션은 queryKey에 현재 페이지를 넣어 새로운 페이지 버튼을 클릭할 때마다 데이터를 fetching하는 방법으로 진행했다.
이 과정에서 페이지 버튼 클릭 시, 새로운 데이터를 가져올 때마다 화면이 깜빡거리는 문제가 발생하였다. 처음에는 전체 댓글 영역이 고정되어 있지 않아 댓글 데이터를 로드하는 중에 화면에 영역이 표시되지 않아서 발생하는 문제라고 생각했다. 따라서 스켈레톤 UI를 먼저 표시하고, 데이터 로드가 완료되면 댓글이 렌더링되도록 했지만 해결되지 않았다.
이후 react query 공식 문서의 Paginated Queries 관련 내용을 읽고, 다음과 같은 원인과 해결책을 찾을 수 있었다.
해결
결론은 useQuery의 옵션인 keepPreviousData를 true로 주는 것이다.
페이지 버튼을 클릭했을 시 새 페이지가 새로운 쿼리로 인식되기 때문에 UI의 상태가 성공↔로딩으로 반복적으로 변경된다. 이 때문에 UI가 깜빡거리는 현상이 발생했던 것이다. 이를 해결하기 위해 react query는 다음 데이터가 요청되어서 전달되는 동안 이전 데이터를 유지시켜주는 옵션인 keepPreviousData 옵션을 가지고 있다. 이를 true로 설정하여 쿼리 키가 변경되더라도 새로r운 데이터를 가져오는 동안 가장 최근에 성공적으로 fetching했던 데이터를 사용하여 UI 변경 시 끊김 없이 동작하도록 했다.
적용
// comments.ts
const url = `/api/comments`;
const COMMENTS_FETCH_SIZE = 5;
const fetchComments =
(storeId: string, pageParam = 1) =>
async () => {
const url = `/api/comments/${storeId}?page=${pageParam}&page_size=${COMMENTS_FETCH_SIZE}`;
const { data } = await axios.get(url);
return data;
};
// useComments.ts
import { useQuery } from '@tanstack/react-query';
import commentQueryKey from '../constants/commentQueryKey';
import { fetchComments } from '../api/comment';
import { AxiosError } from 'axios';
interface CommentsQueryProps {
storeId: string | undefined;
currentPage: number;
}
const commentQueryKey = ['comments']
const commentsQuery = ({ storeId, currentPage }: CommentsQueryProps) => ({
queryKey: [...commentQueryKey, storeId, currentPage],
queryFn: fetchComments(storeId, currentPage),
onError: (error: AxiosError) => console.error(error),
keepPreviousData: true,
});
const useComments = ({ storeId, currentPage }: CommentsQueryProps) => useQuery(commentsQuery({ storeId, currentPage }));
export default useComments;
- commentsQuery를 살펴보자. storeId와 currentPage를 받아 queryKey, queryFn, keepPreviousData를 설정한다.
- queryKey는 commentsQueryKey와 파라미터(storeId, currentPage)를 함께 주어 storeId가 변경될 때, 그리고 페이지가 변경될 때 데이터가 refetch되도록 한다. storeId는 다른 가게 아이템을 클릭했을 때, currentPage는 다른 페이지 버튼을 클릭했을 때 변경된다.
- queryFn은 pagination이 된 api를 호출한다.
- keepPreviousData 옵션을 true로 주어 새로운 데이터를 받아올때까지 이전 데이터를 유지하도록 한다. (중요 !!)
그리고 아래와 같이 쿼리를 호출해 사용할 수 있다.
// CommentsList.tsx
const CommentsList = () => {
const { storeId } = useParams();
const [currentPage, setCurrentPage] = React.useState(1);
// ...생략
const { data } = useComments({ storeId, currentPage });
const { data: commentsData, totalPages }: CommentsDataType = data;
return (
<CommentsContainer>
<Label>댓글</Label>
<Divider my="sm" />
<CommentsTextArea addComment={addComment} setCurrentPage={setCurrentPage} />
{commentsData?.map((commentData, idx) => (
<Comment
key={commentData.commentId}
commentData={commentData}
deleteComment={deleteComment}
hasBorder={idx !== COMMENTS_FETCH_SIZE - 1}
/>
))}
<Pagination
currentPage={currentPage}
setCurrentPage={setCurrentPage}
commentsData={commentsData}
totalPages={totalPages}
/>
</CommentsContainer>
);
};
Pagination 컴포넌트로 currentPage와 keepPreviousData가 적용된 commentsData를 전달한다. 이렇게 하면 페이지를 변경해도 화면이 깜빡거리지 않는다.
// Pagination.ts
const Pagination = ({ currentPage, setCurrentPage, commentsData, totalPages }: PaginationProps) => {
const page = Math.ceil(currentPage / COMMENTS_FETCH_SIZE); // 현재 페이지
const startIndex = (page - 1) * COMMENTS_FETCH_SIZE; // 현재 페이지 시작 인덱스
const endIndex = startIndex + +COMMENTS_FETCH_SIZE; // 현재 페이지 끝 인덱스
const currentPages = Array.from(
{ length: totalPages < 5 ? totalPages : endIndex - startIndex },
(_, i) => startIndex + 1 + i
);
// 페이지 버튼 클릭
const handlePageBtnClick = (page: number) => () => {
setCurrentPage(page);
};
// 이전 버튼 클릭
const handlePrevBtnClick = () => {
setCurrentPage(startIndex);
};
// 다음 버튼 클릭
const handleNextBtnClick = () => {
setCurrentPage(endIndex + 1);
};
return (
<ButtonContainer>
<ButtonGroup>
// 이전 버튼(<)
<Button>
<SlArrowLeft onClick={handlePrevBtnClick} />
</Button>
// 페이지 숫자 버튼
{currentPages.map(pageNum => (
<Button key={pageNum} onClick={handlePageBtnClick(pageNum)}>
{pageNum}
</Button>
))}
// 다음 버튼(>)
<Button>
<SlArrowRight onClick={handlePrevBtnClick} />
</Button>
</ButtonGroup>
</ButtonContainer>
);
};
export default Pagination;
REF
'FrontEnd > React' 카테고리의 다른 글
TanStack Query를 활용하여 Route 기반 Prefetching하기(feat.Render-as-you-fetch) (25) | 2024.02.27 |
---|---|
[React Query] React Query에서 ErrorBoundary로 에러 핸들링하기 (3) | 2023.12.10 |
[React-Query] 쿼리 키 정복하기 (3) | 2023.07.03 |
[React-Query] useMutation 활용편(낙관적 업데이트, custom hook으로 활용하기) (0) | 2023.07.01 |
[React-Query] useMutation 개념편 (0) | 2023.07.01 |