
저번 글에서 useQuery를 다뤘으니 이번 글에서는 디프만 프로젝트인 "Critix"에서 사용한 useQuery 이외에 다른 기능들에 대해 정리해보겠습니다.
useMutation은 CUD 작업, 즉 서버에 데이터를 post, delete, patch, put 과 같이 보내거나 수정해줘야 할 때 사용하는 훅입니다.
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { axiosInstance } from '@/common/services/service-config';
import { Response } from '@/common/types/response';
// 뼈대 코드
export const useMyMutation = () => {
const queryClient = useQueryClient();
return useMutation({
// 필수 X : Devtools 추적 등 용도
mutationKey: ['myMutationKey'],
// 필수 : 실제 api 로직
mutationFn: async (payload: YourPayloadType) => {
const { data } = await axiosInstance.post<Response<YourResultType>>(
'/api/v1/some-endpoint',
payload
);
return data.result;
},
// mutationFn 함수가 실행되기 전에 실행되며, mutation 함수가 받을 동일한 변수가 전달됨
// 낙관적 업데이트가 필요한 로직에서 자주 사용됨
onMutate() {
/* ... */
},
// 쿼리가 성공했을 때의 로직 처리 => 관련 쿼리 무효화 또는 상태 업데이트
onSuccess: (data) => {
// query가 오래 되었음을 판단하고 refetch하는 로직
queryClient.invalidateQueries({ queryKey: ['someQueryKeyToRefetch'] });
console.log('Mutation success:', data);
},
// 쿼리가 실패했을 때의 로직 처리
onError: (error) => {
console.error('Mutation error:', error);
},
// finally와 비슷하게 요청이 성공하든 실패하든 상관없이 마지막에 실행됨
onMutate() {
/* ... */
},
});
};
인터넷 속도가 느리거나 서버가 느릴 때, 유저가 어떠한 액션을 하면 서버와의 통신 후 화면이 업데이트 되기 때문에 체감상 느리게 작동하여 UX에 좋지 않습니다.
이러한 문제를 해결하기 위해 미리 UI를 업데이트 하고, 요청의 결과가 오면 해당 결과에 맞게 UI를 업데이트 or 롤백하는 방식입니다.
- 좋아요 기능에 많이 적용합니다.
onMutate 함수
onMutate: async (newTodo) => {
await queryClient.cancelQueries({ queryKey: ['todos'] })
const previousTodos = queryClient.getQueryData(['todos'])
queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
return { previousTodos }
},
onError 함수
onError: (err, newTodo, context) => {
queryClient.setQueryData(['todos'], context.previousTodos)
},
onSettled 함수
onSettled: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
여러 개의 Query들을 한번에 관리할 수 있게 도와주는 훅입니다.
각각의 쿼리를 배열 형태로 작성하고, 병렬적으로 요청하며 결과를 개별적으로 반환해줍니다.
userIds.map((id) => {
const { data } = useQuery({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
});
});
위의 코드와 같이 useQuery를 배열 내에서 작성하면
React Hook "useQuery" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function.
라는 빨간 줄이 출력됩니다. 이유는 조건문이나 반복문 내에서는 React Hook을 사용하면 안되기 때문입니다.
위와 같은 상황에서 useQueries를 사용하는 것입니다.
import { useQueries } from '@tanstack/react-query';
import axios from 'axios';
const fetchUser = (id: number) => axios.get(`/api/users/${id}`).then(res => res.data);
export const UserList = ({ userIds }: { userIds: number[] }) => {
const queries = useQueries({
queries: userIds.map((id) => ({
queryKey: ['user', id],
queryFn: () => fetchUser(id),
})),
});
};
위와 같이 반복문 내에서 사용하려면 useQueries를 사용해야 합니다!!
이 외에도 코드 간결화, 상태 추적의 어려움을 해결해줍니다.
[
{
data,
isPending,
isError,
error,
},
]
위와 같이 배열로 결과를 반환해주기 때문에 queries[0].data 로 data에 접근할 수 있습니다.
suspense : true 가 기본적으로 지원되는 훅으로 로딩 상태는 Suspense 컴포넌트, 에러 상태는 ErrorBoundary 컴포넌트로 대체되기 때문에 data가 항상 정의된 것을 보장받습니다.
- 부모 컴포넌트에서 ErrorBoundary로 감싸주어야 합니다.
- useSuspenseQuery 는 enabled 옵션을 지원하지 않습니다.
- Suspense로 감싸고 있는 하나의 컴포넌트에서 2개 이상의 요청을 진행하면 병렬적으로 처리하지 않아 네트워크 병목 현상이 발생할 수 있습니다. (관심사 분리가 중요..!)
export const useGetFeedbackHistory = () => {
const endPoint = '/api/v1/feedback/list';
return useSuspenseQuery<UseGetFeedbackHistoryResponse>({
queryKey: [endPoint],
queryFn: () => axiosInstance.get(endPoint).then((res) => res.data),
});
};
// RecentFeedbackModalContent 컴포넌트 안에서 useGetFeedbackHistory 훅을 호출합니다.
// 즉 useSuspenseQuery를 호출하는 컴포넌트 상위에서 FallbackBoundary(QueryErrorResetBoundary + ErrorBoundary)로 감싸줍니다.
<FallbackBoundary suspense={fallbacks.suspense} error={fallbacks.error}>
<RecentFeedbackModalContent closeModal={closeModal} />
</FallbackBoundary>
1. Suspense mount
2. MainComponent mount
3. MainComponent에서 useSuspenseQuery 훅을 사용하여 비동기 데이터 요청
4. MainComponent unmount, fallback UI인 Loader mount
5. 비동기 데이터 요청이 완료되면 fallback UI인 Loader unmount
6. MainComponent mount
QueryClient의 인스턴스를 사용하여 캐시와 상호작용할 수 있게 도와주는 훅입니다.
QueryClient 를 활용하여 기존 쿼리의 캐시된 데이터를 가져오거나, 캐싱된 쿼리 데이터를 즉시 업데이트 하고, 최신화할 수 있습니다.
1. invalidateQueries
특정 queryKey의 데이터를 stale 상태로 만들고 다음 렌더링 시 refetch 하게 유도합니다.
const queryClient = useQueryClient();
queryClient.invalidateQueries({ queryKey: ['todos'] });
2. setQueryData
특정 query의 캐시 데이터를 직접 수정합니다.
queryClient.setQueryData(['user', 1], { id: 1, name: 'Lee' });
3. getQueryData
현재 캐시된 데이터를 조회합니다. (리렌더 없이 데이터 확인 가능)
const user = queryClient.getQueryData(['user', 1]);
4. refetchQueries
지정된 쿼리를 즉시 refetch합니다.
invalidateQueries는 stale로만 만들고, refetch는 React lifecycle에 따라 자동 발생하지만, refetchQueries는 즉시 수행하는 차이점이 있습니다.queryClient.refetchQueries({ queryKey: ['todos'] });
5. cancelQueries
실행 중인 요청을 취소합니다. (주로 onMutate 등에서 낙관적 업데이트를 진행할 때 사용)
await queryClient.cancelQueries({ queryKey: ['todos'] });
6. resetQueries
쿼리 상태를 초기화합니다. (캐시 초기화 + status 초기화)
queryClient.resetQueries({ queryKey: ['user', 1] });
resetQueries / invalidateQueries / refetchQueries 차이점
| 메서드 | 상태 초기화 | 캐시 삭제 | 서버 요청 | 주로 쓰는 상황 |
|---|---|---|---|---|
resetQueries | O | O | O | 에러 후 초기화, 완전 리셋 |
invalidateQueries | X | X | X (다음 렌더링 시 요청) | 데이터 변경 후 무효화 |
refetchQueries | X | X | O | 수동으로 재요청만 하고 싶을 때 |
여기까지가 디프만에서 진행했던 프로젝트인 “Critix” 에서 사용한 Tanstack Query 에서 제공하는 훅들입니다.
정리해보니 프로젝트에서 사용한 기능보다 더 다양하고 유용한 기능이 존재했고, 해당 기능들을 활용하여 로직을 더욱 개선할 예정입니다!!
Tanstack Query에서 제공하는 훅 중 무한 스크롤을 도와주는 useInfiniteQuery훅도 존재하는데, 아직 무한스크롤 기능을 구현해본 경험이 없어 추후에 따로 정리할 계획입니다.
https://github.com/ssi02014/react-query-tutorial?tab=readme-ov-file#usemutation