사용자가 데이터를 수정할 때, 서버와의 통신 결과를 기다리지 않고 즉시 사용자 인터페이스(UI)를 업데이트하는 기법이다. 이 방식은 사용자 경험(UX)을 향상시키고, 애플리케이션의 반응성을 높이는 데 도움을 준다.



소셜 미디어: 댓글 추가, 좋아요 클릭 등에서 즉시 UI를 반영하여, 사용자 경험을 향상시킨다.
할 일 관리 애플리케이션: 사용자가 할 일을 추가하거나 삭제할 때, 즉시 목록에 반영한다.
전자상거래: 장바구니에 상품을 추가할 때, 사용자에게 즉시 업데이트된 장바구니 상태를 보여준다
Tanstack Query(React Query)는 뮤테이션이 완료되기 전에 UI를 낙관적으로 업데이트하는 두 가지 방법을 제공한다.
const addTodoMutation = useMutation({
mutationFn: (newTodo: string) => axios.post('/api/data', { text: newTodo }),
onSettled: async () => { // promise가 완료될 때까지 Pending 상태
return await queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
const { isPending, submittedAt, variables, mutate, isError } = addTodoMutation
<ul>
{todoQuery.items.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
{isPending && <li style={{ opacity: 0.5 }}>{variables}</li>}
</ul>
onSettled를 async 함수로 선언하여 Promise가 완료될 때까지 pending 상태를 유지하고 UI는 사용자에게 처리 중임을 알린다.
pending 상태일 때는 추가될 할 일 목록을 투명도 0.5로 하여 렌더링한다.
pending 상태가 끝났다는건 쿼리 무효화가 완료됨을 의미하니깐 서버와 동기화가 되어 추가한 할 일이 목록에 추가된다.
만약 에러가 발생해도 할 일 목록에 추가 되지 않아서 문제가 되지 않는다!
💡
submittedAt,variables이란?
submittedAt: 뮤테이션 요청이 제출된 시간(타임스탬프)를 나타내는 속성이다.
variables: 뮤테이션 요청을 수행할 때 전달된 인자(변수)를 나타내는 속성이다.
const queryClient = useQueryClient()
useMutation({
mutationFn: updateTodo,
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previousTodos = queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
return { previousTodos }
},
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
현재 진행 중인 쿼리 요청 취소하기
현재 쿼리 캐시에서 todos 데이터 가져와서 previousTodos 변수에 저장(에러 발생 시 롤백할 때 사용)
쿼리 캐시를 낙관적 업데이트하여 새로운 할 일 추가하기
onMutate에서 반환된 객체는 onError에서 사용할 수 있는 컨텍스트를 제공한다.
만약 에러가 발생한다면 반환된 컨텍스트(previousTodos)를 사용하여 롤백
mutate가 끝나면 쿼리 키를 무효화여 서버 동기화
나는 이 코드만 봤을 때는 쿼리 요청을 취소해야 할 이유가 없다고 생각해서 찾아보니깐 지금처럼 요청이 하나인 경우가 아니라 여러 개의 요청이 발생했을 경우에 이미 낙관적 업데이트가 된 UI가 덮어쓰일 수 있기 때문에 쿼리 요청을 취소하는 거였다.
예를 들어 할 일 목록에서 할 일3 을 완료로 업데이트하고 할 일 4를 추가하는 상황을 생각해보면
1. 할 일 3을 업데이트한다. ➔ 할 일 3을 완료로 변경하는 요청을 서버로 보낸다
2. 할 일 4를 추가(낙관적 업데이트)한다. ➔ 할 일 4를 추가하는 요청을 서버로 보내고, UI는 즉시 할 일 4를 추가한 상태로 업데이트 한다.
3. 할 일 3이 업데이트되어 변경 사항을 반영하기 위해 쿼리 무효화(refetch)가 발생한다.
4. 변경 사항이 업데이트 된다. ➔ 기존 낙관적 업데이트로 할 일 4를 추가했는데 업데이트로 인해 사라질 수 있다.
5. 할 일 4를 추가하는 뮤테이트가 완료된 후 변경 사항이 업데이트되어 할 일 4가 다시 보이게 된다.
💡 이렇게 쿼리 무효화(refetch)를 통해 업데이트가 발생하는 경우에 기존 낙관적 업데이트로 추가된 항목이 서버 데이터와 일치하지 않을 경우 UI에서 사라질 수 있기 때문에 낙관적 업데이트의 장점이 사라지게 되어 쿼리 요청을 취소하는 것이다!
UI: UI에서 직접적으로 상태를 관리하여 사용자가 요청한 변경 사항을 즉시 반영한다.
Cache: 서버에 요청을 보내기 전에 먼저 로컬 캐시를 즉시 업데이트한다. 이는 사용자가 요청한 변경 사항을 즉시 반영하는 방식이다.
UI: 서버에서 응답이 성공적으로 돌아오면 그에 맞춰 UI 상태를 업데이트하므로, 데이터의 일관성이 유지된다.
Cache: 사용자가 요청을 보낸 즉시 UI에 변화가 반영되므로, 사용자 경험이 향상된다.
UI: 별도의 상태 관리 로직을 구현해야 하므로 복잡성이 증가할 수 있다.
Cache: 서버에서 응답이 실패하거나 데이터가 변경되면, UI와 서버 데이터 간의 불일치가 발생할 수 있다.
UI: 데이터의 일관성이 매우 중요한 경우(ex: 결제 시스템, 사용자 프로필 업데이트 등)
Cache: 사용자가 즉시 결과를 확인해야 하는 경우(ex: 할 일 추가, 삭제 등)
UI를 통한(Via the UI) 낙관적 업데이트는 데이터의 일관성을 유지하는데 유리하지만, 사용자 경험에 약간의 지연이 발생할 수 있다.
캐시를 통한(Via the Cache) 낙관적 업데이트는 즉각적인 반응성을 제공하지만, 데이터의 일관성 문제와 오류 처리의 복잡성을 동반한다.
즉각적인 사용자 피드백이 필요한 경우 ➔ 캐시를 통한 방법
데이터의 일관성이 중요한 경우 ➔ UI를 통한 방법
Optimistic Updates in Tanstack Query v5
Tanstack Query 공식문서 - Optimistic Updates
예시로 살펴보는 낙관적 업데이트(optimistic updates) 2가지 방법
react-query optimistic update시 데이터 꼬임 방지
React-Query를 사용하여 optimistic update 구현하기
잘 읽고 갑니다! 출처 표기까지 해주셔서 편하네요 :) 좋은하루 되세요!