이전에 쓴 [react-query] useQuery의 데이터 불변성을 지켜야할까?? 와 이어진다.
이전 글에서는 useQuery
로 받아온 data를 어떻게 건들이지 않아야하는지를 다뤘다.
이번에는 해당 데이터를 어떻게 수정해서 mutate까지 이어지는지 적어보고자 한다. (이것때문에 하루종일 맘고생했다😢)
서버 업데이트를 하기 전에 미리 화면의 UI를 바꿔준 후, 서버와의 통신 결과에 따라 확정 / 롤백을 결정하는 방식이다. (아, 어차피 되게 되어있다구~ 해서 낙관적이라고 명명했나보다.)
코드는 다음과 같다.
const onDragEnd = result => {
// source : 현재 위치한 droppable의 위치, 인덱스
// destination : dnd를 마친 후 droppable의 위치, 인덱스
// 바깥으로 drop 시에
if (!result.destination) {
return;
}
// 같은 자리에 가져다 두었다면 그냥 리턴
if (result.destination.index === result.source.index) {
return;
}
const items = reorder(
gameList,
result.source.index,
result.destination.index,
);
if (data) gameList = items; // 새로운 것 옮겨두고.
gameListMutate.mutate(gameList); // 드래그 앤 드랍이 끝나면 이걸 mutate
};
const component = () => {
...
let gameList = useMemo(() => data.data, [data]); // data의 불변성을 유지해주기 위해서
const queryClient = useQueryClient();
const gameListMutate = useMutation(
items => {
return axiosInstance.put('게임리스트 API', items);
},
{
// onMutate : mutation function이 시작되기 전에 작동
onMutate: () => {
const oldData = queryClient.getQueryData(['game-account-list']);
// 우리 update overwrite하지 않기 위해 미리 취소
queryClient.cancelQueries(['game-account-list']);
// 미리 UI에 적용시켜 놓음
queryClient.setQueryData(['game-account-list'], () => {
return gameList;
});
// 만약 에러나서 롤백 되면 이전 것을 써놓음.
return () => queryClient.setQueryData(['game-account-list'], oldData);
},
// success든 error든 invalidate해서 새로 받아옴.
onSettled: () => {
return queryClient.invalidateQueries(['game-account-list']);
},
// onError의 세번째 인수 rollback이 onMutate의 return
onError: (err, values, rollback) => {
if (rollback) {
rollback();
} else {
console.log(err);
}
},
},
);
...
}
위의 방법으로 data의 불변성을 유지한채로 통신이 가능해졌으나, 계정의 위치를 이동시킬 때마다 통신이 되어서 매번 새로운 데이터를 불러와서 깜빡이는 문제가 발생했다. 다음의 방법으로 해결했다.
const component = () => {
...
let gameList = useMemo(() => {
return data.data
? data.data
: queryClient.getQueryData(['game-account-list']);
}, [data, queryClient]); // 🌹 gameList에 넣은 data가 없을 때를 대비해서 이전에 캐싱해둔 데이터를 넣어주기로 했다.
const gameListMutate = useMutation(
items => {
return axiosInstance.put('게임리스트 API', items);
},
{
// onMutate : mutation function이 시작되기 전에 작동
onMutate: async newData => { // 🌹 공식 문서를 따라서 newData 부분 수정
const oldData = queryClient.getQueryData(['game-account-list']);
// 우리 update overwrite하지 않기 위해 미리 취소
await queryClient.cancelQueries(['game-account-list']);
// 미리 UI에 적용시켜 놓음
queryClient.setQueryData(['game-account-list'], newData);
// 만약 에러나서 롤백 되면 이전 것을 써놓음.
return () => queryClient.setQueryData(['game-account-list'], oldData);
},
// success든 error든 invalidate해서 새로 받아옴.
onSettled: () => {
//return queryClient.invalidateQueries(['game-account-list']);
// 🌹 mutate가 일어나고 바로 새로운 것 받아오지 않도록 변경. 이러면 setQueryData에서 설정한 새로운 값이 캐싱되어서 그걸 쓸 것임.
},
// onError의 세번째 인수 rollback이 onMutate의 return
onError: (err, values, rollback) => {
if (rollback) {
rollback();
} else {
console.log(err);
}
},
},
);
// 🌹 대신 저장 버튼 누르면 쿼리 다시 받아오기로.
// 제출 button click 시에
const onGameEnrollmentOrderSubmit = () => {
gameListMutate.mutate(gameList, {
onSuccess: () => {
closeModal();
return queryClient.invalidateQueries(['game-account-list']); // 저장 눌어야만 실제 서버 데이터랑 동기화
},
onError: err => {
console.log(err);
},
});
};
...
data.data
에서 받아오는 gameList
의 check를 수정할 때, 저장 버튼을 누르지 않고 닫기를 눌러도 저장되는 것 처럼 보이는 버그 발생.
해결방법 : gameList
를 lodash의 cloneDeep으로 저장
let gameList = useMemo(() => {
return data?.data
? _.cloneDeep(data.data)
: [...queryClient.getQueryData(['game-account-list'])];
}, [data, queryClient]);
https://velog.io/@sv002/%EC%A2%8B%EC%95%84%EC%9A%94-%EA%B8%B0%EB%8A%A5-react-query-optimistic-update
https://react-query.tanstack.com/guides/optimistic-updates#_top