React Query - 낙관적 업데이트

박정호·2023년 1월 17일
0

React Query

목록 보기
12/14
post-thumbnail

🚀 Start

앞서 구현해본 쿼리 무효화를 통한 데이터 업데이트는 한가지 단점이 존재한다.

아래의 network 관리창을 확인해보면 appointments 데이터가 두번 통신한 것을 확인할 수 있다.

왜냐하면 한번은 기존의 데이터가 요청되었다가 무효화되었고, 새로운 데이터가 또 한번 요청되었기 때문이다.

  • (첫번째 appointments는 무효화되어 No Content라고 뜨는 것을 확인.)

이럴경우 결국 서버에 업데이트된 데이터가 들어올 때까지 기다리고 화면에 데이터가 반영될 것이다.

예를 들어 좋아요 버튼 클릭시 위의 과정들을 모두 거치고 반영되기 때문에 좋아요가 눌린 화면은 언제 들어올지 미지수다.

🧐 그렇다면 다른 방법은 없을까?

서버 업데이트를 하기 전에 미리 화면의 UI를 바꿔준 후, 서버와의 통신 결과에 따라 확정 / 롤백을 결정하는 방식의 낙관적 업데이트를 진행하는 낙관적 업데이트 방식을 구현해보자.

쉽게 말해, 좋아요를 누르면 일단 즉시 화면에 좋아요가 눌린 화면이 반영되고, 서버에서 요청의 성공 유무에 따라 다시 데이터를 출력시킨다.

왜냐하면 말 그대로 낙관적으로 생각하는 것이다. 어차피 화면의 데이터는 변하게 되어있어 라는 생각을 가지고 진행한다고 보면 될 것 같다.



⭐️ 낙관적 업데이트

낙관적 업데이트는 onMutate를 통해 서버로 보내지는 것을 가로채서 업데이트 후에 수정사항을 보내게 됨으로 속도가 더 빠르다.

따라서 캐시가 더 빨리 업데이트된다는 장점이 있다. 왜냐하면 캐시를 업데이트하기 위해 서버 응답을 기다릴 필요가 없기 때문이다.

단, 한가지 단점은 서버 업데이트가 실패했을 경우에 실행되는 코드가 복잡하다는 것이다.


Updates from Mutation Responses

서버에서 개체를 업데이트하는 변이를 처리할 때 변이에 대한 응답으로 새 개체가 자동으로 반환되는 것이 일반적이다. (참고)

👎 해당 항목에 대한 쿼리를 다시 가져오고 이미 가지고 있는 데이터에 대한 네트워크 호출을 낭비.

👍 Query Client의 setQueryData 메서드를 사용하여 변형 함수가 반환한 개체를 활용하고 기존 쿼리를 새 데이터로 즉시 업데이트 가능



👉 OnMutate

만약 서버 업데이트가 실패할 경우는 업데이트 이전의 데이터로 되돌려야 한다는 뜻이고 해당 데이터를 저장해둬야하므로 캐시에 데이터가 필요하단 뜻이다.

  • useMutations에는 onMutate 콜백이 존재한다.

이를 통해 context값을 반환하고 onError 핸들러가 이 콘텍스트 값을 인수로 받는다.
(여기서 context는 낙관적 업데이트가 적용되기 전의 데이터를 의미한다.)

또한 진행중인 모든 refetch를 취소한다. refetch를 취소해야 이전 데이터가 다시 캐시에 저장되는 것을 막을 수 있기 때문이다.

따라서 만약 에러가 생기면 onError 핸들러가 호출되어 실행되고 캐시 값을 이전으로 복원시킬 수 있다.



👉 쿼리 취소

앞서 말했듯이 refetch를 취소해야하며, 이 말은 query를 취소해야한다는 뜻이다.

React Query의 일부 쿼리는 자동적으로 취소된다. 요청 중에 기한이 만료되거나 비활성화 또는 컴포넌트가 헤제되는 경우가 있을 것이다.

하지만, 지금처럼 낙관적 업데이트를 위한 수동적인 쿼리 취소가 필요한 경우 서버와의 통신을 돕는 Axios에 중단한다는 신호를 쿼리함수의 인수에 담아 전달해야한다.

React Query는 표준 JS 인터페이스에 해당하는 AboutController 인터페이스로 쿼리를 취소한다.

async function getUser(signal: AbortSignal): Promise<User | null> {
  if (!user) return null;
  const { data }: AxiosResponse<{ user: User }> = await axiosInstance.get(
    `/user/${user.id}`,
    {
      headers: getJWTHeader(user),
      signal,
    },
  );
  return data.user;
}

...

useQuery(queryKeys.user, ({ signal }) => getUser(user, signal) });


👉 구현

export function usePatchUser(): UseMutateFunction<...> {
  const { user, updateUser } = useUser();
  const toast = useCustomToast();
  const queryClient = useQueryClient();

  const { mutate: patchUser } = useMutation(
    (newUserData: User) => patchUserOnServer(newUserData, user),
    {
      onMutate: async (newData: User | null) => {
        
        // 사용자 데이터에 대한 모든 퀴리요청을 취소하여 이전 서버 데이터가 낙관적 업데이트를 덮어쓰지 않도록.
        queryClient.cancelQueries(queryKeys.user);
        
        // 이전 사용자 값의 snapshot
        const previousUserData: User = queryClient.getQueriesData(queryKeys.user);
        
        //  낙관적업데이트는 새로운 사용자 값으로 캐시를 업데이트.
        updateUser(newData);
        
        //  snapshot 값이 있는 컨텍스트 객체 반환
        return { previousUserData };
      },
      onError: (error, newData, context) => {
        
        // 캐시를 저장된 값으로 롤백
        if (context.previousUserData) {
          updateUser(context.previousUserData);
          toast({
            title: 'Update failed, restoring previous values',
            status: 'warning',
          });
        }
      },
      onSuccess: (userData: User | null) => {
        if (user) {
          toast({
            title: 'User updated!',
            status: 'success',
          });
        }
      },
      onSettled: () => {
        // 쿼리 함수의 성공, 실패 두 경우 모두 실행.
        queryClient.invalidateQueries(queryKeys.user);
      },
    },
  );

  return patchUser;
}

...

//useUser.ts (커스텀훅)
const updateUser(newUser: User): void {
    queryClient.setQueryData(queryKeys.user, newUser);
 }



💡 참고하자
👉 Query Cancellation
👉 Axios - Cancellation
👉 AbortController
👉 리액트 쿼리 : 뮤테이션

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글