왜 사용?
좋아요 버튼을 누르고 서버에서 상태값을 받아오고 그 값을 토대로 업데이트 된 화면을 보기까지 시간이 꽤 걸림
따라서, 클라이언트 상태에서는 동작이 미래에 낙관적으로 업데이트 될거라고 가정하에 미리 화면부터 변경시키고 이후에 서버로부터 받은 상태와 비교를 해서 동일할 경우 그대로 유지하고, 다를 경우 이전의 상태로 롤백하는 것이다.
전역 상태관리로 구현하는 방법(간단 설명)
값이 업데이트 되면 우선 stroe의 값을 임시값으로 바꿔주고, 서버의 값을 업데이트하고, store의 값을 서버에서 요청한 값으로 업데이트한다.
Tanstack query로 구현하는 방법
현재 프로젝트에서는 서버와의 데이터 요청, 동기화등은 Tanstack query를 사용하고 있고, 낙관적 업데이트에 대한 내용도 공식문서에 있으므로 Tanstack query를 통해 구현해봤다.
이전 코드
import { postLike } from "@api/like/postLike" import { Project, postLikePayload } from "api-models" import { useMutation, useQueryClient } from "@tanstack/react-query" import { QUERY_KEY_GET_PROJECT_DETAIL } from "../queries/useProjectDetailQuery" const QUERY_KEY_POST_LIKE = "POST_LIKE_234893204832" export const usePostLikeMutation = () => { const queryClient = useQueryClient() const postLikeMutation = useMutation({ mutationKey: [QUERY_KEY_POST_LIKE], mutationFn: (data: postLikePayload) => postLike(data), onSuccess: () => { queryClient.invalidateQueries({ querykey: [QUERY_KEY_GET_PROJECT_DETAIL], }) } }) return { postLikeMutation } }
이전에는 낙관적 업데이트 없이 좋아요 요청/삭제시 API를 호출 후 성공시에 프로젝트 정보를 다시 받아온다.
이제 수정해보자.
순서는 다음과 같다.
- onMutate(요청을 보내기전 실행되는 함수) 안에서 요청을 보내기 전
cancleQueries
를 통해 프로젝트 정보를 받아오는 쿼리키를 무효화 하자.
onMutate: async ({ projectId }) => { await queryClient.cancelQueries({ queryKey: [QUERY_KEY_GET_PROJECT_DETAIL], }) }
getQueryData
를 통해 이전 쿼리값을 저장해논다.
여기서 많이 헤맸는데 제네릭으로 타입을 명시해주지 않으면 타입이
unknown
으로 지정돼서 아래에서 이 값에 접근할때 빈 객체로 판단되어 타입 오류가 난다.const previousLikeState = queryClient.getQueryData<Project>([ QUERY_KEY_GET_PROJECT_DETAIL, projectId, ])
- 이전 데이터가 존재하는 경우
setQueryData
를 통해 쿼리의 데이터를 업데이트한다
현재 프로젝트 정보를 받아올때 내려오는 좋아요와 관련된 데이터는 다음과 같아서 likeId는 임의의 숫자를 넣는다.
임의의 숫자를 넣으면 서버는 알아서 null이 아닌 경우 사용자를 인식해 고유한 아이디를 다시 부여하고 좋아요를 등록한다.
likeCount는 +1을 해준다.
likeId : number // 해당 게시물의 좋아요 고유 아이디(내가 이미 누른 경우 number, 안 누른 경우 null) likeCount: number // 해당 게시물의 좋아요 수
if (previousLikeState) { const updatedLikeState = { ...previousLikeState, likeId: 99999, likeCount: previousLikeState.likeCount + 1, } queryClient.setQueryData( [QUERY_KEY_GET_PROJECT_DETAIL, projectId], updatedLikeState, ) }
- 여기서 이전 상태를 return하는 이유는 onMutate안에서 리턴하는 값은 context로 전달하기 위해서이다. 즉, 롤백을 위해서이다.
return { previousLikeState }
- 에러가 난 경우(서버에서 데이터를 전달 받지 못한 경우) context에서 이전 데이터를 가져와 롤백한다.
onError: (err, _, context) => { console.log(err) queryClient.setQueryData( [QUERY_KEY_GET_PROJECT_DETAIL], context?.previousLikeState, ) },
onSettled
를 통해 요청의 성공/실패 여부와 상관없이 쿼리키를 invalidate해서 리패치 한다.
onSettled: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEY_GET_PROJECT_DETAIL], }) },
전체코드
import { postLike } from "@api/like/postLike" import { Project, postLikePayload } from "api-models" import { useMutation, useQueryClient } from "@tanstack/react-query" import { QUERY_KEY_GET_PROJECT_DETAIL } from "../queries/useProjectDetailQuery" const QUERY_KEY_POST_LIKE = "POST_LIKE_234893204832" export const usePostLikeMutation = () => { const queryClient = useQueryClient() const postLikeMutation = useMutation({ mutationKey: [QUERY_KEY_POST_LIKE], mutationFn: (data: postLikePayload) => postLike(data), onMutate: async ({ projectId }) => { await queryClient.cancelQueries({ queryKey: [QUERY_KEY_GET_PROJECT_DETAIL], }) const previousLikeState = queryClient.getQueryData<Project>([ QUERY_KEY_GET_PROJECT_DETAIL, projectId, ]) if (previousLikeState) { const updatedLikeState = { ...previousLikeState, likeId: previousLikeState.likeId ? null : 99999, likeCount: previousLikeState.likeCount + 1, } queryClient.setQueryData( [QUERY_KEY_GET_PROJECT_DETAIL, projectId], updatedLikeState, ) } return { previousLikeState } }, onError: (err, _, context) => { console.log(err) queryClient.setQueryData( [QUERY_KEY_GET_PROJECT_DETAIL], context?.previousLikeState, ) }, // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우 onSettled: () => { queryClient.invalidateQueries({ queryKey: [QUERY_KEY_GET_PROJECT_DETAIL], }) }, }) return { postLikeMutation } }