1. Tanstack Query란?
: 서버 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 비동기 상태 관리 라이브러리
- Tanstack Query를 사용함으로써 서버 데이터와 클라이언트 데이터를 분리
Tanstack Query는 비동기 처리 라이브러리(Data fetching library)가 아니라, 비동기 상태 관리 라이브러리입니다. 비동기 처리 라이브러리는, 브라우저에 내장되어 있는 Fetch API 및 서드 파티 라이브러리인 Axios가 있습니다. 지역 상태를 관리하는 useState, 전역 상태를 관리하는 Context API, Zustand, Redux 등이 있다면 서버 상태를 관리하는 Tanstack Query, SWR, Redux Saga등이 있습니다. 이렇듯 클라이언트와 서버의 상태 분리를 할 수 있도록 해주며, 비동기 로직을 제거하여 관심사가 분리되어 선언적으로 작성할 수 있도록 해주는 라이브러리입니다.
타 라이브러리와 비교
Tanstack Query vs Redux Saga vs SWR

전역 상태 관리 라이브러리에서처럼 큰 차이를 보이지는 않고, 어느정도 치열한 다운로드 수 차이를 확인할 수 있습니다.
1. Redux Saga
Redux Saga는 액션을 모니터링하고 있다가 특정 액션이 발생하면 이에 따라 특정 작업을 하는 방식으로 사용합니다. 여기서 특정 작업이란, 특정 자바스크립트를 실행하는 것 일수도 있고, 다른 액션을 디스패치 하는 것 일수도 있고, 현재 상태를 불러오는 것 일수도 있습니다.
- 단점
- Javascript 초심자라면 생소할만한 Generator 문법을 사용
- 사용할 때 늘어나는 보일러 플레이트 코드와 사가 함수 재사용에 대한 제약들로 인해 쓸데없이 추가되는 코드
- 유지보수가 어려움
2. SWR
SWR은 Next.js를 만든 vercel에서 개발한 라이브러리로, Tanstack Query와 유사한 기능을 제공합니다.
- Tanstack Query가 SWR보다 좋은 점
- Devtools : 데이터 흐름을 파악할 수 있음. SWR은 서드 파티 라이브러리를 이용해야 한다.
- 무한 스크롤 구현
getPreviousPageParam,fetchPreviousPage,hasPreviousPage와 같은 다양한 페이지 관련 기능 존재
select를 사용해 raw data로부터 원하는 데이터 추출하여 반환할 수 있다.- data optimization
- garbage Collection : 캐싱된 데이터가 메모리에서 해제되는 시점을 정할 수 있다.
2. Tanstack Query의 각 기능들
- 캐싱 :
Query Key를 이용하여 관리 - get한 데이터에 대해 update를 하면 자동으로 get을 다시 수행한다.
- e.g. 게시판의 글을 가져왔을 때 게시판의 글을 생성하면 게시판 글을
get하는api를 자동으로 실행
- e.g. 게시판의 글을 가져왔을 때 게시판의 글을 생성하면 게시판 글을
- 데이터가 오래 되었다고 판단되면 다시 get :
invalidateQueries - 동일 데이터 여러번 요청하면 한번만 요청한다.
- 옵션에 따라 중복 호출 허용 시간 조절 가능
- 무한 스크롤 :
Infinite Queries - 비동기 과정을 선언적으로 관리할 수 있다.
1. useQuery
- 데이터를 GET하기 위한 훅
const Todos = () => {
const { isLoading, isError, data, error } = useQuery("todos", fetchTodoList);
if (isLoading) return <div>로딩중</div>;
if (isError) return <div>{error.message}</div>;
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
};
-
useQuery(
unique Key,비동기 함수(promise),옵션)unique Key: 배열의 첫 번째 값은 다른 컴포넌트에서 부를 값, 두 번째 값은 query 함수 내부에 인자로 전달
-
비동기로 작동
enabled사용하여 동기적으로 사용 가능
-
useQueries이용하여 비동기로 여러개 실행할 수 있음 -
QueryCache : 쿼리에 대해 성공, 실패 전처리할 수 있음
const queryClient = new QueryClient({ queryCache: new QueryCache({ onError: (error, query) => { console.log(error, query); if (query.state.data !== undefined) { toast.error(`에러가 났어요!!: ${error.message}`); }, }, onSuccess: data => { console.log(data) } }) });
2. useMutation
-
데이터를 post, update하기 위한 api
-
사용법은
useQuery와 동일 -
update후에 get함수 간단히 재실행 가능
const mutation = useMutation(postTodo, { onSuccess: () => { // postTodo가 성공하면 todos로 맵핑된 useQuery api 함수를 실행합니다. queryClient.invalidateQueries("todos"); } }); -
만약 mutation에서 return된 값을 이용하여 get함수의 파라미터를 변경해야 할 경우
setQueryData이용const queryClient = useQueryClient(); const mutation = useMutation(editTodo, { onSuccess: data => { // data가 fetchTodoById로 들어간다 queryClient.setQueryData(["todo", { id: 5 }], data); } }); const { status, data, error } = useQuery(["todo", { id: 5 }], fetchTodoById); mutation.mutate({ id: 5, name: "nkh" });
3. suspense
-
Suspense를 사용하여
loading을, Error boundary를 사용하여error핸들링을 직관적으로 할 수 있음// src/index.js const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0, suspense: true } } }); ReactDOM.render( <React.StrictMode> <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> </React.StrictMode>, document.getElementById("root") ); -
위 경우는 전역으로
suspense를 사용하는 경우 (v4 기준이다. v5에서는 이 옵션이 사라져서 아래useSuspenseQuery를 쓴다) -
위처럼 suspense 설정을 했다면 React에서 제공하는
Suspense사용하면 된다const { data } = useQurey("test", testApi, { suspense: true }); return ( // isLoading이 true이면 Suspense의 fallback 내부 컴포넌트가 보여집니다. // isError가 true이면 ErrorBoundary의 fallback 내부 컴포넌트가 보여집니다. <Suspense fallback={<div>loading</div>}> <ErrorBoundary fallback={<div>에러 발생</div>}> <div>{data}</div> </ErrorBoundary> </Supense> );
4. 캐싱
- 캐싱 : 특정 데이터의 복사본을 저장하여 이후 동일한 데이터의 재접근 속도를 높이는 것
- fresh한 데이터 = 최신의 데이터
- 기존의 데이터 = stale한 데이터
- 데이터 갱신하는 타이밍
- 화면을 보고 있을 때
- 페이지의 전환이 일어났을 때
- 페이지 전환없이 이벤트가 발생해 데이터를 요청할 떄
1. 다양한 옵션 제공
refetchOnWindowFocus: default = true. 브라우저에 포커스가 들어온 경우refetchOnMount: 새로운 컴포넌트 마운트가 발생한 경우refetchOnReconnect: default = true. 네트워크 재연결이 발생한 경우staleTime: default = 0. 데이터가 fresh → stale 상태로 변경되는 데 걸리는 시간- fresh 상태일 때 refetch 트리거(위 3가지)가 발생해도 refetch가 일어나지 않음
- 기본값이 0이므로 따로 설정해주지 않으면 refetch가 일어났을 때 무조건 refetch 발생

cacheTime: default = 5분 (60 * 5 * 1000) 데이터가 inactive(비활성)한 상태일 때 캐싱된 상태로 남아있는 시간
- 특정 컴포넌트가 unmount(페이지 전환 등으로 화면에서 사라질 때)되면 사용된 데이터는 inactive상태로 변환 > 데이터는 cacheTime만큼 유지
- cacheTime 이후 데이터는 가비지 컬렉터로 수집되어 메모리에서 해제됨
- 만약 cacheTime이 지나지 않았는데 해당 데이터를 사용하는 컴포넌트가 다시 mount되면, 새로운 데이터는 fetch해오는 동안 캐싱된 데이터를 보여줌
- 즉, 캐싱된 데이터는 계속 보여주는 게 아니라 fetch하는 동안 임시로 보여줌
2. Query Key
query cache는 Key가 직렬화되어 있고, Key는 해쉬되어 관리됩니다.
💡 직렬화?
: 데이터 구조나 객체 상태를 저장 또는 전송 가능한 형식으로 변환하는 과정
- Javascript에서 객체를 직렬화하면 JSON 문자열 형태로 변환됩니다. 이 때 이 직렬화를 의미한다.
또한, 해쉬되어 관리되기에 배열 원소의 순서는 Query Key를 식별하는데 관련이 없습니다.
-
예시
useQuery(['todos', { status, page }], ...) useQuery(['todos', { page, status }], ...) useQuery(['todos', { page, status, other: undefined }], ...)위 쿼리는 Key가 모두 같으므로 같은 쿼리로 취급한다.
-
Key는 쿼리에 대해 unique해야 한다.
-
Tanstack Query는 cache에 Key를 이용해서 접근한다.
-
객체로 Key 관리하는 방법
export const commentQueryKey = { comments: (idCardId: number) => ['comments', idCardId], commentReplies: (idCardId: number, commentId: number) => ['replies', idCardId, commentId], commentCount: (idCardId: number) => ['commentCount', idCardId], };- 권장하는 방식
- 유지보수에 용이
5. prefetching
: 새로운 데이터를 받기 전에 사용자가 캐싱된 데이터를 볼 수 있어 UX에 좋은 영향을 준다.
1. 페이지네이션에서, page=2로 이동했을 때 page=3의 데이터를 미리 받아놓는 것
const prefetchTodos = async () => {
// The results of this query will be cached like a normal query
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
}
2. Hydration과 함께 데이터를 넘겨주는 방법
export const HydrationProvider = async ({
children,
queryKey,
queryFn,
isInfiniteQuery = false,
}: StrictPropsWithChildren<HydrationProviderProps>) => {
const getQueryClient = cache(() => new QueryClient());
const queryClient = getQueryClient();
await queryClient.prefetchQuery(queryKey, queryFn);
const dehydratedState = dehydrate(queryClient);
return <Hydrate state={dehydratedState}>{children}</Hydrate>;
};
6. invalidateQueries
- 캐시된 데이터를 무효화하는 함수
- 해당 데이터가 즉각 stale 상태가 되어 refetching한다.
const queryClient = useQueryClient();
const {mutateAsync} = useMutation(postLogout,{
onSuccess: () => queryClient.invalidateQueries(["todos"]);
})
7. setQueryData
-
쿼리 데이터를 수동으로 설정
const queryClient = useQueryClient(); queryClient.setQueryData(['todos', 'list', { filters }], (previous) => previous.map((todo) => (todo.id === newTodo.id ? newtodo : todo)) )
8. cancelQueries
-
쿼리를 취소하는 함수
const queryClient = useQueryClient(); const {mutateAsync} = useMutation(postLogout,{ onSuccess: () => queryClient.cancelQueries(["todos"]); })
3. Tanstack Query 특징
1. Tanstack Query v4에서의 변경점
- 라이브러리명이 기존
react-query에서@tanstack/react-query로 변경 - Query key에 입력값이 하나여도 무조건 배열로 넣어야 함
- useQueries에 여러 쿼리를 넘길 떄 queries를 key값으로 설정
- query가 잘못된 경우
undefined를 반환하지 않고 error처리
2. Mutate vs MutateAsync
mutate: 아무것도 반환 XmutateAsync: mutation 결과를 담은 Promise 반환- 이를 사용할 경우
Promise를 직접 제어할 수 있어서 오류를 직접 처리해야 함
- 이를 사용할 경우
- 비추
4. 코드 분석
1. 낙관적 업데이트 - 1
const [text, setText] = useState('')
const addTodoMutation = useMutation({
onMutate: async (newTodo: string) => {
setText('')
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previousTodos = queryClient.getQueryData<Todos>(['todos'])
if (previousTodos) {
queryClient.setQueryData<Todos>(['todos'], {
...previousTodos,
items: [
...previousTodos.items,
{ id: Math.random().toString(), text: newTodo },
],
})
}
return { previousTodos }
},
})
// ...
return(
<form
onSubmit={(e) => {
addTodoMutation.mutate(text)
}}
>
</form>
)
onMutate: mutation 함수가 실행되기 전에 실행되고 mutation 함수가 받을 동일한 변수가 전달됩니다.setText(''): input을 초기화합니다.await queryClient.cancelQueries({ queryKey: ['todos'] })- 쿼리가 실행되는 동안 새로운 쿼리가 실행되지 않도록 쿼리를 취소하여 이전 서버 데이터가 낙관적 업데이트를 덮어쓰지 않도록 합니다.
const previousTodos = queryClient.getQueryData<Todos>(['todos'])- 이전 쿼리 데이터를 가져옵니다.
queryClient.setQueryData<Todos>(['todos'], { ...previousTodos, items: [ ...previousTodos.items, { id: Math.random().toString(), text: newTodo }, ], })- 이전 쿼리 데이터를 가져와서 새로운 데이터를 추가한 후 쿼리 데이터를 업데이트
- 즉, 낙관적 업데이트
return { previousTodos }- 이전 쿼리 데이터를 반환합니다.
낙관적 업데이트 이후의 코드
onError: (err, variables, context) => {
if (context?.previousTodos) {
queryClient.setQueryData<Todos>(['todos'], context.previousTodos)
}
},
// Always refetch after error or success:
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
onError: (err, variables, context) => { if (context?.previousTodos) { queryClient.setQueryData<Todos>(['todos'], context.previousTodos) } }- 에러가 발생한다면 이전 쿼리 데이터를 가져와서 쿼리 데이터를 해당 데이터로 업데이트
onSettled: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }) }- 에러가 발생하든 성공하든 쿼리를 다시 실행
queryClient.invalidateQueries({ queryKey: ['todos'] })- 캐싱을 초기화, 즉 쿼리를 다시 실행
2. 낙관적 업데이트 - 2
export const commentQueryKey = {
comments: (idCardId: number) => ['comments', idCardId],
commentReplies: (idCardId: number, commentId: number) => ['replies', idCardId, commentId],
commentCount: (idCardId: number) => ['commentCount', idCardId],
};
export const deleteComment = ({ idCardId, commentId }: CommentDeleteRequest) =>
privateApi.delete<CommentDeleteResponse>(`id-cards/${idCardId}/comments/${commentId}`);
// useDeleteComment 커스텀 훅. 댓글 삭제 기능을 처리하는데 사용
export const useDeleteComment = (idCardId: number) => {
// QueryClient 인스턴스를 가져옴. 이 인스턴스를 사용해 쿼리 데이터를 읽고 쓸 수 있음
const queryClient = useQueryClient();
// 에러 메시지를 표시하는 훅
const { errorToast } = useToastMessageStore();
// useMutation 훅. 댓글 삭제 API를 호출하고 응답을 처리하는 함수를 생성
return useMutation({
// API 호출을 수행하는 함수. CommentDeleteRequest 형식의 데이터를 받아 deleteComment 함수에 전달
mutationFn: (commentInfo: CommentDeleteRequest) => deleteComment(commentInfo),
// 댓글 삭제 API 호출 전에 실행하는 함수
onMutate: async (commentInfo: CommentDeleteRequest) => {
// 댓글 및 댓글 수 관련 쿼리를 취소
await Promise.all([
queryClient.cancelQueries({ queryKey: commentQueryKey.comments(idCardId) }),
queryClient.cancelQueries({ queryKey: commentQueryKey.commentCount(idCardId) }),
]);
// 이전 쿼리 데이터를 가져옴
const previousComments = queryClient.getQueryData<CommentPages>(
commentQueryKey.comments(idCardId),
);
const previousCommentCount = queryClient.getQueryData<CommentCountGetResponse>(
commentQueryKey.commentCount(idCardId),
);
// 쿼리 데이터를 업데이트
const updatedComments = removeCommentToPages(previousComments, commentInfo.commentId);
queryClient.setQueryData(commentQueryKey.comments(idCardId), updatedComments);
const updatedCommentCount = decreaseCommentCount(previousCommentCount);
queryClient.setQueryData(commentQueryKey.commentCount(idCardId), updatedCommentCount);
// 이전 쿼리 데이터를 반환. onError에서 사용할 수 있도록 함
return { previousComments, previousCommentCount };
},
// API 호출에 오류가 발생했을 때 실행하는 함수
onError: (err, newComment, context) => {
// 이전 쿼리 데이터가 있으면
if (context?.previousComments !== undefined && context?.previousCommentCount !== undefined) {
// 에러를 토스트로 표시
const error = err as AxiosError;
errorToast(error.message);
// 쿼리 데이터를 이전 상태로 복구
queryClient.setQueryData(commentQueryKey.comments(idCardId), context.previousComments);
queryClient.setQueryData(
commentQueryKey.commentCount(idCardId),
context.previousCommentCount,
);
}
},
});
};
//
export type CommentDeleteRequest = {
idCardId: number;
commentId: number;
};
-
useQueryClient()를 가져와서, 이전에 실행한 댓글 및 댓글 수 관련 쿼리를 취소하고, 이전 쿼리 데이터를 가져왔다. -
cancelQueries: 쿼리를 취소하는 함수.queryKey를 전달하면 해당 쿼리를 취소한다. -
getQueryData: 쿼리 데이터를 가져오는 함수.queryKey를 전달하면 해당 쿼리 데이터를 가져온다. -
setQueryData: 쿼리 데이터를 업데이트하는 함수.queryKey와 업데이트할 데이터를 전달하면 해당 쿼리 데이터를 업데이트한다. -
계속 등장하는 queryKey인
CommentQueryKey.comments(idCardId)는 어디 있느냐?// src/app/planet/[communityId]/id-card/[idCardId]/page.tsx <HydrationProvider queryKey={commentQueryKey.comments(idCardId)} queryFn={getCommentsQuery} > <CommentList idCardId={idCardId} /> </HydrationProvider> -
HydrationProvider컴포넌트는useQuery훅을 사용해 쿼리 데이터를 가져오는 컴포넌트이다.queryKey와queryFn을 전달하면 해당 쿼리 데이터를 가져온다.
사용법
const { mutate: mutateDeleteComment } = useDeleteComment(idCardId);
const onClickToDeleteComment = () => {
mutateDeleteComment({ idCardId, commentId });
};
5. Typescript
option
-
onSuccess option을 props로 넘기는 코드
export const useDeleteMate = ( options: Pick< UseMutationOptions< Awaited<ReturnType<typeof deleteMate>>, unknown, Parameters<typeof deleteMate>[0] >, 'onSuccess' > ) => useMutation(deleteMate, options);
6. 궁금증
1. queryClient는 어떤 방식으로 구현이 되었을까?
queryClient
queryClient는 packages/query-core에 위치해있다. 이 queryClient는 react-query,solid-query,vue-query 등 다른 라이브러리 및 프레임워크에서도 공통적으로 쓰이는 듯하다.
export class QueryClient {
// ..
}
하나의 instance로 관리하고 있다.이 안에는 fetchQuery, resetQueries가 있다.
-
fetchQueryfetchQuery< TQueryFnData, TError = DefaultError, TData = TQueryFnData, TQueryKey extends QueryKey = QueryKey, TPageParam = never, >(): Promise<TData> { const defaultedOptions = this.defaultQueryOptions(options) if (typeof defaultedOptions.retry === 'undefined') { defaultedOptions.retry = false } const query = this.#queryCache.build(this, defaultedOptions) return query.isStaleByTime(defaultedOptions.staleTime) ? query.fetch(defaultedOptions) : Promise.resolve(query.state.data as TData) }인스턴스의 프로퍼티에서 defaultQueryOptions를 가져와서 defaultOptions에 할당한다. 그리고, stale시간지 지났는지 체크를 해서, 지났다면 query.fetch를 실행하고, 아니라면 저장된 query.state.data를
Promise.resolve메서드를 이용하여 결과값을 캐싱된 값을 리턴한다. -
resetQueries(filters?: QueryFilters, options?: ResetOptions): Promise<void> { const queryCache = this.#queryCache const refetchFilters: RefetchQueryFilters = { type: 'active', ...filters, } return notifyManager.batch(() => { queryCache.findAll(filters).forEach((query) => { query.reset() }) return this.refetchQueries(refetchFilters, options) }) }인스턴스의 프로퍼티에서 queryCache를 가져와서 모두 reset하는 함수이다. 그리고, refetchQueries를 실행하여 새로운 값을 가져온다.
2. QueryClientProvider는 어떻게 구현이 되어있을까?
QueryClientProvider
QueryClientProvider는 Context의 Provider로 queryClient를 모든 컴포넌트에 뿌려주는 Provider일 뿐이다.
-
export const QueryClientProvider = ({ client, children, }: QueryClientProviderProps): JSX.Element => { React.useEffect(() => { client.mount() return () => { client.unmount() } }, [client]) return ( <QueryClientContext.Provider value={client}> {children} </QueryClientContext.Provider> ) }
QueryClientProvider는 client를 전역으로 뿌려주고, 우리는 아래의 이 queryClient를 사용하여 캐싱된 쿼리값을 사용할 수 있는 것이다.
-
export const useQueryClient = (queryClient?: QueryClient) => { const client = React.useContext(QueryClientContext) if (queryClient) { return queryClient } if (!client) { throw new Error('No QueryClient set, use QueryClientProvider to set one') } return client } -
useQueryClient사용 예시export const usePostArticle = (groupId: number) => { const { replace } = useAppRouter(); const queryClient = useQueryClient(); return useMutation(postArticle, { onSuccess: (data) => { queryClient.invalidateQueries(GroupsKeys.getArticles(groupId)); queryClient.invalidateQueries(GroupsKeys.getNotices(groupId)); replace(`/grouping/${groupId}/articles/${data.articleId}`); }, }); };
3. useSuspenseQuery는 어떻게 구현을 햇을까?
useSuspenseQuery는 react의 Suspense와 함께 사용할 수 있는 훅이다. react-query v5에서 suspense옵션은 제거되었으며 따라서 Suspense와 함께 사용하기 위해서는 이 훅을 사용해야 한다. Suspense를 좀 더 타입 세이프하게 사용할 수 있다.
// useSuspenseQuery.ts
export function useSuspenseQuery(){
return useBaseQuery(
{
...options,
enabled: true,
suspense: true,
throwOnError: defaultThrowOnError,
},
QueryObserver,
queryClient,
) as UseSuspenseQueryResult<TData, TError>
}
사용자 경험을 개선할 수 있는 다양한 react-query의 방법
1. prefetching
예정된 다음 페이지에서 필요한 데이터를 미리 prefetch하면 된다. 이를테면 페이지네이션으로 구현한 페이지에서 말이다.
const prefetchNextPosts = async (nextPage: number) => {
const queryClient = useQueryClient();
// 해당 쿼리의 결과는 일반 쿼리들처럼 캐싱 된다.
await queryClient.prefetchQuery({
queryKey: ["posts", nextPage],
queryFn: () => fetchPosts(nextPage),
// ...options
});
};
// 단순 예
useEffect(() => {
const nextPage = currentPage + 1;
if (nextPage < maxPage) {
prefetchNextPosts(nextPage);
}
}, [currentPage]);
2. 낙관적 업데이트 (cancleQueries, setQueryData)
서버 업데이트 시 UI에서도 어차피 업데이트할 것이라고 가정해서 미리 UI를 업데이트 시켜주고, 서버를 통해 검증을 받고 업데이트 또는 롤백하는 방식
const useAddSuperHeroData = () => {
const queryClient = useQueryClient();
return useMutation({
mutateFn: addSuperHero,
onMutate: async () => {
await queryClient.cancelQueries(["super-heroes"]);
// 이전 값
const previousHeroData = queryClient.getQueryData(["super-heroes"]);
// 새로운 값으로 낙관적 업데이트 진행
queryClient.setQueryData(["super-heroes"], (oldData: any) => {
return {
...oldData,
data: [
...oldData.data,
{ ...newHero, id: oldData?.data?.length + 1 },
],
};
});
// 값이 들어있는 context 객체를 반환
return { previousHeroData };
},
// mutation이 실패하면 onMutate에서 반환된 context를 사용하여 롤백 진행
onError(error, hero, context: any) {
queryClient.setQueryData(["super-heroes"], context.previousHeroData);
},
// 오류 또는 성공 후에는 항상 refetch
onSettled() {
queryClient.invalidateQueries(["super-heroes"]);
},
});
};
낙관적 업데이트를 할 때, cancelQueries를 왜할까?
좋아요를 낙관적업데이트 했을 때의 프로세스이다.
- 사용자가 좋아요를 누른다.
- refetch를 실행하여 api 요청이 날라갈 것이다.
- cancleQueries를 실행해 refetch를 취소한다.
- cancleQueries를 실행하지 않다면, 만약 query데이터 요청이 setQueryData보다 일찍 일어났다면(그럴리 없을 것 같긴하다), setQueryData로 인해 낙관적 업데이트가 먼저 실행될 것이다.
- setQueryData를 사용하여 좋아요가 눌렸을 때 화면을 보여준다.
- onError에서는 만약 api실패했을 때 화면을 보여분다.
- onSettle에서는 api가 실패하든 성공하든 데이터를 재요청해서 정상적인 데이터를 보여준다.