
이전 포스트에서 invalidateQueries와 setQueryData를 공부하면서 두 함수는 서로 다른 역할을 한다는 것을 알았고, 이를 공부하면서 낙관적 업데이트에 대해 알게됐다.
물론 setQueryData는 비슷하게 동작하는 것 처럼 보였지만 단순히 setQueryData 하나로는 낙관적 업데이트가 아니다. 서버의 데이터와 클라이언트에서 바꾼 데이터가 동일한지 검증하는 과정이 없다.
클라이언트에서 변경한 데이터를 UI에 보여줄 때, 서버의 확인을 기다리지 않고 클라이언트에서 먼저 계산한 변경된 UI를 즉시 보여준다. 그 후 서버에서 확인하여 데이터 무결성 체크가 잘됐다면 그대로 OK, 아니라면 다시 변경 전 UI로 롤백한다.
낙관적 : 미래를 밝고 희망적으로 보는 (것).
클라이언트에서 계산한 UI와 서버에서 확인한 UI가 같을 것이라고 희망을 가진채로 업데이트하기에 낙관적 업데이트라고 불리는 것 같다.
1. UI 업데이트 : 변경된 데이터로 먼저 어떻게 UI가 바뀔지 예상하여 해당 UI를 먼저 클라이언트에 보여준다.
2. 서버 확인 : 서버에서 낙관적 업데이트 한 UI를 검증
3. 검증 성공 시 : 서버에서 낙관적 업데이트에서 완벽한 업데이트할 UI에 필요한 데이터를 fetch하여 클라이언트와 서버의 데이터를 연동.
4. 검증 실패 시 : 이전 UI로 롤백
예를 들면 좋아요(like) state 를 변경하고,
변경한 state를 서버에 보내준 후 그 state가 적용된 UI를 서버에서 받아야 하는데,
낙관적 업데이트는 미리 그 state가 적용된 UI 를 서버에서 받기전에 클라이언트에 보여준다. 그리고 변경한 state를 받은 서버에서 낙관적 업데이트 한 클라이언트의 UI를 확인하여 검증이 완료되면 이제 낙관적 UI와 서버의 완벽한 UI와 연동 실행, 아니면 이전 UI로 롤백한다.
react-query에서는 onMutate를 통해 낙관적 업데이트를 실행한다.
export default function useWebtoons(){
const queryClient = useQueryClient();
// 웹툰리스트 데이터 가져오기. { list : webtoon[], totalCount(전체 웹툰 개수) : number }
const { data : webtoons } = useQuery({
queryKey : ["webtoons"],
queryFn : () => getWebtoons(),
})
const { mutate } = useMutation({
mutationFn : () => addWebtoon(),
onMutate : async () => {
const previousWebtoons = queryClient.getQueryData(["webtoons"]);
// 리스트에 새로운 웹툰을 넣고, totalCount + 1을 하여
// 새로운 웹툰리스트 데이터를 ["webtoons"] 키를 가진 쿼리 캐시에 삽입하여
// 낙관적 업데이트 실시
queryClient.setQueryData(["webtoons"], ()=>{
return {
list : [
...previousWebtoons.list,
newWebtoon
],
totalCount = totalCount + 1
}
})
// 아래 onError의 context 에서 사용하기 위함.
return { previousWebtoons };
},
// 낙관적 업데이트 후 서버에서 검증 후 실패 시
// ["webtoons"] 쿼리 캐시의 데이터를 업데이트 이전 값으로 롤백시킴.
onError : (error, newWebtoon, context) => {
queryClient.setQueryData(["webtoons"], context.previousWebtoons)
},
// 낙관적 업데이트 후 서버에서 검증 후 성공 시
// invalidateQueries로 낙관적 업데이트에 의한 ["webtoons"] 쿼리 캐시에 저장한 데이터를
// 서버에서 가져온 데이터로 갱신하기 위하여 삭제
onSettled : () => {
queryClient.invalidateQueries(["webtoons"])
}
})
return { webtoons, addWebtoon : mutate }
}
완전한 낙관적 업데이트를 위해서는 invalidateQueries와 setQueryData 를 둘 다 사용한다는 것을 알 수 있었다.
또 다른 라이브러리인 SWR에서도 낙관적 업데이트를 지원한다.
const { mutate } = useSWRConfig()
mutate("cache key")
위의 코드는 global mutate 로써, refetch 를 하고 싶은 캐시키를 전달하면
해당 캐시키를 가진 쿼리를 refetch 한다.
하지만 global이 아닌 Bound Mutate를 통해 낙관적 업데이트가 가능하다.
const { data, isLoading, mutate } = useSWR("cache key");
mutate(mutate Fn, {
optimisticData : newdata ,
populateCache : false,
revaildate : false,
rollBackOnError : true
})
optimisticData : 즉각적으로 UI에 업데이트(낙관적 업데이트)하기 위한 데이터를 이 옵션에 전달해주면 해당 값으로 UI를 먼저 업데이트하고, 그 동안 백그라운드에서 서버로부터 데이터를 받아온다.
populateCache :mutate Fn의 반환값으로 업데이트하는 것이 아니라,optimisticData로 업데이트 하기 위해false를 전달.
revaildate :optimisticData에 전달한newdata는 UI를 낙관적 업데이트 하기 위한 데이터이다. 즉, 결국 서버에서 가져올 데이터와 동일할 수 있는데, 동일하다는 것을 검증할 수만 있다면 서버에서 새롭게 가져올 필요가 없다. 그래서false로 지정하여revaildate를 막는다.
rollBackOnError :optimisticData로 로컬 상의 UI를 먼저 업데이트 했는데 mutate Fn 이 중간에 제대로 동작하지 못해서 1.1 문단에서 말한 검증에 실패할 경우 optimisticData로 로컬상의 UI를 먼저 변경한 내용을 롤백한다.