⚽️ 목표
useQuery사용 시 select로 백앤드로 부터 받아오는 데이터를 정제하여 사용하고 있던 중, Optimistic Update로 부딪혀 버린 불지옥
•
이글 은 optimistic update 자체에 대한 설명이 아니다.
TL;DR
select로 정제된 데이터를 기반으로 Optimistic Update를 수행하는게 아니라 select 이전 원본 데이터를 기반으로 Optimistic Update를 수행해야한다.
•
이 짧은 한구절을 제외하곤 어디서도 제대로 써있지 않았다. → “It affects the returned data value, but does not affect what gets stored in the query cache.”
1. 어떻게 굴러가고 있니?
•
selector로 useQuery 데이터를 변형해서 사용하고 있으며 게시글 삭제 시 Optimistic Update를 활용해 게시글을 삭제하는 현재 상황이다.
백엔드에서 상하차로 데이터를 내려주고 있는 데이터 형태
interface PostDef {
id: number;
posting_title: string;
posting_author: string;
posting_content: string;
}
TypeScript
복사
•
백엔드에서 브라우저로 전달해주는 데이터 형태
•
id를 제외하곤 데이터가 스네이크 케이스로 상하차되어 내려오고 있다.
interface ChangedDef {
postingId: number;
title: string;
author: string;
content: string;
}
TypeScript
복사
•
useQuery의 결과 값인 data에서 실제로 사용 할 수 있는 데이터 형태
•
프론트에선 ChangedDef 형태로 변형해서 사용하고 있다.
useGetPostArr.tsx
const getPostArr: GetPostArrDef = async () => {
const { data } = await axios.get("http://localhost:3005/posting");
return data;
};
export const useGetPostArr = () =>
useQuery(postingArrKeyObj.postingArr, getPostArr, {
select: (data) => postingArrSelector(data),
});
TypeScript
복사
•
포스팅을 불러오는 훅, postingArrSelector에서 데이터를 변환하여 해당 데이터가 필요한 컴포넌트에서 간편하게 우리가 원하는 대로 꺼내먹고 있는 상황이다.
2. 문제점
select에서 변화하고 난 후의 값을 참조하려 했던 것
export const useDeletePost = () => {
const queryClient = useQueryClient();
return useMutation(deletePost, {
onMutate: async (requestObj) => {
// ...생략
// optimistic update가 수행되는 부분에 generic 인자 없음
queryClient.setQueryData(
postingArrKeyObj.postingArr,
(oldPostArr) => {
if (!oldPostArr) return undefined;
// selector로 변환된 값인 postingId가 아닌 변환되기 전 값 posting_id로 참조가능
const filteredArr = oldPostArr.filter(
(oldPost) => oldPost.postingId !== requestObj.postingId
);
return [...filteredArr];
}
);
return { previousData, requestObj };
},
});
};
TypeScript
복사
•
useQuery에서 select를 했으므로 id(서버에서 내려오는 기본 형태)가 아닌 postingId(select로 변경한 값)로 참조되야하는게 아닌가?로 시작한 기능 적용.
•
optimistic update가 수행되지 않고 있는 상황이다. postingId를 전혀 참조하지 못하고 있는 상황.
3. 해결
•
공식문서에도 나와있다 싶이 (Direct 하지 않고 너무 원론적인 설명이라고 찡찡대지 말자, 이해가 안되면 또읽어보고 또 읽어봤어야지.) 반환된 데이터 값에만 영향을 끼치고 쿼리 캐시에는 영향을 끼치지 않는다.
•
좀 더 쉽게 말하자면 useQuery에서 select하여 변경된 값들은 useQuery 내부에서만 해당되는 값들이다.
•
optimistic update에서 사용되는 setQueryData에서는 useQuery select를 이용해 변경하기 이전 값을 참조해야한다.
setQueryData
setQueryData<TQueryFnData>(queryKey: QueryKey, updater: Updater<TQueryFnData | undefined, TQueryFnData | undefined>, options?: SetDataOptions): TQueryFnData | undefined;
TypeScript
복사
•
generic으로 설정된 TQueryFnData가 전달되지 않으면 setQueryData내에서 updater function의 값들은 unknown으로 추론된다.
•
“setQueryData는 당연히 니가 바꾼 데이터에 대한 타입을 모르지” → “key값을 인자로 받고 키값에 매칭되는 데이터의 형태를 어떻게 아니? 그러니까 unknown 아니겠니?”
•
여기서 generic으로 선언되는 타입은 select 이전 API에서 받아오는 타입 이여야 한다.
◦
해당 케이스에서는 ChangeDef가 아닌 PostDef여야한다.
최종 코드
// ... 생략 ...
export const useDeletePost = () => {
const queryClient = useQueryClient();
return useMutation(deletePost, {
onMutate: async (requestObj) => {
await queryClient.cancelQueries(postingArrKeyObj.postingArr);
const previousData = queryClient.getQueryData(
postingArrKeyObj.postingArr
);
// PostDef를 Generic으로 지정
queryClient.setQueryData<PostDef[]>(
postingArrKeyObj.postingArr,
(oldPostArr) => {
if (!oldPostArr) return undefined;
// selector로 변환된 값인 postingId가 아닌 변환되기 전 값 posting_id로 참조가능
const filteredArr = oldPostArr.filter(
(oldPost) => oldPost.id !== requestObj.id
);
return [...filteredArr];
}
);
return { previousData, requestObj };
},
});
};
TypeScript
복사
•
setQueryData에 generic 타입을 선언해주어 실제로 참조 가능한 타입 추론이 가능하다.
•
당연한 말이겠지만 useDeletepost라는 wrapper를 사용하는 모든 쿼리가 OU가 수행되어야한다면 쿼리선언부에 위와 같이 설정해줘도 좋으나, 대부분 상황마다 실행하는 OU가 다르다. 본인은 mutation을 수행하는 Eventy Handler에 getQueryData와 setQueryData를 직접 호출해 사용한다.