react query에서의 optimistic update

HSKwon·2023년 10월 5일
3
post-thumbnail

useMutation 훅의 옵션 중 하나인 onMutate에 대한 정리.

우선, onMutate 는 mutation을 시작하기 직전에 호출되는 콜백함수이다.

1. Optimistic Update

  • 데이터를 실시간으로 변경하는 것처럼 보이게 하려면, 서버의 응답을 기다리지 않고 클라이언트쪽에서 UI를 먼저 업데이트할 수 있다. 만약 서버에서 오류가 응답되면 원래 상태로 롤백하는 작업을 수행한다.

  • getQueryData를 사용하여 현재 캐싱된 데이터를 가져오는데, mutation이 실패한 경우 원래 상태로 롤백하는데 사용될 수 있다. onError 옵션과 함께 사용된다.

  • 아래의 코드에서 originalTodos를 반환하고 있는데, 이 반환 객체는 react query의 context로 전달된다. context는 여러 옵션들 (onSuccess, onError, onSettled)의 콜백에서 첫번째 인자 다음에 제공된다. onError에서는 mutation이 실패했을 경우에 대한 콜백이 실행되므로 원래 캐싱된 데이터로 롤백하기 위해

    와 같이 작성해준다.

onMutate: (newTodo) => {
  // todos라는 쿼리키를 이용, 원래 상태 저장
  const originalTodos = queryClient.getQueryData('todos');
  
  // 새 todo 항목을 추가하여 클라이언트 상태 업데이트
  queryClient.setQueryData('todos', old => [...old, newTodo]);
  
  // 원래 상태 반환하여 필요한 경우 롤백할 수 있게 함
  return { originalTodos };
}

2. Mutation 취소

  • onMutate에서는 현재 진행중인 관련된 쿼리를 취소하여 mutation 중에 쿼리 결과가 중복 반환되는 것을 방지할 수 있다.
onMutate: () => {
  queryClient.cancelQueries('todos');
}

3. 사전 처리

  • 입력 데이터를 mutation 하기 전에 조정하거나 유효성 검사 등을 수행할 수 있다.
onMutate: (values) => {
  if(!isValid(values)) {
    throw new Error('Invalid data');
  }
}

4. 호출 순서

  1. cancelQuery를 통해 진행중인 쿼리를 취소함으로써, mutation 동안 다른 요청에 의한 변경을 방지한다.
  2. getQueryData를 사용하여 현재 캐싱된 데이터를 가져온다.
  3. setQueryData를 사용하여 optimistic update를 수행한다.

    👉 만약 순서가 바뀌어서 cancelQuery 전에 getQueryData와 setQueryData를 호출한다고 가정해 본다면, 그 사이 다른 요청이 발생할 경우 데이터 불일치 문제가 발생할 수 있다.

onMutate에 대한 내용을 참고하여 update를 수행하는 훅을 살펴보자.

import { useMutation } from 'react-query';
import client from '@/lib/client';
import { queryClient } from '@/lib/react-query';
import { RecommendDto } from '@/model/models';

export const updateToolRecommends = (toolRecommend: RecommendDto) => {
  return client.put(`/api/v2/admin/recommends`, toolRecommend);
};

export const useUpdateToolRecommends = (id: string) => {
  const queryKey = ['recommends', id];

  return useMutation({
    mutationKey: ['update-toolRecommend'],
    mutationFn: updateToolRecommends,
    async onMutate(newUser) {
      // mutation이 성공적으로 실행될것이라 가정하고 현재 쿼리 데이터를 수정했기 때문에
      // 현재의 mutation과의 충돌을 방지하기 위해 cancleQueries를 이용해 쿼리를 취소해준다.
      1️⃣await queryClient.cancelQueries(queryKey);

      // recommends 쿼리키로 캐싱되어있는 데이터를 가져온다.
      // prev는 mutation이 실패했을 때 원래의 경우로 롤백하기위해 사용된다. (onError)
      // mutation 이전의 값을 가져와 context로 이전시킨다.
      2️⃣const prev = queryClient.getQueryData(queryKey);
		
      // optimistic update를 위해 쿼리 데이터를 임시로 수정된 값으로 업데이트 한다.
      3️⃣queryClient.setQueriesData(queryKey, old => ({
        ...(old ?? {}),
        ...newUser,
      }));
	
      // 업데이트 이전의 값이 담긴 context 객체를 return 한다.
      return { prev };
    },
    
    onSuccess: () => {
      toast.success({ message: '수정하였습니다.' });
    },
    
    // 만약 mutate 작업이 실패했을 경우, 이전에 저장했던 값으로 다시 롤백시킨다.
    onError: (err: any, __, context: any) => {
      toast.error({
        message: err.message ?? '실패하였습니다',
        description: '다시 시도해주세요.',
      });

      if (context?.prev) queryClient.setQueryData(queryKey, context.prev);
    },
    
    // mutation의 성공 혹은 실패 여부와 관계없이 mutation 이후에 쿼리를 stale하게 만들어 refetch를 유도한다.
    onSettled() {
      queryClient.invalidateQueries(queryKey);
    },
    useErrorBoundary: false,
  });
};

onMutation과 getQueryData

  • onMutate는 mutation이 시작되기 직전에 호출된다.
  • 여기에서 getQueryData를 사용하여 현재 캐시에 저장된 데이터(recommends라는 쿼리키에 캐싱된 데이터)를 가져온다.
  • 가져온 데이터는 prev로 반환되고, 이는 mutation이 실패할경우(onError) 원래의 상태로 롤백하기 위해 사용된다.

onError에서의 롤백

  • onError는 mutation이 실패했을 때 호출된다.
  • 이때, context?.prev를 통해 setQueryData를 사용하여 캐시의 데이터를 원래 상태로 롤백한다.
  • 만약 mutation이 실패하더라도 사용자에게는 이전 상태의 데이터가 계속 보이도록 하는것이다.

cancelQueries

  • 해당 쿼리키와 일치하는, 진행중인 모든 쿼리를 취소한다.
  • 일반적으로, mutation이 발생하기 전에 현재 진행중인 쿼리를 중단하여 UI가 일관성을 유지하도록 한다.

    예를 들어 사용자가 어떤 항목을 수정하려고 할때, 수정 작업이 수행되기 전에 해당 항목의 데이터를 가져오는 쿼리가 진행중이라면, 그 쿼리를 취소하여 데이터의 일관성을 보장할 수 있는 것이다.

onSettled에서의 invalidateQueries

  • onSettled는 mutation이 성공하든 실패하든 상관없이 끝난 뒤에 호출된다.
  • 여기에서 invalidateQueries를 호출하여 recommends 쿼리를 무효화한다.
  • 이렇게 하면, 해당 쿼리가 이후에 호출될 때 최신의 데이터를 서버로부터 다시 가져올 수 있다.
  • 이는 mutation이 성공적으로 이루어졌더라도 서버와의 데이터 동기화를 보장하기 위해 필요하다.

즉, onMutate 안에서 cancelQueries를 통해 mutation이 발생하기 전에 해당 쿼리를 취소하고, onSettled 안에서 invalidateQueries를 사용하여 mutation이 완료된 이후에 해당 쿼리를 무효화하여 최신 데이터를 다시 가져올 수 있도록 하는것이다!

onMutate

  • react query에서 제공하는 옵션으로, mutation이 실제로 발생하기 전에 일부 로직을 실행하기 위해 사용한다.
  • optimistic update : 사용자에게 mutation의 결과를 즉시 반영하는것처럼 보이게 하려면 (실제로는 아직 서버의 응답을 받지 않았지만) onMutate에서 캐시를 일시적으로 업데이트할 수 있다.

정리

  • onMutate에서는 mutation이 실패할 경우를 대비하여 현재 캐시의 데이터 상태를 저장하고, onError에서는 mutation이 실패했을때 이전의 상태로 롤백한다. onSettled에서는 mutation이 끝난 후에 recommends 쿼리를 무효화하여 해당 쿼리가 다시 호출될 때 최신 데이터를 가져올 수 있도록 하는것이다.
profile
공부한 내용이나 관심 있는 정보를 글로 정리하며 익숙하게 만들고자 합니다.

0개의 댓글

관련 채용 정보