먼저 useQuery와 useMutation의 차이점에 대해 가볍게 알아보자.
❗ useQuery와 useMutation 차이
1. no cache data : useMutation은 일회성이고 fetching이나 reFetching 그리고 업데이트할 데이터가 없기 때문에 캐시 데이터라는 정의가 존재하지 않는다.
2. no retries : 직접 재시도를 구현할 수는 있지만 기본적으로는 재시도가 없다. (useQuery는 기본적으로 3번 재시도 함)
3. no refetch : 관련된 데이터가 없으므로 refetch가 없다.
4. no isLoading vs isFetching : useMutation은 캐시데이터도 없고 isLoading은 데이터가 없을 때 이루어지는 fetching이기 때문에 isLoading과 isFetching이 구분되지 않는다. (캐시데이터가 없으니 isLoading은 없음)
❗ queryClient의 invalidateQueries 메소드
mutate를 통해 데이터를 변경할 때 데이터에 대한 캐시를 무효화 하는데 사용하는 메소드이고 사용자가 페이지를 새로고침 할 필요없이 변경된 데이터를 볼 수 있게 해줄 때 유용하다
invalidateQueries의 effects
1. query를 stale로 표시
2. query가 현재 렌더징 중이면 refetch를 트리거 (query를 사용하는 컴포넌트가 표시되는 경우)
3.mutate를 호출 -> mutate에 있는 onSuccess 핸들러가 관련 쿼리를 무효화 시킴(invalidateQueries) -> 데이터 refetch
해당 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",
});
},
}
);
먼저 useQueryClient에 대한 정의가 필요하다
해당 코드는 setAppointmentUser 함수에 대해서 성공시에 invalidateQueries를 통해 새로고침 없이 즉각적으로 사용자에게 최신 데이터를 보여줄 것이다. invalidateQueries의 인자값엔 캐시를 무효화 하기위해 사용될 쿼리키를 명시해야한다
❗ Optimistic Updates (낙관적인 업데이트)
새 값이 무언인지 알고있는 경우 서버로부터 응답을 받기전에 사용자 캐시를 업데이트 하는 것
캐시를 업데이트 하기 위해 서버 응답을 기다릴 필요가 없어 캐시가 더 빨리 업데이트가 된다는 장점이 있다.
특히, 여러개의 컴포넌트가 해당 데이터를 사용하는 경우 더 유용하게 쓰일 수 있다.
하지만 단점으로는 서버 업데이트가 실패한 경우에는 코드가 많이 복잡해질 수 있다
서버 업데이트가 실패한 경우 업데이트 이전의 데이터로 롤백을 해야하는데 이를 사용하려면 해당 데이터를 저장해둬야 한다. 이 떄 사용해야하는 방법은 에러가 생기면 useMutation의 onMutate 콜백에서 onError 핸들러를 통해 context value(낙관적 업데이트를 적용하기 전의 value)를 인수로 받아서 캐시 값을 이전으로 복원하는것이다.
이에 대해 간단히 읊어보자면 캐시를 업데이트할 데이터를 포함하는 특정 쿼리에서 onMutate 함수는 진행 중인 모든 refetch를 취소하게 되고 refetch가 진행되는 동안 캐시를 업데이트 하는데 서버에서 다시 가져온 이전 데이터로 캐시를 덮어씌어 낙관적 업데이트를 한 후에 이전 데이터로 캐시를 덮어쓰지 않도록 쿼리를 취소해야한다. (쿼리를 취소하지 않으면 쿼리를 다시 가져올 수 있다.)
낙관적 업데이트의 작업 흐름을 봐보도록 하자
1. 사용자가 업데이트를 트리거하여 mutate를 호출
2. mutate가 실행되어 업데이트를 위한 데이터를 서버로 보내고 onMutate 콜백도 실행된다.
-> onMutate 콜백의 작업은 사용자가 직접 작성해야하고 해당 작업에는 서버에서 오는 데이터가 낙관적 업데이트를 훼손하지 않도록 진행 중인 쿼리를 취소하는 것, 쿼리 캐시를 낙관적으로 업데이트하는 것, 이전 캐시 값을 onMutate 핸들러에서 반환된 context로 저장하는 것이 있다.
3. 서버 업데이트에 성공했다면 서버에서 최신 데이터를 가져올 수 있도록 invalidateQuery를 통해 쿼리를 무효화한다.
4. 만약 서버 업데이트를 성공하지 못했다면 onError 콜백이 실행되고 onMutate에서 반환된 context를 사용하여 캐시를 낙관적 업데이트를 하기 전 상태로 되돌리기 위한 작업을 거치고 (콜백 직접 작성) 동일하게 invalidateQuery를 통해 쿼리를 무효화한다.
** 쿼리를 취소하기 위해선 promise를 반환하는 함수가 필요함
다음으로는 예제 코드를 살펴보자,
예제 코드는 길이가 길어서 1,2,3번과 같이 나열하기 보다는 각 코드마다 주석을 달아두었으니
직접 세세하게 확인해보도록 하자.
const { mutate: patchUser } = useMutation(
(newUserData: User) => patchUserOnServer(newUserData, user),
{
// onMutate 함수는 onError 핸들러에 context를 반환함
onMutate: async (newData: User | null) => {
// 발신하는 모든 쿼리를 취소함, 즉 오래된 서버 데이터는 낙관적 업데이트를 덮어씌지 않음
queryClient.cancelQueries([queryKeys.user]);
// 기존 사용자 데이터(업데이트 하기전에 캐시에 있었던 값)의 snapshot을 찍음
const previousUserData: User = queryClient.getQueryData([
queryKeys.user,
]);
// 새로운 값으로 캐시를 낙관적 업데이트를 함
updateUser(newData);
// 해당 context를 return
return { previousUserData };
},
onError: (error, newData, context) => {
// error가 있는 경우 저장된 값으로 캐시를 롤백
if (context.previousUserData) {
updateUser(context.previousUserData);
toast({
title: "Update failed; restoring previous values",
status: "success",
});
}
},
onSuccess: (userData: User | null) => {
if (user) {
toast({
title: "User updated!",
status: "success",
});
}
},
// mutate를 resolve했을 때 성공 여부와 관계없이 onSettles 콜백을 실행
onSettled: () => {
// 사용자에 대한 데이터를 무효화하여 서버에서 최신 데이터를 보여줄 수 있도록 함
queryClient.invalidateQueries([queryKeys.user]);
},
}
);
끝 !
'자바스크립트 - React.js' 카테고리의 다른 글
[ReactQuery] useMutation과 useQuery에서 TypeScript 적용하기 (2) | 2023.11.07 |
---|---|
[React.js] Recoil의 atom, atomfamily와 selector, selectorFamily에 대해서 알아보자 (0) | 2023.09.12 |
[ReactQuery] refetch에 대해서 알아보자 ! (feat. 전역 refetching, Polling) (0) | 2023.07.24 |
[ReactQuery] useQuery의 select 옵션을 사용해보자 ! (0) | 2023.07.18 |
[ReactQuery] Query Key - 의존성 배열을 활용해보자 ! (2) | 2023.07.14 |