react-query Optimistic Update(낙관적 업데이트)

Song-Minhyung·2023년 5월 14일
4
post-thumbnail

Optimistic Update?

공식 문서를 보면 아래와같이 정의하고 있다.

When you optimistically update your state before performing a mutation, there is a chance that the mutation will fail. In most of these failure cases, you can just trigger a refetch for your optimistic queries to revert them to their true server state. In some circumstances though, refetching may not work correctly and the mutation error could represent some type of server issue that won't make it possible to refetch. In this event, you can instead choose to rollback your update.

간단하게 번역하면 이렇다.
mutation을 수행하기 전 낙관적으로 업데이트를 하면 mutation이 실패할 가능성이 있다. 대부분의 실폐에서 다시 가져오기를 트리거해 실제 서버 상태로 되돌릴수가 있다. 하지만 일부 상황에서 이마저 제대로 작동하지 않을 수 있는데 이때는 롤백으로 상태를 최초로 되돌릴수가 있다.

좋지않은 사용자 경험

최근 프로젝트를 진행하다가 사용자가 실제로 앱을 사용할 때 ux가 좋지 않은 부분을 발견했다.

우선 찜하기 버튼을 눌렀을 때 useMutation으로 해당 장소를 찜하는 API를 호출한다.
그 후 usqQuery를 사용해 좋아요를 했는지 여부를 받아오는 API를 호출한다.

여기서 문제가 발생한다 만약 사용자의 인터넷 환경이 안좋거나 서버가 느릴경우 아래 사진처럼 찜하기 버튼이 눌리는데까지 시간이 걸릴수가 있게된다.
(아래는 임의로 3G 네트워크 정도 속도로 느리게 테스트했다.)

위와같은 경험을 좋게 바꾸려면 버튼이 눌리자마자 하트 버튼이 채워져야 할것이다.
전에 redux를 사용할땐 값이 업데이트 되면 우선 store의 값을 임시로 입력한 값으로 바꿔준 후
서버의 값을 업데이트 하고, 다시 store의 값을 값을 서버에서 불러온 값으로 바꿔줬다.

react-query는 위와같은 과정을 간단하게 해결할 수 있는 방법을 제공해준다.
진짜 쓰면서 알아갈수록 더욱더 좋아지는 라이브러리다.

react-query로 Optimistic Update를 하는법

모두 공식 문서에 잘 설명이 되어있따.

useMutation({
  mutationFn: updateTodo,
  // mutate가 호출되면 onMutate를 실행시킴:
  onMutate: async (newTodo) => {
    // 진행중인 refetch가 있다면 취소시킨다.
    // 만약 그러지 않는다면 refetchOnMount등을 true로 해뒀을 때 
    // 페이지를 들어오자 마자 refetch를 하면 refetch가 두번 실행되고, 
    // 화면에 최신 데이터를 그려주지 않을 가능성이 있다.
    // 그것을 방지하기 위해 cancelQueries를 실행시켜준다.
    await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })

    // 이전 쿼리값의 스냅샷
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])

    // setQueryData 함수를 사용해 newTodo로 Optimistic Update를 실시한다.
    queryClient.setQueryData(['todos', newTodo.id], newTodo)

    // context를 리턴하는데 여기에는 이전 스냅샷, 새로운 값을 넣어 리턴해준다.
    // 혹은 롤백하는 함수를 여기서 리턴해줘도 된다.
    return { previousTodo, newTodo }
  },
  // If the mutation fails, use the context we returned above
  // mutation 실패시 onMutate가 리턴한 context를 사용해 값을 되돌린다.
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(
      ['todos', context.newTodo.id],
      context.previousTodo,
    )
  },
  // 성공하거나 실패시 쿼리를 무효화해 최신 데이터를 받아와준다.
  onSettled: (newTodo) => {
    queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] })
  },
})

Optimistic Update 적용후 사용자 경험

const useDeleteLike = (idx: string) => {
  const queryClient = useQueryClient();
  return useMutation(['deleteLike'], {
    mutationFn: () => deleteLike(idx),
    onMutate: async () => {
      const oldData = queryClient.getQueryData(['like']);
      console.log('oldData: ', oldData);
      // 우리 update overwrite하지 않기 위해 미리 취소
      await queryClient.cancelQueries(['like']);
      // 미리 UI에 적용시켜 놓음
      queryClient.setQueryData(['like'], '');
      // 만약 에러나서 롤백 되면 이전 것을 써놓음.
      return () => queryClient.setQueryData(['like'], oldData);
    },
    onError: (error, variable, rollback) => {
      if (rollback) rollback();
      else console.log(error);
    },
    onSettled: () => {
      queryClient.invalidateQueries(['like']);
    },
  });
};

위와같이 낙관적 업데이트를 적용했더니

  • 위와 같았던 사용자 경험이

  • 이렇게 눈에띄게 좋아졌다.
profile
기록하는 블로그

2개의 댓글

comment-user-thumbnail
2023년 10월 24일

잘읽었습니다 ~

1개의 답글