Tanstack-query를 사용하는 큰 이유중에 하나는 캐싱 관리이다.
나는 기존에 진행하던 사이드 프로젝트에서 쿼리키를 다음과 같이 관리하고 있었다.


const MEETING_QUERY_KEYS = {
topMeetings: (category: string) => ['topMeetings', category] as const,
meetings: (category: string) => ['meetings', category] as const,
};
staleTime을 1분으로 설정해두었는데, 1분 이내에 페이지를 이동해도 추천 모임은 캐싱된 데이터를 사용하기 때문에 API 호출이 안되는데,
모임 목록쪽은 계속 API가 호출되는것으로 보아 모임 목록 캐싱이 잘 되지 않는걸 알 수 있었다.

그 이유는 추천 모임과 모임 목록의 useQuery 쪽을 보면 알 수 있었다.
const useTopMeetings = (category: CategoryTitle, options = {}) => {
return useQuery({
queryKey: MEETING_QUERY_KEYS.topMeetings(category),
queryFn: () => getTopMeetings(category),
...options,
});
};
const useInfiniteSearchMeetings = (
category: CategoryTitle,
searchQueryObj: IMeetingSearchCondition,
option = {},
) => {
return useInfiniteQuery({
queryKey: MEETING_QUERY_KEYS.meetings(category),
queryFn: ({ pageParam = 0 }) =>
getMeetings(pageParam, category, searchQueryObj),
initialPageParam: 0,
getNextPageParam: (lastPage) => {
return lastPage?.nextCursor;
},
...option,
});
};
추천 모임을 불러오는 useTopMeetings 에서는 쿼리키로 MEETING_QUERY_KEYS.topMeetings(category) 을 사용하고 있고 queryFn에서도 category만 인자로 보내주어 API 요청시 category만 쿼리로 사용되고 있다.
하지만, 모임 목록을 불러오는 useInfiniteSearchMeetings에서는 쿼리키로 MEETING_QUERY_KEYS.meetings(category)을 사용하고 있지만, queryFn에서는 category와 searchQueryObj를 인자로 보내주고 API 요청 시 두가지 다 사용되고 있다.
캐싱이 정상적으로 작동하기 위해서는 캐싱 기준에 영향을 주는 모든 변수가 포함되어야 하는데, 나는 searchQueryObj 가 쿼리키에 사용되지 않고 있어 이러한 문제가 발생하는 것이었다.
위 내용은 공식문서에 잘 나와있다. (공식문서를 잘 읽자 🥲)

그래서 searchQueryObj도 쿼리키에 추가해주었다.
const getSortedSearchQuery = (
searchQueryObj: IMeetingSearchCondition,
): IMeetingSearchCondition => ({
...searchQueryObj,
skillArray: [...searchQueryObj.skillArray].sort(),
});
const MEETING_QUERY_KEYS = {
topMeetings: (category: string) => ['topMeetings', category] as const,
meetings: (category: string, searchQueryObj: IMeetingSearchCondition) => {
const sortedSearchQueryObj = getSortedSearchQuery(searchQueryObj);
return ['meetings', category, sortedSearchQueryObj] as const;
},
meetingId: (
id: string,
category: string,
searchQueryObj: IMeetingSearchCondition,
) =>
[
...MEETING_QUERY_KEYS.meetings(
category,
getSortedSearchQuery(searchQueryObj),
),
id,
] as const,
};
const useInfiniteSearchMeetings = (
category: CategoryTitle,
searchQueryObj: IMeetingSearchCondition,
option = {},
) => {
return useInfiniteQuery({
queryKey: MEETING_QUERY_KEYS.meetings(category, searchQueryObj),
queryFn: ({ pageParam = 0 }) =>
getMeetings(pageParam, category, searchQueryObj),
initialPageParam: 0,
getNextPageParam: (lastPage) => {
return lastPage?.nextCursor;
},
...option,
});
};
searchQueryObj의 skillArray는 배열이기 때문에 같은 값인데 순서가 바뀌어서 들어올 경우를 고려해 정렬된 값을 쿼리에 담아주었다.
이제 캐싱이 잘 작동해서 1분 이내에 페이지 이동시 캐싱된 데이터가 잘 사용될까?

그 이유는 내가 개발중인 데빙 사이트에서는 사진과 같이 검색어, 기술스택, 정렬로 검색 쿼리를 사용하고 있는데

검색 쿼리가 바뀔때마다 useEffect로 쿼리를 지우고 API 를 refetch 해주는 로직을 따로 넣어놨었다.
const queryClient = useMemo(() => new QueryClient(), []);
// 필터에 따른 재검색
useEffect(() => {
queryClient.removeQueries({ queryKey: [MEETING_QUERY_KEYS.meetings] });
refetch();
}, [queryClient, searchQuery, refetch]);
그러다보니 페이지 이동시 저 useEffect의 콜백이 발생해 쿼리키를 잘 생성해 놓았어도 API가 refetch 되는 것이었다.
저 로직은 이제 더 이상 필요 없는 로직이다.
왜냐하면, 쿼리키만 잘 짜놓는다면 tanstack-query에서 쿼리키가 변경되면 알아서 refetch를 해주기 때문 …! 😲
그래서 해당 부분을 지우고 다시 확인을 해보았다.
정상적으로 데이터가 캐싱되어 더 이상 API가 호출되지 않음을 알 수 있었다.

근데 tanstack-query에서 자동으로 refetch 해주니, 검색쿼리 변경시에 상태가 isLoading 될 때마다
스켈레톤 UI가 잠깐 나타났다가 사라지면서 UX가 좋지 않았다.

그래서 이전의 데이터를 유지해주는 옵션을 적용했더니, 검색 쿼리 변경시에는 스켈레톤 UI 없이 변경되어
시각적으로 피로도가 훨씬 줄어들어 UX를 개선시킬 수 있었다.
import { keepPreviousData } from '@tanstack/react-query';
{
placeholderData: keepPreviousData,
},

tanstack-query를 사용하면서 디테일한 쿼리키를 두어 캐싱 전략을 잘 짜놓으면 페이지 로드 시간이나 불필요한 API 호출을 줄여 네트워크 요청 최적화가 가능하기때문에 앞으로도 쿼리키 생성 시 좀 더 신경써야겠다고 느꼈다!
✨참고 자료✨
- https://tanstack.com/query/v4/docs/framework/react/overview
'Study' 카테고리의 다른 글
| [Next.js] redirects() 사용 시 페이지가 작동하지 않습니다. 이슈 해결하기 (0) | 2025.03.09 |
|---|---|
| [Next 14] Next.js 프로젝트에 테스트 환경 구축하기 (jest, react-testing-library) (1) | 2025.02.10 |
| [Study] Tanstack Query v5 알아보기 (0) | 2024.11.11 |
| [React] Eslint 오류 해결하기 (no-else-return, jsx-a11y) (0) | 2024.10.15 |
| [React + TS] useRef 사용시 The expected type comes from property 'ref' which is declared here on type 에러 (+ useRef 3가지 정의) (1) | 2024.09.05 |