🔥 낙관적 업데이트
강의 목차
React Query는 뮤테이션이 완료되기 전에 UI를 낙관적으로 업데이트하는 두 가지 방법을 제공합니다. onMutate
옵션을 사용하여 캐시를 직접 업데이트하거나, useMutation
결과에서 반환된 변수를 활용하여 UI를 업데이트할 수 있습니다.
UI를 통한 방법
이 방법은 캐시와 직접 상호작용하지 않기 때문에 더 간단합니다.
const addTodoMutation = useMutation({ mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }), // 쿼리 무효화에서 Promise를 반드시 반환해야 합니다. // 이렇게 하면 리패치가 완료될 때까지 뮤테이션이 '대기' 상태를 유지합니다. onSettled: async () => { return await queryClient.invalidateQueries({ queryKey: ['todos'] }) }, }) const { isPending, submittedAt, variables, mutate, isError } = addTodoMutation
typescript
이제 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>
typescript
뮤테이션이 대기 중인 동안 다른 투명도로 임시 항목을 렌더링합니다. 완료되면 이 항목은 자동으로 더 이상 렌더링되지 않습니다. 리패치가 성공했다면 목록에서 "일반 항목"으로 이 항목을 볼 수 있을 것입니다.
뮤테이션이 실패하면 항목도 사라집니다. 하지만 원한다면 뮤테이션의 isError
상태를 확인하여 계속 표시할 수 있습니다. 뮤테이션이 실패해도 variables
는 지워지지 않으므로 여전히 접근할 수 있으며, 심지어 재시도 버튼을 표시할 수도 있습니다:
{ isError && ( <li style={{ color: 'red' }}> {variables} <button onClick={() => mutate(variables)}>재시도</button> </li> ) }
typescript
뮤테이션과 쿼리가 같은 컴포넌트에 없는 경우
이 방법은 뮤테이션과 쿼리가 같은 컴포넌트에 있을 때 잘 작동합니다. 하지만 전용 useMutationState
훅을 통해 다른 컴포넌트에서도 모든 뮤테이션에 접근할 수 있습니다. 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, })
typescript
variables
는 배열이 될 것입니다. 동시에 여러 뮤테이션이 실행될 수 있기 때문입니다. 항목에 고유한 키가 필요하다면 mutation.state.submittedAt
도 선택할 수 있습니다. 이렇게 하면 동시에 발생하는 낙관적 업데이트도 쉽게 표시할 수 있습니다.
캐시를 통한 방법
뮤테이션을 수행하기 전에 상태를 낙관적으로 업데이트할 때, 뮤테이션이 실패할 가능성이 있습니다. 대부분의 실패 사례에서는 낙관적 쿼리를 리패치하여 실제 서버 상태로 되돌릴 수 있습니다. 하지만 일부 상황에서는 리패치가 제대로 작동하지 않을 수 있으며, 뮤테이션 오류가 리패치를 불가능하게 만드는 서버 문제를 나타낼 수 있습니다. 이런 경우에는 업데이트를 롤백하는 방법을 선택할 수 있습니다.
이를 위해 useMutation
의 onMutate
핸들러 옵션을 사용하면 나중에 onError
와 onSettled
핸들러에 마지막 인자로 전달될 값을 반환할 수 있습니다. 대부분의 경우 롤백 함수를 전달하는 것이 가장 유용합니다.
새로운 할 일을 추가할 때 할 일 목록 업데이트하기
const queryClient = useQueryClient() useMutation({ mutationFn: updateTodo, // mutate가 호출될 때: onMutate: async (newTodo) => { // 진행 중인 리패치를 취소합니다 // (낙관적 업데이트를 덮어쓰지 않도록) await queryClient.cancelQueries({ queryKey: ['todos'] }) // 이전 값의 스냅샷을 만듭니다 const previousTodos = queryClient.getQueryData(['todos']) // 새로운 값으로 낙관적으로 업데이트합니다 queryClient.setQueryData(['todos'], (old) => [...old, newTodo]) // 스냅샷된 값을 포함한 컨텍스트 객체를 반환합니다 return { previousTodos } }, // 뮤테이션이 실패하면, // onMutate에서 반환된 컨텍스트를 사용하여 롤백합니다 onError: (err, newTodo, context) => { queryClient.setQueryData(['todos'], context.previousTodos) }, // 오류나 성공 후 항상 리패치합니다: onSettled: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) }, })
typescript
단일 할 일 업데이트하기
useMutation({ mutationFn: updateTodo, // mutate가 호출될 때: onMutate: async (newTodo) => { // 진행 중인 리패치를 취소합니다 // (낙관적 업데이트를 덮어쓰지 않도록) await queryClient.cancelQueries({ queryKey: ['todos', newTodo.id] }) // 이전 값의 스냅샷을 만듭니다 const previousTodo = queryClient.getQueryData(['todos', newTodo.id]) // 새로운 값으로 낙관적으로 업데이트합니다 queryClient.setQueryData(['todos', newTodo.id], newTodo) // 이전 할 일과 새로운 할 일을 포함한 컨텍스트를 반환합니다 return { previousTodo, newTodo } }, // 뮤테이션이 실패하면, 위에서 반환한 컨텍스트를 사용합니다 onError: (err, newTodo, context) => { queryClient.setQueryData( ['todos', context.newTodo.id], context.previousTodo, ) }, // 오류나 성공 후 항상 리패치합니다: onSettled: (newTodo) => { queryClient.invalidateQueries({ queryKey: ['todos', newTodo.id] }) }, })
typescript
원한다면 별도의 onError
와 onSuccess
핸들러 대신 onSettled
함수를 사용할 수도 있습니다:
useMutation({ mutationFn: updateTodo, // ... onSettled: (newTodo, error, variables, context) => { if (error) { // 무언가를 수행합니다 } }, })
typescript
언제 어떤 방법을 사용해야 할까
낙관적 결과를 보여줄 곳이 한 군데뿐이라면, variables
를 사용하고 UI를 직접 업데이트하는 방법이 더 적은 코드를 필요로 하며 일반적으로 이해하기 쉽습니다. 예를 들어, 롤백을 전혀 처리할 필요가 없습니다.
하지만 화면의 여러 곳에서 업데이트를 알아야 한다면, 캐시를 직접 조작하는 방법이 이를 자동으로 처리해 줄 것입니다.