useMutation 를 아세요?

limhi·2024년 1월 29일
0

React Query

목록 보기
4/4
post-thumbnail

useMutation

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);
	},
});

invalidateQueries

위 예시에서 onSettled 에 작성해주었던 queryClient.invalidateQueries 는 QueryClient 객체의 메소드이며, 캐시에 있는 해당 query key 의 쿼리를 강제로 오래된 데이터로 취급하는 무효화 처리를 하여 refetch 시키는 메소드입니다.

활성 쿼리를 다시 가져오는 것(refetch) 을 원하지 않고 단순히 유효하지 않은 것으로 표시하려는 경우,
refetchType: 'none' 옵션을 사용할 수 있습니다. 비활성 쿼리도 다시 가져오려면 refetchType: 'all' 옵션을 사용하도록 합니다.

await queryClient.invalidateQueries({ queryKey: ['todos'], refetchType: 'none' })

optimistic updates

위에서 언급하였던 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 나 불투명도를 다르게 랜더링 함으로써 유용하게 쓰일 수 있을 것 같습니다.

references

TanStack Query - useMutation
TanStack Query - optimistic updates
Jane_Log - 🏵️ React-Query 제대로 사용해보기 (1) useMutation
hyolog - [React Query] 리액트 쿼리 useMutation 실용 편(custom hook 으로 사용해보자)

profile
null 사랑하지 않아 - 어반자카파

0개의 댓글