React Query 공부 (11) mutation , invalidateQueries, Optimistic update

JunSeok·2023년 3월 1일
1

react-query

목록 보기
11/11
post-thumbnail

Mutation(변이)

React-Query에서 mutation의 역할은 서버에 있는 데이터를 업데이트하기 위해 서버로 네트워크 콜을 보내는 것이다.
Create, Update, Delete할 때 쓰인다.
우리는 mutaion을 활용하기 위해 useMutation hook을 사용할 것이다.

CRUD의 사용방법은 다음과 같다.
Read: useQuery
Create, Update, Delete: useMutation

useMutation

useMutation은 몇 가지 빼고는 useQuery와 비슷하다.

  • 실제 네트워크 콜 할 때, 사용할 mutate function을 리턴
    - 우리는 useMutation 리턴 오브젝트 값의 mutate function property를 이용하여 mutation call을 전달할 것이다.

  • cache에 데이터를 저장하는 것이 아니기 떄문에 query key는 불필요하다.

  • cache data가 없기 때문에 isFetching은 없고 isLoading만 있다.

  • cache data가 없기 때문에 refetch하지 않는다.
    isFetching과 isLoading의 차이를 알고 싶다면 여기로

  • default값으로 retry를 하지 않는다. (물론 따로 설정 가능)
    useQuery는 default로 3번 retry한다.

  • onMutate callback함수가 있다. 이는 후에 optimistic update할 때 사용한다.

mutaion 공식문서
useMutation 공식문서

flow

  1. useMutation을 통해 값이 변했다고 서버에 네트워크 콜을 보낸다.
  2. 리턴받은 오브젝트에서 mutate function을 이용하여 변이된 쿼리를 무효화하고 캐시의 데이터를 삭제한다.
  3. 서버는 업데이트된 최신 값을 리턴해주고, 클라이언트는 그 값으로 캐시 데이터를 업데이트한다.

구현

global fetching indicator와 error handling

mutation을 사용하기 전 global fetching indicator와 error handling을 설정해준다.

  • Loading indicator
    - useQuery 때는 useIsFetcing을 사용했는데, 이와 비슷한 useIsMutating을 사용한다.
    미해결된 mutation call의 수(integer 값)를 리턴해주는데, 이를 활용하여 값이 1 이상일 때는 로딩창을 띄워주면 된다.
    useIsFetching 공식문서
  • error handling
    - error handling은 query client에서 설정해준다.
    default option이 아닌 mutation cache에 설정해주는 이유는 캐시에 handler를 설정해놓으면 모든 retry가 실패한 뒤에 트리거되기 때문이다.
	export const queryClient = new QueryClient({
      mutationCache: new MutationCache({
        onError: queryErrorHandler
      })
    })	

useMutation 사용

  • useMutation을 통해 mutate function을 리턴받는다.
  • useMutation은 cache data가 없기 떄문에 query key가 필요없다.
    mutation function만 있으면 된다.
const { mutate } = useMutation((updateData) => mutateFn(updateData))

// updateData를 mutate함수의 인자에 넣어주면 mutateFn이 인자를 받아 서버로 mutate 콜을 보낸다.
mutate(updateData)
  • 서버의 데이터가 업데이트 되면 해당 데이터 query를 무효화(invalidate)함으로써 이 데이터는 변이된 데이터임을 알린다.
    이를 위해 QueryClient의 invalidateQueries method를 사용한다.

invalidateQueries

QueryClient에는 invalidateQueries라는 method가 있다. 우리는 변이된 cache data를 무효화하기 위해 이를 사용할 것이다.
Query Invalidation 공식문서

  • 역할
    - query에 stale 마크를 남긴다. => 이 데이터는 상했어! 라고 말하는 것

  • 이를 사용하면 유저는 page를 refresh할 필요가 없다!
    - 무효화하면 react-query 내에서 이 데이터는 변이된 데이터임을 알아차리고 refetch를 하기 때문이다!

사용코드

const queryClient = useQueryClient()
const { mutate } = useMutation((updateData) => mutateFn(updateData), {
	onSuccess: () => {
    	queryClient.invalidateQueries('query key')
    }
})

flow 정리

  • 내가 mutate를 요청
  • useMutation의 onSuccess callback에서 관련 query를 QueryClient의 invalidateQueries를 통해 invalidate시킨다.
  • 데이터가 stale되면 react-query가 내부적으로 이를 알아차리고 데이터이 refetch를 유발한다.
  • 이로 인하여 유저가 페이지를 refresh하지 않아도 유저의 데이터가 내부적으로 업데이트된다.

[Typescript]useMutation return type

useMutation return type으로 UseMutationFunction을 사용한다.
인자 값은 총 4개이다.

  • TData
    mutate function에서 반환하는 data type
  • TError
    에러날 때
  • TVariables
    변수가 있을 때, 변수 타입
  • TContext
    optimistic update할 때 onMutate가 설정하는 type

반환하는 데이터가 없다면 void로 설정

사용예시

export function useReserveAppointment(): UseMutateFunction<void, unknown, Appointment, unknown> {
  const { user } = useUser();
  const toast = useCustomToast();
  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"
      })
    }
  })
  return mutate
}

Optimistic update

optimistic update는 말 그대로 낙관적 업데이트를 의미한다.
바뀌는 값을 안다면, 서버에서 잘 작동할 것이라 가정하고 클라이언트 내에서 해당 query key의 cache 데이터를 업데이트한다.

  • 장점
    - 바뀐 값을 서버에서 다시 받아오는 것이 아니기 때문에 cacha 값 업데이트 속도가 매우 빠르다.
    • 특히 해당 데이터를 사용하는 컴포넌트들이 많을수록 효율적이다.
  • 단점
    - 만약 작업이 실패핬을 경우를 대비해야 하기 때문에 코드가 복잡해진다.
    • 실패할 경우 이전 값으로 다시 되돌려야 하기 때문에 이전 값을 미리 저장해놔야 한다.

flow

  • 유저가 mutate를 이용하여 update를 트리거한다.
  • 서버에 Mutate call을 보낸다.
  • onMutate callback
    - 중간에 발생할 수도 있는 refetch를 막기 위해 cancelQueies 이용
    - query cache 업데이트
    - 실패할 시 되돌릴 이전 값 저장
  • onSuccess
    - 성공했음을 client에 알린다.
  • onError
    - 이전 값으로 복구하기 위해 onMutate로부터 context value로부터 저장해둔 이전 값을 추출하여 사용한다.
  • onSettle
    - 성공하든 실패하든 최신 값을 유지하기 위해 query를 invalidate해준다.

구현 코드

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

  const { mutate } = useMutation((newUserData: User) => patchUserOnServer(newUserData, user), {
    // onMutate returns context that is passed to onError
    onMutate: async (newData: User | null) => {
      // 중간에 이전값이 덮어씌워지는 것을 막아준다.
      queryClient.cancelQueries(queryKeys.user)
      // 실패할 것을 대비하여 이전 값 저장
      const previousUserData:User = queryClient.getQueryData(queryKeys.user)
      
      // 업데이트 값을 cache data에 저장
      queryClient.setQueryData(queryKeys.user, newData)
      
      // 저장해둔 이전 값을 리턴 => context value를 통해 onError로 전달한다.
      return { previousUserData }
    },
    onError: (error, newData, context) => {
      // 저장해둔 값을 context에서 추출한다.
      if(context.previousUserData) {
        queryClient.setQueryData(queryKeys.user, context.previousUserData)
        toast({
          title: "Update failed; restoring previous values",
          status: "warning"
        })
      }
    },
    onSuccess: (userData: User | null) => {
      // 성공시 toast 날린다.
      if(userData) {
        toast({
          title: "User updated!",
          status: "success"
        })
      }
    },
    onSettled: () => {
      // 성공했든 실패했든 cache 데이터를 최신값으로 유지하기 위해 무조건 invalidate해준다.
      queryClient.invalidateQueries(queryKeys.user)
    }
  })
  return mutate;
}

react-query 공부 끝

공식문서와 udemy의 react-query 강의를 통해 시작한 react-query 공부가 끝이 났다.
마지막에 testing 관련 강의가 있었지만 아직 이해를 하지 못해서 velog에는 쓰지 않기로 했다.

정말 유용한 기능들이 많았다. 이를 잘 활용하기 위해 꾸준히 복습하고 내 프로젝트에 적용시켜봐야겠다.

profile
최선을 다한다는 것은 할 수 있는 한 가장 핵심을 향한다는 것

0개의 댓글