본문으로 건너뛰기

낙관적 업데이트

React Query는 뮤테이션이 완료되기 전에 UI를 낙관적으로 업데이트하는 두 가지 방법을 제공합니다. onMutate 옵션을 사용하여 캐시를 직접 업데이트하거나, 반환된 variables를 활용하여 useMutation 결과로 UI를 업데이트할 수 있습니다.

UI를 통해

이는 캐시와 직접 상호작용하지 않으므로 더 간단한 변형입니다.

const addTodoMutation = useMutation({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
// make sure to _return_ the Promise from the query invalidation
// so that the mutation stays in `pending` state until the refetch is finished
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})

const { isPending, submittedAt, variables, mutate, isError } = addTodoMutation

그러면 추가된 todo를 포함하는 addTodoMutation.variables에 접근할 수 있습니다. 쿼리가 렌더링되는 UI 목록에서 뮤테이션이 isPending 동안 목록에 다른 항목을 추가할 수 있습니다:

<ul>
{todoQuery.items.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
{isPending && <li style={{ opacity: 0.5 }}>{variables}</li>}
</ul>

뮤테이션이 대기 중인 동안에는 다른 opacity를 가진 임시 항목을 렌더링합니다. 완료되면 해당 항목은 자동으로 더 이상 렌더링되지 않습니다. 다시 가져오기에 성공했으므로 목록에서 해당 항목이 "일반 항목"으로 표시되어야 합니다.

뮤테이션에서 오류가 발생하면 항목도 사라집니다. 하지만 원한다면 뮤테이션의 isError 상태를 확인하여 계속 표시할 수 있습니다. 뮤테이션에서 오류가 발생해도 variables지워지지 않으므로 계속 접근할 수 있으며, 재시도 버튼을 표시할 수도 있습니다:

{
isError && (
<li style={{ color: 'red' }}>
{variables}
<button onClick={() => mutate(variables)}>Retry</button>
</li>
)
}

뮤테이션과 쿼리가 동일한 컴포넌트에 있지 않은 경우

이 접근 방식은 뮤테이션과 쿼리가 같은 컴포넌트에 있을 때 매우 효과적입니다. 하지만 전용 useMutationState 훅을 통해 다른 컴포넌트의 모든 뮤테이션에도 접근할 수 있습니다. mutationKey와 함께 사용하는 것이 가장 좋습니다:

// somewhere in your app
const { mutate } = useMutation({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
mutationKey: ['addTodo'],
})

// access variables somewhere else
const variables = useMutationState<string>({
filters: { mutationKey: ['addTodo'], status: 'pending' },
select: (mutation) => mutation.state.variables,
})

동시에 여러 뮤테이션이 실행 중일 수 있으므로 variablesArray입니다. 항목에 고유한 키가 필요하다면 mutation.state.submittedAt을 선택할 수도 있습니다. 이렇게 하면 동시 낙관적 업데이트도 아주 쉽게 표시할 수 있습니다.

캐시를 통해

뮤테이션을 수행하기 전에 상태를 낙관적으로 업데이트하면 뮤테이션이 실패할 가능성이 있습니다. 이러한 실패 사례 대부분에서는 낙관적 쿼리의 다시 가져오기를 트리거하여 실제 서버 상태로 되돌릴 수 있습니다. 하지만 일부 상황에서는 다시 가져오기가 올바르게 작동하지 않을 수 있으며, 뮤테이션 오류가 다시 가져오기를 불가능하게 만드는 일종의 서버 문제를 나타낼 수도 있습니다. 이 경우에는 업데이트를 롤백하도록 선택할 수 있습니다.

이를 위해 useMutationonMutate 핸들러 옵션을 사용하면 나중에 onErroronSettled 핸들러 모두에 마지막 인수로 전달될 값을 반환할 수 있습니다. 대부분의 경우 롤백 함수를 전달하는 것이 가장 유용합니다.

새 할 일을 추가할 때 할 일 목록 업데이트하기

const queryClient = useQueryClient()

useMutation({
mutationFn: updateTodo,
// When mutate is called:
onMutate: async (newTodo, context) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await context.client.cancelQueries({ queryKey: ['todos'] })

// Snapshot the previous value
const previousTodos = context.client.getQueryData(['todos'])

// Optimistically update to the new value
context.client.setQueryData(['todos'], (old) => [...old, newTodo])

// Return a result with the snapshotted value
return { previousTodos }
},
// If the mutation fails,
// use the result returned from onMutate to roll back
onError: (err, newTodo, onMutateResult, context) => {
context.client.setQueryData(['todos'], onMutateResult.previousTodos)
},
// Always refetch after error or success:
onSettled: (data, error, variables, onMutateResult, context) =>
context.client.invalidateQueries({ queryKey: ['todos'] }),
})

단일 할 일 업데이트하기

useMutation({
mutationFn: updateTodo,
// When mutate is called:
onMutate: async (newTodo, context) => {
// Cancel any outgoing refetches
// (so they don't overwrite our optimistic update)
await context.client.cancelQueries({ queryKey: ['todos', newTodo.id] })

// Snapshot the previous value
const previousTodo = context.client.getQueryData(['todos', newTodo.id])

// Optimistically update to the new value
context.client.setQueryData(['todos', newTodo.id], newTodo)

// Return a result with the previous and new todo
return { previousTodo, newTodo }
},
// If the mutation fails, use the result we returned above
onError: (err, newTodo, onMutateResult, context) => {
context.client.setQueryData(
['todos', onMutateResult.newTodo.id],
onMutateResult.previousTodo,
)
},
// Always refetch after error or success:
onSettled: (newTodo, error, variables, onMutateResult, context) =>
context.client.invalidateQueries({ queryKey: ['todos', newTodo.id] }),
})

원한다면 별도의 onErroronSuccess 핸들러 대신 onSettled 함수를 사용할 수도 있습니다:

useMutation({
mutationFn: updateTodo,
// ...
onSettled: async (newTodo, error, variables, onMutateResult, context) => {
if (error) {
// do something
}
},
})

무엇을 언제 사용할지

낙관적 결과를 표시할 곳이 하나뿐이라면 variables를 사용하고 UI를 직접 업데이트하는 접근 방식이 코드가 더 적게 필요하며 일반적으로 이해하기도 더 쉽습니다. 예를 들어 롤백을 전혀 처리할 필요가 없습니다.

하지만 화면의 여러 위치에서 업데이트를 알아야 하는 경우 캐시를 직접 조작하면 이 작업이 자동으로 처리됩니다.

추가 자료

TkDodo가 작성한 동시 낙관적 업데이트 가이드를 살펴보세요.