
tanstack-query v5를 이용한 댓글 삭제 기능을 구현하는데 refetch는 항상 데이터를 가지고 온다는 것을 알게 되었다. 보다 성능 또는 UX적으로 보다 나은 방법이 있는지 고민하다가 낙관적 업데이트라는 기능을 접하게 되었고, 이를 적용한 뒤 어떤 점에서 개선되었는지 공유하고자 한다.
우선 refetch는 쿼리를 수동으로 다시 가져오는 함수로, 항상 데이터를 가지고 오며 요청 성공시에 UI 변화시킨다.
// 기존 PostManagePage.tsx 일부 코드
// 후기 리스트를 가져오는 쿼리 (조건부로 호출)
const {
status: statusReviewList,
data: reviewList,
error: errorReviewList,
refetch: refetchReviews, //
} = useQuery({
queryKey: ["reviewList", showId],
queryFn: () => getReviewList(showId),
enabled: !!showId,
});
// 후기 삭제를 위한 뮤테이션
const { mutate: deleteMutate } = useMutation({
mutationFn: (review: ReviewDeleteParamType) => deleteAdminReview(review),
onSuccess: () => refetchReviews(), // 삭제 성공시 refetch로 reviewList 다시 불러옴
onError: () => toast.error("댓글 삭제 실패"),
});
해당 모달창에서는 댓글 리스트 요청이 전부이고 [’reviewList’, showId] 쿼리는 해당 모달창에서만 사용하기 때문에 단순히 refetch에서 invalideQueries로 변경하는 것은 차이가 없다고 판단
refetch는 모든 페이지의 데이터를 불러오지만 (키를 기준으로 inactive 한 데이터 까지 불러옴)
invalidation은 우선 query를 stale하게만 만들기 때문에 active한 페이지의 데이터를 가져옴
낙관적 업데이트를 사용하면 인터넷 속도가 느리거나 서버가 느릴 때 사용자 경험 측면에서 좋으므로 refetch에서 해당 방법으로 변경
// 후기 리스트를 가져오는 쿼리 (조건부로 호출)
const {
status: statusReviewList,
data: reviewList,
error: errorReviewList,
} = useQuery({
queryKey: ["reviewList", showId],
queryFn: () => getReviewList(showId as string),
enabled: !!showId,
});
// 후기 삭제를 위한 뮤테이션
const { mutate: deleteMutate } = useMutation({
mutationFn: (review: ReviewDeleteParamType) => deleteAdminReview(review),
onMutate: async (review) => {
await queryClient.cancelQueries({ queryKey: ["reviewList", showId] });
const oldData = queryClient.getQueryData<ReviewType[]>(["reviewList", showId])!;
const newData = oldData.filter((item) => item.id !== review.review_id);
queryClient.setQueryData(["reviewList", showId], [...newData]);
return { oldData };
},
onError: (_error, _variables, context) => {
queryClient.setQueryData(["reviewList", showId], [...context!.oldData]);
toast.error("댓글 삭제 실패");
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ["reviewList", showId] });
queryClient.invalidateQueries({ queryKey: ["showList"] });
},
});
화면을 최신 상태로 유지하는 가장 간단한 방법
게시글을 작성하거나 삭제할때 목록을 실시간으로 최신화 해야하는 경우, query Key가 변하지 않으므로 강제로 쿼리를 무효화하고 최신화를 진행해야함
즉, query가 오래되었다는 것을 판단하고 다시 refetch를 할 때 사용
query가 stale 상태로 변경된다고 바로 데이터를 다시 가져오지 않고 대략 다음과 같은 조건에서 실행
queryKey에 "쿼리키"를 포함하는 모든 쿼리가 무효화
queryClient.invalidateQueries({ queryKey: ["쿼리키"] });
queryClient.setQueryData(["쿼리키"], (oldData: any) => {
return {
...oldData,
data: [...oldData.data, data.data],
};
});
useMutation({
mutationFn: updateTodo,
// mutate가 호출되면 onMutate를 실행시킴:
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })
// 이전 값 저장
const previousTodo = queryClient.getQueryData(['todos', newTodo.id])
// 낙관적 업데이트
queryClient.setQueryData(['todos', newTodo.id], newTodo)
// context를 리턴하는데 여기에는 이전 스냅샷, 새로운 값을 넣어 리턴
// 혹은 롤백하는 함수를 여기서 리턴해줘도 됨
return { previousTodo, newTodo }
},
// mutation 실패시 onMutate가 리턴한 context를 사용해 값을 되돌림
onError: (err, newTodo, context) => {
queryClient.setQueryData(
['todos', context.newTodo.id],
context.previousTodo,
)
},
// 성공하거나 실패시 쿼리를 무효화해 최신 데이터를 받아와 연동
onSettled: (newTodo) => {
queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] })
},
})
https://careerly.co.kr/qnas/1362
https://github.com/ssi02014/react-query-tutorial?tab=readme-ov-file