home

사무치는 Optimistic update (feat. tanstack query)

글 분류
main
키워드
react query
생성일
2023/02/05 04:02
최근 수정일
2023/05/28 11:33
작성중

⚽️ 목표

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를 직접 호출해 사용한다.

참조