지금까지는 데이터를 가져오는 방식의 query 요청에서 대해서 커스텀훅을 사용하여 전역적으로 사용할 수 있게 하는 방법들에 대해 알아보았다.
이번엔 데이터 업데이트에 대한 mutate 요청에 대해서도 커스텀훅을 사용하여 전역적으로 패칭이 가능하게 하고 그에 대한 에러 처리도 해주자.
query 요청에 대한 에러와 로딩에 대해서는 이미 전역적으로 처리를 하였다. mutate도 같은 방식으로 전역 처리해주자.
✅ Error
// queryClient.ts
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
...
},
mutations: {
onError: queryErrorHandler,
},
},
});
✅ Loading
// queryClient.ts
export const Loading = (): ReactElement => {
const isFetching = useIsFetching();
const isMutating = useIsMutating();
const display = isFetching || isMutating ? 'inherit' : 'none';
return (
<Spinner>
<Text display="none">Loading...</Text>
</Spinner>
);
}
그럼 이제 데이터 변경요청을 하기 위해서 useMutation
을 사용해보자.
useMutation
: 데이터를 생성 / 업데이트 / 삭제 할 때 사용 💡 참고하자
👉 React Query - useMutation
👉 useMutation
👉 Mutations
React Query에서는 API가 반환하는 데이터의 타입을 알 수 없기 때문에 Generics를 많이 사용한다. 만약 타입을 지정하지 않고 사용하게 되면 대부분 데이터가 unknown, any 상태로 사용하게 돼서 어떤 데이터를 다루는지 알기가 어렵다.
따라서 react-query는 typescript의 제네릭 형식을 강력하게 드라이브 하고 있기 때문에 제네릭 타입만 잘 사용한다면 타입을 보장 받으면서 편리하게 데이터를 다룰 수 있다.
Generic이란 데이터의 타입을 일반화한다(generalize)한다는 것을 뜻하며, 자료형을 정하지 않고 여러 타입을 사용할 수 있게 해준다.
즉, 선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법이다.
✅ useMutation & useMutateFucntion
useMutateFunction
에는 몇가지 매개변수가 있다.
TData
: 변이 함수 자체에서 반환된 데이터TError
: 발생할 것으로 예상되는 오류Tvariables
: mutate함수가 예상하는 변수TContext
: 낙관적 업데이트 롤백을 위해 onMutate에 설정useMutateFunction<TData = unknown, TError = unknown, TVariables = void, TContext = unknown>
✅ 적용
void
: mutate함수로부터 반환될 데이터는 없으므로 voidunknown
: 오류 유형은 unkownAppointment
: mutate함수에 인수로 전달될 변수인 appointment 타입 정의unknown
: 낙관적 업데이트는 잠시후 작성후 작성. 일단 unknownexport const useReserveAppointment = (): UseMutateFunction<
void,
unknown,
Appointment,
unknown
> => {
const { user } = useUser();
const toast = useCustomToast();
const { mutate } = useMutation((appointment: Appointment) =>
setAppointmentUser(appointment, user?.id),
);
return mutate;
}
앞서본 mutate함수에 인수로 전달되는 값인 appointment는 예약버튼 클릭시 데이터가 전달되어 setAppointmentUser가 실행되고 서버가 업데이트되고 있는 상태이다.
하지만, 예약클릭을 한후 즉시 화면에 예약결과가 출력되지 않고 새로고침해야 업데이트된 데이터가 출력되는 것을 확인할 수 있다.
왜냐하면 데이터를 업데이트시키는 요청을 하였지만, 그와 동시에 데이터를 받아오는 요청도 함께 이루어지고 있다. 따라서, 업데이트되기 이전인 기존의 데이터가 그대로 출력되고 있는 것이다.
따라서, 관련 쿼리를 무효화하여 데이터가 최신이 아님을 React Query에 알려야 한다.
✅ invalidateQueries를 사용하자!
QueryClient에는 지능적으로 쿼리를 오래된 것으로 표시하고 잠재적으로 다시 가져올 수 있는 invalidateQueries 메서드가 있다.(참고)
이 메서드를 사용하면 사용자가 페이지를 새로고침할 필요없이 즉시 새로운 데이터가 출력되는 것을 확인할 수 있다.
const queryClient = useQueryClient();
const { mutate } = useMutation(
(appointment: Appointment) => setAppointmentUser(appointment, user?.id),
{
onSuccess: () => {
queryClient.invalidateQueries([queryKeys.appointments]);
toast({ // 예약완료 알림창 출력
title: ' You have reserved the appointment!',
status: 'success',
});
},
},
);
하지만, 전체 예약에 대한 쿼리와 사용자의 예약내역 쿼리는 다르기 때문에 전체 예약에 대한 쿼리만 무효화시켰을 뿐 예약내역에는 여전히 기존의 쿼리가 실행되어 최신화된 데이터를 받아오지 못하고 있다.
✅ 이를 위해 쿼리키 접두사(Prefix)를 사용
invalidateQueries
는 정확한 키가 아닌 접두사를 사용한다. 따라서 동일한 쿼리키 접두사로 서로 관련된 쿼리를 설정하면 모든 쿼리를 한번에 무효화할 수 있다.
// 사용자 예약 내역
const { data: userAppointments = fallback } = useQuery(
[queryKeys.appointments, queryKeys.user, user?.id], // 배열형태 업데이트
() => getUserAppointments(user),
{ enabled: !!user },
);
// 로그아웃시 유저정보 삭제 함수
const clearUser = () => {
queryClient.setQueryData(queryKeys.user, null);
queryClient.removeQueries([queryKeys.appointments, queryKeys.user]);
}
async function removeAppointmentUser(appointment: Appointment): Promise<void> {
const patchData = [{ op: 'remove', path: '/userId' }];
await axiosInstance.patch(`/appointment/${appointment.id}`, {
data: patchData,
});
}
...
export function useCancelAppointment(){
...
const { mutate } = useMutation(
(appointment: Appointment) => removeAppointmentUser(appointment),
{
onSuccess: () => {
queryClient.invalidateQueries([queryKeys.appointments]);
toast({
title: ' You have canceled the appointment!',
status: 'success',
});
},
},
);
return mutate;
}
✅ 예약하기
✅ 예약취소