Tanstack Query의 mutateAsync 함수를 사용하다가 이슈가 생겨서 작성한 글입니다.
mutate, mutateAsync 함수는 Tanstack Query의 useMutation() 훅을 호출하면 return 되는 함수이다.
해당 함수를 실행하여 생성, 수정, 삭제를 처리하게 되고, api 응답 결과에 따라 기존 쿼리 키로 캐싱된 데이터를 invalidate해서 staleTime을 초기화시킨다.
staleTime이 초기화되면 refetching이 트리거 되는 구조라, useQuery()나 useInfinityQuery()를 사용하여 캐싱했다면, mutate로 캐싱된 데이터를 invalidate 해주는 것이 거의 반필수이다.
생성, 수정, 삭제를 위해 사용하는 함수는 크게 2가지 이다.
첫째, mutate
둘째, mutateAsync
두 함수의 결정적 차이는 Promise를 반환하는 구조인가 아닌가이다.
mutate는 Promise를 반환하지 않기에 await 키워드나 then 체이닝으로 사용할 수 없다.
mutateAsync는 Promise를 반환하기에 await 키워드를 사용할 수 있고, 에러를 핸들링하기 위해 try~catch문으로 구성한다.
나는 useMutation()의 옵션으로 onMutate와 onError, onSettled를 사용하여 낙관적 UI 업데이트를 구성했다.
하지만, 오류가 발생했을 때 mutateAsync는 onError를 실행하지 않았고, onMutate와 onSettled는 정상 실행됐다.
// 낙관적 업데이트 커스텀 훅
export const useOptimisticEmotionLog = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (params: EmotionRequset) => {
return postTodayEmotionLog(params);
},
onMutate: async (params: EmotionRequset) => {
const previousData: TodayEmotionLogs = queryClient.getQueryData(['todayEmotionLog'])!;
await queryClient.cancelQueries({ queryKey: ['todayEmotionLog'] });
queryClient.setQueryData(['todayEmotionLog'], {
...previousData,
emotion: params.emotion,
});
return previousData;
},
onError: (error, variables, context) => {
if (context) {
queryClient.setQueryData(['todayEmotionLog'], context);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todayEmotionLog'] });
queryClient.invalidateQueries({ queryKey: ['month'] });
},
});
};
// 클릭 이벤트 핸들러
const handleEmotionClick = async (emotion: Emotion) => {
setSelectedEmotion(emotion);
await updateEmotionLog({emotion}); //custom hook에서 추출한 mutateAsync
};
위와 같이 코드를 작성했을 때 기대했던 동작은 오류 발생 시 UI가 롤백되는 것이 었는데, 롤백이 되지 않았고, 하물며 애플리케이션에서 오류가 발생했다.
onError가 실행되지 않는 것이 원인이었다.
이를 해결하기 위해 두가지 방법이 존재했다.
1. try~catch문을 통해 catch에서 error를 롤백하기
2. mutateAsync에서 mutate로 변경하기
나는 2번의 방법을 채택하였고, 다음과 같이 코드를 수정하였다.
//클릭 이벤트 핸들러
const handleEmotionClick = async (emotion: Emotion) => {
setSelectedEmotion(emotion);
updateEmotionLog( //custom hook에서 추출한 mutate
{ emotion },
{
onError: (_, __, context) => {
setSelectedEmotion(context?.emotion ?? null);
},
},
);
};
위와 같이 작성하면 커스텀 훅에서 작성한 onError가 무시된다고 생각할 수도 있는데,
먼저 커스텀 훅의 onError가 실행되고, mutate를 사용하는 곳에서 onError를 정의한 것이 후속으로 실행된다.
위와 같은 조치를 한 결과, 롤백처리 및 상태 변경이 의도한대로 동작하였다!