Will find a way
[React] Tanstack Query (구: React Query) 본문
들어가기 전
리액트를 공부하거나 다뤄본 사람들은 React Query를 한번쯤 다뤄봤거나 다루게 될 것이다. 그만큼 리액트에서는 반드시 알아야 라이브러리다. 리액트 쿼리에 대해서 잘 모를 때 서버에서 데이터를 가져올 때 사용했기 때문에 리액트에서 사용하는 비동기라이브러리(?) 중 하나라고만 생각했다. 공부를 하고 적용을 하며 내가 생각한만큼 단순한 것이 아님을 알게 됐다. 내가 가지고 있는 잘못된 개념을 다시 한번 정리하고자 이 글을 포스팅 하게 됐다.
Tanstack Query 란?
Tanstack Query는 서버에서 데이터를 가져오기, 데이터 캐싱, 캐시 제어 등 데이터를 효율적으로 관리할 수 있는 라이브러리다. 원래는 React Query라는 이름을 사용하였으나 Vue나 다른 곳에서도 사용할 수 있게 되면서 React Query에서 Tanstack Query로 이름을 변경하였다.
'데이터를 관리한다.' 라는 말을 생각하면서
이 글을 읽으면 조금 더 도움이 될 것이다.
사용 이유
React는 클라이언트 상태 관리에만 집중되어 있다.
- 서버 상태는 언제 바뀔지 모른다 (외부 API, DB 등)
- 직접 useEffect + axios + useState 조합으로 짜면 코드가 복잡해지고 재사용도 어려움
- 캐시, refetch, 로딩 처리 등 반복적인 작업이 많음
위와 같은 것들을 자동으로 처리해주는 도구라고 생각하면 된다.
사용 예시
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
const fetchUser = async () => {
const { data } = await axios.get('/api/user')
return data;
};
export const UserComponent = () => {
const {} = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
});
if (isLoading) return <p>로딩 중...</p>;
if (isError) return <p>에러 발생</p>;
return <div>사용자 이름: {data.name}</div>;
}
핵심 기능 및 주요 훅
useQuery (Read)
Tanstack Query 에서 사용하는 주요 훅으로
컴포넌트에서 데이터를 가져올 때 사용, 비동기 캐싱 + 자동갱신 + 로딩/에러 관리
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
enabled: !!userId,
staleTime: 1000 * 60 * 5, // 5분간 fresh
});
자주 사용하는 옵션
옵션 | 설명 |
queryKey | 쿼리 고유 키 (캐싱 기준) |
queryFn | 데이터를 불러오는 함수 |
enabled | 자동 실행 여부 |
staleTime | 데이터가 fresh 상태 유지 시간 |
select | 반환값 가공 |
refetchOnWindowFocus | 창 복귀 시 리패치 여부 |
retry | 실패 시 재시도 횟수 |
onSuccess, onError | 성공/실패 콜백 |
useMutation - 데이터 수정하기 (Create / Update / Delete)
- 기본 용도: 서버에 데이터를 생성/수정/삭제할 때 사용
- 캐싱 X, 대신 성공 시 쿼리 무효화 (invalidate)
const userId = 42; // 여기서 정의됨
const mutation = useMutation({
mutationFn: ({ userId, name }) =>
axios.put(`/users/${userId}`, { name }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['user', userId] });
},
});
const handleSave = () => {
mutation.mutate({ userId, name: 'Jerome' }); // 위의 userId가 사용됨
};
자주 쓰는 옵션
옵션 | 설명 |
mutationFn | 서버 요청 함수 |
onSuccess | 성공 후 캐시 무효화 등 처리 |
onError | 실패 시 알림 등 처리 |
onSettled | 성공/실패 모두 후처리 |
useInfiniteQuery - 무한 스크롤 (페이지네이션)
사용 예시
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['posts'],
queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
getNextPageParam: (lastPage, allPages) => {
return lastPage.hasNext ? allPages.lenth + 1 : undefined;
},
});
반환 값
반환 값 | 설명 |
data | 여러 페이지 데이터를 담은 객체 (pages, pageParams) |
fetchNextPage | 다음 페이지 요청 |
fetchPreviousPage() | 이전 페이지 요청 |
hasNextPage | 다음 페이지 있는지 여부 (getNextPageParam에 따라 결정됨) |
hasPreviousPage | 이전 페이지가 있는지 여부 (getPreviousPageParam에 따라 결정됨) |
isFetchingNextPage | 다음 페이지 로딩 중인지 여부 |
isFetchingPreviousPage | 이전 페이지 로딩 중인지 여부 |
refetch() | 전체 refetch |
자주 쓰는 옵션
옵션 | 설명 |
queryKey | 캐시 키, 일반 쿼리처럼 사용 |
queryFn | - 데이터를 불러오는 함수. 인자로 { pageParam }을 받음 - pagaParam은 getNextPageParam에서 반환한 값 |
getNextPageParam(lastPage, allPages) | 다음 페이지를 어떻게 불러올지 알려주는 함수 - lastPage: 직전에 불러온 페이지 데이터 - allPages: 지금까지 불러온 모든 페이지 |
getPreviousPageParam | 이전 페이지가 있는 경우 정의 (선택사항) |
initialPageParam | 초기 pageParam 값 (기본값은 undefined) |
queryClient - 전역 캐시 관리
import { useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
함수
함수 | 설명 |
invalidateQueries(queryKey) | 쿼리 무효화 (-> refetch 유도) |
refetchQueries(queryKey) | 강제 리패치 |
removeQueries(queryKey) | 쿼리 캐시 삭제 |
setQueryData(queryKey, data) | 수동으로 캐시 변경 |
getQueryData(queryKey) | 캐시 값 가져오기 |
캐싱, 리패칭, 무효화 용어 요약
캐싱 (cahing) : 서버에서 받아온 데이터를 저장하여 다시 사용할 수 있게 함
무효화 (invalidate) : 기존 캐시를 오래됨(stale) 으로 표시해 다음에 refetch 유도
리패칭 (refetch) : 데이터를 새로 요청하여 업데이트 (자동 or 수동)
useInfiniteQuery 사용 예제
import { useInfiniteQuery } from '@tanstack/react-query';
import axios from 'axios';
const fetchPosts = async (page: number) => {
// page 번호에 따라 게시글 데이터를 받아오는 API 호출
const res = await axios.get(`/api/posts?page=${page}`);
return res.data; // 응답 데이터 반환
};
const PostList = () => {
// useInfiniteQuery 훅 사용
const {
data, // 전체 페이지 데이터를 담고 있는 객체
fetchNextPage, // 다음 페이지를 가져오는 함수
hasNextPage, // 다음 페이지가 존재하는지 여부 (getNextPageParam이 반환값으로 결정)
isFetchingNextPage, // 다음 페이지를 불러오는 중인지 여부
isLoading, // 최초 로딩 여부
isError, // 에러 발생 여부
error, // 에러 객체
} = useInfiniteQuery({
queryKey: ['posts'], // 캐시 키 (고유한 요청 식별자)
queryFn: async ({ pageParam = 1 }) => {
// 데이터를 가져오는 함수. pageParam은 자동으로 넘겨짐 (없ㅁ으면 기본 값1)
return await fetchPosts(pageParam);
},
getNextPageParam: (lastPage, allPages) => {
// 다음 페이지를 불러오기 위한 pageParam을 리턴
// 예 : 응답 객체에 nextPage가 있으면 그걸 반환, 없으면 undefined로 종료
return lastPage.hasNext ? lastPage.nextPage : undefined;
},
initialPageParam: 1, // 초기 pageParam (기본 페이지 번호)
});
// 로딩 중이면 로딩 메시지 표시
if (isLoading) return <div>Loading...</div>;
// 에러 발생 시 에러 메시지 출력
if (isError) return <div>Error: {error.message}</div>;
return (
<div>
{/* 여러 페이지를 반복하며 렌더링 */}
{data?.pages.map((page, index) => (
<div key={index}>
{page.posts.map((post: any) => (
<div key={post.id}>{post.title}</div>
))}
</div>
))}
{/* 여러 페이지를 반복하며 렌더링 */}
{hasNextPage && (
<button
onClick={() => fetchNextPage()} // 다음 페이지 요청
disabled={isFetchingNextPage} // 요청 중이면 버튼 비활성화
>
{isFEtchingNextPage ? 'Loading more...' : 'Load More'}
</button>
)}
</div>
);
};
export default PostList;
마무리
Tanstack Query를 사용할 때 유독 queryKey가 어떤건지 유독 헷갈렸다. 캐싱과 리페칭 개념을 공부하면서 비로소 리액트쿼리가 뭔지 조금 보이기 시작했다. 글을 쓰면서 부족한 부분은 차후에 추가나 수정 할 예정이다.
'FrontEnd > React' 카테고리의 다른 글
navigate / Navigate(redirect) 그냥 페이지 이동 아니야? (0) | 2025.06.20 |
---|---|
React-Hook-Form(RHF) : Form을 편하게 관리해보자 (Feat. Zod) (0) | 2025.05.11 |
상태관리 : 약관동의 구현 (모든 약관, 필수약관) / TypeScript, JS 메서드 (0) | 2025.05.06 |
새로운 라우터가 등장했다고? (createBrowserRouter) (0) | 2025.04.10 |
[React] Zustand 에 대해서 (React 상태관리) (0) | 2025.04.01 |