refetch에서 invalidateQueries로, 그리고 Optimistic Update

이보경·2024년 1월 16일
post-thumbnail

tanstack-query v5를 이용한 댓글 삭제 기능을 구현하는데 refetch는 항상 데이터를 가지고 온다는 것을 알게 되었다. 보다 성능 또는 UX적으로 보다 나은 방법이 있는지 고민하다가 낙관적 업데이트라는 기능을 접하게 되었고, 이를 적용한 뒤 어떤 점에서 개선되었는지 공유하고자 한다.

📌 refetch

우선 refetch는 쿼리를 수동으로 다시 가져오는 함수로, 항상 데이터를 가지고 오며 요청 성공시에 UI 변화시킨다.

// 기존 PostManagePage.tsx 일부 코드

// 후기 리스트를 가져오는 쿼리 (조건부로 호출)
const {
  status: statusReviewList,
  data: reviewList,
  error: errorReviewList,
  refetch: refetchReviews, // 
} = useQuery({
  queryKey: ["reviewList", showId],
  queryFn: () => getReviewList(showId),
  enabled: !!showId,
});

// 후기 삭제를 위한 뮤테이션
const { mutate: deleteMutate } = useMutation({
  mutationFn: (review: ReviewDeleteParamType) => deleteAdminReview(review),
  onSuccess: () => refetchReviews(), // 삭제 성공시 refetch로 reviewList 다시 불러옴
  onError: () => toast.error("댓글 삭제 실패"),
});

📌 개선 방법

성능적 측면

해당 모달창에서는 댓글 리스트 요청이 전부이고 [’reviewList’, showId] 쿼리는 해당 모달창에서만 사용하기 때문에 단순히 refetch에서 invalideQueries로 변경하는 것은 차이가 없다고 판단

refetch는 모든 페이지의 데이터를 불러오지만 (키를 기준으로 inactive 한 데이터 까지 불러옴)
invalidation은 우선 query를 stale하게만 만들기 때문에 active한 페이지의 데이터를 가져옴

UX 측면

낙관적 업데이트를 사용하면 인터넷 속도가 느리거나 서버가 느릴 때 사용자 경험 측면에서 좋으므로 refetch에서 해당 방법으로 변경


  // 후기 리스트를 가져오는 쿼리 (조건부로 호출)
  const {
    status: statusReviewList,
    data: reviewList,
    error: errorReviewList,
  } = useQuery({
    queryKey: ["reviewList", showId],
    queryFn: () => getReviewList(showId as string),
    enabled: !!showId,
  });

  // 후기 삭제를 위한 뮤테이션
  const { mutate: deleteMutate } = useMutation({
    mutationFn: (review: ReviewDeleteParamType) => deleteAdminReview(review),
    onMutate: async (review) => {
      await queryClient.cancelQueries({ queryKey: ["reviewList", showId] });
      const oldData = queryClient.getQueryData<ReviewType[]>(["reviewList", showId])!;
      const newData = oldData.filter((item) => item.id !== review.review_id);
      queryClient.setQueryData(["reviewList", showId], [...newData]);
      return { oldData };
    },
    onError: (_error, _variables, context) => {
      queryClient.setQueryData(["reviewList", showId], [...context!.oldData]);
      toast.error("댓글 삭제 실패");
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ["reviewList", showId] });
      queryClient.invalidateQueries({ queryKey: ["showList"] });
    },
  });



📌 queryClient.invalideQueries

  • 화면을 최신 상태로 유지하는 가장 간단한 방법
    게시글을 작성하거나 삭제할때 목록을 실시간으로 최신화 해야하는 경우, query Key가 변하지 않으므로 강제로 쿼리를 무효화하고 최신화를 진행해야함

  • 즉, query가 오래되었다는 것을 판단하고 다시 refetch를 할 때 사용

  • query가 stale 상태로 변경된다고 바로 데이터를 다시 가져오지 않고 대략 다음과 같은 조건에서 실행

    • query instance가 마운트 될 때 (observer 생성)
      • Observer: 현재페이지에서 해당 데이터를 사용하고 있는 컴포넌트
      • Observer가 1개 이상이거나 1개 이상이 되면(마운트) 데이터를 가져옴
        네트워크 재연결
    • 윈도우 다시 포커싱
    • refetch Interval로 인한 refetch
  • queryKey에 "쿼리키"를 포함하는 모든 쿼리가 무효화

  • queryClient.invalidateQueries({ queryKey: ["쿼리키"] });


📌 queryClient.setQueryData

  • 쿼리의 캐시 된 데이터를 즉시 업데이트하는 데 사용할 수 있는 동기 함수
  • setQueryData 두 번째 인자는 updater 함수, 해당 함수에서 첫 번째 매개변수는 oldData로 기존 데이터
queryClient.setQueryData(["쿼리키"], (oldData: any) => {
  return {
    ...oldData,
    data: [...oldData.data, data.data],
  };
});

📌 queryClient.cancelQueries

  • cancelQueries는 쿼리를 수동으로 취소, 쿼리를 취소하고 이전 상태로 되돌리는 것
    쿼리 취소뿐만아니라 queryFn의 Promise도 취소
  • 예를 들어, 요청을 완료하는 데 시간이 오래 걸리는 경우, 사용자가 취소 버튼을 클릭하여 요청을 중지하는 경우 이용
  • 또는, 아직 HTTP 요청이 끝나지 않았을 때, 페이지를 벗어날 경우에도 중간에 취소해서 불 필요한 네트워크 리소스를 개선

📌 Optimistic Update

  • 서버 업데이트 시 UI에서도 어차피 업데이트할 것이라고(낙관적인) 가정해서 미리 UI를 업데이트 시켜주고 서버를 통해 검증을 받고 업데이트 또는 롤백하는 방식
  • 유저가 행한 액션을 기다릴 필요 없이 바로 업데이트되는 것처럼 보이기 때문에 사용자 경험(UX) 측면에서 좋음
  • 예를 들어, 좋아요 버튼을 유저가 누르면, 일단 client 쪽 state를 먼저 업데이트함 그리고 만약에 실패하면, 예전 state로 돌아가고, 성공하면 필요한 데이터를 다시 fetch해서 서버 데이터와 확실히 연동을 진행하는 것
  • 동작과정
    1. onMutate은 mutation을 시작하기 전에 실행되는 비동기 함수임
      여기서 우선 해당 쿼리를 취소하고, 이전(기존) 데이터를 가져와서 저장함
      그리고 낙관적 업데이트를 수행하여 새로운 데이터를 기존 데이터에 추가 또는 변경함
      마지막으로, 이전 데이터를 가지고 있는 context 객체를 반환함
    2. mutation이 실패하면 onMutate에서 반환된 context를 사용하여 롤백 진행
    3. onSettled는 try catch finally의 finally와 같이 무조건 실행되는 함수
      오류 또는 성공 후에는 항상 refetch로 서버 데이터와 확실하게 연동
useMutation({
  mutationFn: updateTodo,
  // mutate가 호출되면 onMutate를 실행시킴:
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] })

    // 이전 값 저장
    const previousTodo = queryClient.getQueryData(['todos', newTodo.id])

    // 낙관적 업데이트
    queryClient.setQueryData(['todos', newTodo.id], newTodo)

    // context를 리턴하는데 여기에는 이전 스냅샷, 새로운 값을 넣어 리턴
    // 혹은 롤백하는 함수를 여기서 리턴해줘도 됨
    return { previousTodo, newTodo }
  },

  // mutation 실패시 onMutate가 리턴한 context를 사용해 값을 되돌림
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(
      ['todos', context.newTodo.id],
      context.previousTodo,
    )
  },
  // 성공하거나 실패시 쿼리를 무효화해 최신 데이터를 받아와 연동
  onSettled: (newTodo) => {
    queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] })
  },
})
  • onMutate는 mutation 함수가 실행되기 전에 실행되고, mutation 함수가 받을 동일한 변수가 전달됨

참고

https://careerly.co.kr/qnas/1362
https://github.com/ssi02014/react-query-tutorial?tab=readme-ov-file

0개의 댓글