useQuery 와는 다르게 데이터를 생성(C) | 수정(U) | 삭제(D)할 때 사용됩니다.
읽기(R) 와 차이점을 두는 이유는 C, U, D 가 실행되는 쿼리의 cache 는 필요하지 않기 때문입니다.
따라서 캐싱하는데 필요한 query key 는 불필요하며, isFetching 은 없고 isLoading 만 있습니다.
refetch 또한 없으며 onMutate callback 함수가 있어 optimistic update 할 때 사용됩니다. optimistic update 란 useMutation 을 활용하면서 사용자 경험을 높이는 구현 방법이기 때문에 후에 다루도록 하겠습니다.
이를 호출하는데 필요한 인자는 다음과 같습니다.
mutationFn
필수이지만 default mutation 함수가 정의되지 않은 경우에만 해당합니다. 비동기 작업을 수행하고 반환하는 함수이며 변순는 mutate 가 mutationFn 에 전달할 객체입니다.
그리고 아래의 옵션을 활용하여 더욱 효과적인 활용을 할 수 있습니다.
onMutate
mutation 함수가 실행되기 전에 실행되며, mutation 함수가 받을 동일한 변수가 전달됩니다.
mutation 함수가 성공하기를 바라며 리소스에 대한 optimistic update 를 수행하는데 유용합니다. 이 함수에서 반환된 값은 mutation 실패 시 onError 및 onSettled 함수 모두에 전달되며 optimistic update 를 롤백하는데 유용할 수 있습니다.
onSuccess
,onError
,onSettled
mutation 이 성공했을 때와 실패했을 때 실행되는 함수들 입니다. onSettled 의 경우, mutation 이 성공적으로 가져오거나 오류가 발생하여 데이터 또는 오류가 전달될 때 실행됩니다. 즉, 성공과 실패 두 경우 모두 결과가 전달됩니다.
useMutation 을 호출할 시, 반환되는 값은 다음과 같습니다.
mutate
mutation 을 트리거하기 위해 변수(mutationFn 에 전달하는 객체)를 사용하여 호출할 수 있는 mutation 함수와 선택적으로 추가 콜백 옵션을 연결할 수 있습니다.
onSuccess
,onError
,onSettled
위 option 의 내용과 같습니다.
다만, 두 곳에서 추가 콜백을 실행할 경우, useMutation 의 추가 콜백 → mutate 의 추가 콜백 순서로 실행됩니다. 컴포넌트가 unmount 되면 추가 콜백이 실행되지 않습니다.
mutateAsync
mutate 와 유사하지만 Promise 형태를 반환합니다.
useMutation 의 예제는 다음과 같습니다.
// #1
const { mutate, isLoading, isError, error, isSuccess } = useMutation({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
});
// in component
return (
<input type='text' value={text} />
{isError && <p>error: {error.message}</p>}
{isSuccess && <p>Success!</p>}
<Button onClick={() => mutate(text)}>
{!isLoading ? Enter : <Spinner />}
</Button>
)
// #2
const const { mutate } = useMutation({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
onSuccess: (data) => { // 성공 시 response 를 변수로 받을 수 있다.
queryClient.invalidateQueries({ queryKey: ['todos'] }),
console.log(data);
},
onError: (error) => { // 실패 시 error 를 변수로 받을 수 있다.
console.log(error);
},
});
위 예시에서 onSettled
에 작성해주었던 queryClient.invalidateQueries
는 QueryClient 객체의 메소드이며, 캐시에 있는 해당 query key 의 쿼리를 강제로 오래된 데이터로 취급하는 무효화 처리를 하여 refetch 시키는 메소드입니다.
활성 쿼리를 다시 가져오는 것(refetch) 을 원하지 않고 단순히 유효하지 않은 것으로 표시하려는 경우,
refetchType: 'none'
옵션을 사용할 수 있습니다. 비활성 쿼리도 다시 가져오려면 refetchType: 'all'
옵션을 사용하도록 합니다.
await queryClient.invalidateQueries({ queryKey: ['todos'], refetchType: 'none' })
위에서 언급하였던 optimistic updates 는 mutation 이 완료되기 전에 성공할 것이라고 가정하여 UI 를 낙관적으로 업데이트 하는 것을 말합니다.
onMutate
옵션을 사용하여 캐시를 직접 업데이트 하거나, 반환된 변수를 활용하여 useMutation 결과에서 업데이트 할 수 있습니다.
// # onMutate 옵션을 사용하여 업데이트
useMutation({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
onMutate: async (newTodo) => {
// 나가는 refetch 를 취소하여 optimistic update 를 덮어쓰지 않기 위함
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previousTodos = queryClient.getQueryData(['todos'])
queryclient.setQueryData(['todos'], (old) => [...old, newTodo])
return { previousTodos }
},
// onMutate 에서 반환된 context 를 사용하여 롤백
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
// # useMutation 결과를 받아 업데이트
const { variables, mutate, isPending, isError } = useMutation({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
});
return (
<ul>
{todoQuery.items.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
{isPending && <li style={{ opacity: 0.5 }}>{variables}</li>
</ul>
)
위와 같이 mutation 이 pending 되는 동안 다른 불투명도로 임시 항목을 랜더링 할 수 있습니다.
완료되면 항목이 더 이상 자동으로 랜더링 되지 않으며 refetch 가 성공했다면 해당 항목이 목록에 ‘일반 항목’ 으로 표시되어야 합니다.
원한다면 isError
상태를 확인하여 해당 항목이 실패하였음 을 나타낼 수도 있습니다.
{
isError && (
<li style={{ color: 'red' }}>
{variables}
<button onClick={() => mutate(variables)}>Retry</button>
</li>
)
}
이 접근 방식은 mutation 과 query 가 동일한 구성요소에 있는 경우 매우 효과적입니다. 그러나 전용 useMutationState
hook 을 통해 다른 구성요소의 모든 mutation 에 접근할 수도 있습니다.
이는 mutationKey
를 활용하는 것이 좋은데, 아래와 같이 작성할 수 있습니다.
const { mutate } = useMutation({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
mutationKey: ['addTodo']
})
const variables = useMutationState<String>({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables,
})
return (
<ul>
{todoQuery.items.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
{isPending && <li style={{ opacity: 0.5 }}>{variables}</li>
</ul>
)
react query 공식문서에서는 언제 무엇을 사용해야 하는 지에 대해 다음과 같이 설명합니다.
낙관적인 결과를 표시해야 하는 위치가 하나만 있는 경우 변수를 사용하고 UI 를 직접 업데이트 하는 것이 코드가 덜 필요하고 일반적으로 추론하기 더 쉬운 접근 방식입니다. 예를 들어, 롤백을 전혀 처리할 필요가 없습니다.
그러나 화면에 업데이트에 대해 알아야 할 여러 위치가 있는 경우 캐시를 직접 조작하면 이 작업이 자동으로 처리됩니다.
optimistic updates 를 이용하면 api 요청 성공 유무를 기다리지 않고 사용자가 UI 단에서 변화를 확인 할 수 있기 때문에 사용자 경험을 높일 수 있을 것으로 기대됩니다.
예를 들어, 비즈니스 로직이 방대한 경우 isPending 을 활용하여 spinner 나 불투명도를 다르게 랜더링 함으로써 유용하게 쓰일 수 있을 것 같습니다.
TanStack Query - useMutation
TanStack Query - optimistic updates
Jane_Log - 🏵️ React-Query 제대로 사용해보기 (1) useMutation
hyolog - [React Query] 리액트 쿼리 useMutation 실용 편(custom hook 으로 사용해보자)