mutation이란 ?
일반적으로 mutation은 부수 효과(side effect)가 있는 함수를 말한다. 예를 들어, 배열의 push 메서드는 원본 배열을 변경하는 부수효과가 있는 함수이다. 같은 맥락에서 React Query의 useMutation은 서버에 부수 효과(side effect)를 일으킨다. 데이터 추가, 삭제 또는 유저의 로그인 모두 이 경우에 해당한다.
useQuery와 useMutation
useQuery와 useMutation을 처음 보면 어떤 상황에서 사용해야 하나 헷갈릴 수 있다. useQuery는 GET 요청과, useMutation은 POST, PATCH, DELETE 요청과 유사하다고 생각하면 된다.
- 언제 사용하면 될까?
- useQuery는 고유한 쿼리 키를 기반으로 서버에서 fetch한 데이터를 관리한다. 즉, 처음에 데이터를 가져와서 렌더링할 때 useQuery로 하면 된다.
- 대신 이 데이터를 수정하고 서버에 해당 내용을 반영해야 하는 경우는 useMutation을 사용한다. TodoList를 만들고 있다면, todo를 추가, 삭제, 또는 수정해야 하는 경우에 해당한다.
- 차이점
- useQuery를 통해 관리하는 쿼리는 서버 데이터와 클라이언트의 데이터가 sync 되도록 자동으로 업데이트 해준다. 반면, useMutation으로 관리하는 mutation은 서버 데이터 변경 시 자동으로 화면이 업데이트되지 않는다.
- useQuery는 상태를 공유하기 때문에 여러 번 호출해도 동일한 결과를 반환한다. 하지만 useMutation의 경우 매번 서버의 상태를 새롭게 변경한다.
useMutation 사용 시 쿼리 업데이트하기
이 부분이 사실 가장 중요하고, 어려웠던 것 같다. mutation을 실행하고 난 후 화면이 변경한 서버 데이터와 동일하게 업데이트된다고 생각했지만 예상과 달랐다. TKdodo 블로그를 보고 useQuery로 가져온 데이터(쿼리)를 수정했을 때 쿼리에 이를 반영하는 방법을 알 수 있었다. 쿼리는 화면에 렌더링하고 있는 데이터이므로 화면을 업데이트하는 방법이라고 생각할 수 있다.
결론적으로 mutation을 실행하고 난 후 화면을 변경한 서버 데이터와 동일하게 업데이트하려면 1. invalidateQueries를 통한 캐시 무효화 또는 2. setQueryData를 통한 캐시 업데이트를 하는 방법이 있다.
1. 캐시 무효화(invalidateQueries) : 서버 데이터를 mutation으로 업데이트할 때, invalidateQueries 를 통해 React Query에게 캐싱된 쿼리 데이터가 무효하다(더 이상 사용할 수 없다)고 알려줄 수 있다. 그러면 React Query는 해당 데이터를 refetch하고, fetch가 끝나면 화면이 자동으로 업데이트된다.
// 캐시되어 있는 모든 쿼리 무효화
queryClient.invalidateQueries()
// 'todos'로 시작하는 쿼리 키를 가진 모든 쿼리 무효화
queryClient.invalidateQueries({ queryKey: ['todos'] })
2. 캐시 업데이트(setQueryData) : 만약 데이터를 refetch 하고 싶지 않다면 setQueryData를 사용해 쿼리 캐시를 바로 업데이트할 수 있다. mutation이 필요한 정보를 모두 반환하는 경우가 이에 해당한다. setQueryData를 통해 캐시를 업데이트하면 서버에서 데이터를 받은 것처럼 해당 쿼리를 사용하는 모든 컴포넌트가 리렌더링된다.
정리하자면 invalidateQueries는 서버 데이터를 업데이트하고, 업데이트된 데이터를 refetch한다. 그리고 이를 기반으로 쿼리 캐시를 업데이트하고, 컴포넌트를 리렌더링한다. 반면, setQueryData는 서버 데이터를 업데이트하고, 해당 로직으로 쿼리 캐시를 업데이트한다. 그리고 쿼리 캐시를 기반으로 컴포넌트를 리렌더링한다. 이 경우, 프론트엔드에 서버를 업데이트하는 로직이 중복되고 코드가 길어질 수 있어 invalidateQueries를 통해 전체를 새로 받아오는 것이 더 안전하고 선호되는 방식이다.
Optimistic Update(낙관적 업데이트)
낙관적 업데이트는 서버에 데이터를 전송하기 전에 mutation에 성공했다고 가정하고 UI를 업데이트하는 것이다. 서버로부터 성공 응답을 받으면 해당 실제 데이터를 기반으로 쿼리를 invalidate하여 컴포넌트를 리렌더링한다. 만약 실패한 경우, mutation 이전의 상태를 기억해뒀다가 롤백한다.
낙관적 업데이트는 유저의 인터랙션에 대한 즉각적 반응이 필요할 때 유용하다. 예를 들어, 토글 버튼을 클릭했을 때는 바로 반응을 보여주는 것이 좋은데, 그렇지 않으면 유저가 기다리지 못하고 여러 번 클릭해버리는 상황이 발생할 수 있기 때문이다.
Tanstack Query 공식문서에 아래와 같이 자세하게 낙관적 업데이트를 하는 방법을 보여주고 있다.
const queryClient = useQueryClient()
useMutation({
// Promise를 반환하는 비동기 함수
mutationFn: updateTodo,
// mutate가 호출될 때 사용됨
onMutate: async (newTodo) => {
// 낙관적 업데이트를 덮어쓰지 못하도록 모든 refetch 취소
await queryClient.cancelQueries({ queryKey: ['todos'] })
// 이전 상태 기억
const previousTodos = queryClient.getQueryData(['todos'])
// 새로운 상태로 낙관적 업데이트(화면 먼저 업데이트
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
// 기억해 둔 상태 context 객체로 반환
return { previousTodos }
},
// mutation이 시래할 경우 이전 상태로 롤백
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
// 성공, 실패하는 경우 모두 항상 쿼리 무효화하여 refetch
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
이제 실제로 프로젝트에 이를 어떻게 적용하였는지에 대해 알아보자!! 또 useMutation을 다른 여러 쿼리에 적용할 수 있도록 커스텀 훅으로 만들고, 타입스크립트도 적용해보자.
REF
https://tanstack.com/query/v4/docs/react/guides/query-invalidation
'FrontEnd > React' 카테고리의 다른 글
TanStack Query를 활용하여 Route 기반 Prefetching하기(feat.Render-as-you-fetch) (25) | 2024.02.27 |
---|---|
[React Query] React Query에서 ErrorBoundary로 에러 핸들링하기 (3) | 2023.12.10 |
[React-Query] React Query로 페이지네이션하기(keepPreviousData 옵션) (0) | 2023.07.04 |
[React-Query] 쿼리 키 정복하기 (3) | 2023.07.03 |
[React-Query] useMutation 활용편(낙관적 업데이트, custom hook으로 활용하기) (0) | 2023.07.01 |