Contents
see ListReact Query(TanStack Query)로 서버 상태 관리
React Query는 서버 상태(서버에서 가져온 데이터)를 효율적으로 관리하는 라이브러리입니다. 캐싱, 동기화, 백그라운드 업데이트를 자동으로 처리합니다.
언제 사용하나요?
- API에서 데이터를 가져와 캐싱할 때
- 데이터 자동 갱신(재검증)이 필요할 때
- 페이지네이션, 무한 스크롤 구현
- 낙관적 업데이트(Optimistic Update)
- 로딩/에러 상태 관리 간소화
설치
npm install @tanstack/react-query
npm install @tanstack/react-query-devtools초기 설정
// App.jsx
import { QueryClient, QueryClientProvider } from \047@tanstack/react-query\047;
import { ReactQueryDevtools } from \047@tanstack/react-query-devtools\047;
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1분간 fresh
gcTime: 1000 * 60 * 5, // 5분간 캐시 유지
retry: 1, // 실패시 1회 재시도
refetchOnWindowFocus: false,
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<MyApp />
<ReactQueryDevtools />
</QueryClientProvider>
);
}useQuery - 데이터 조회
import { useQuery } from \047@tanstack/react-query\047;
function UserList() {
const { data, isLoading, isError, error, refetch } = useQuery({
queryKey: [\047users\047],
queryFn: async () => {
const response = await fetch(\047/api/users\047);
if (!response.ok) throw new Error(\047Network error\047);
return response.json();
},
});
if (isLoading) return <div>로딩 중...</div>;
if (isError) return <div>에러: {error.message}</div>;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}queryKey - 캐시 키
// 배열 형태로 캐시 키 지정
useQuery({ queryKey: [\047users\047], ... }); // 전체 목록
useQuery({ queryKey: [\047users\047, userId], ... }); // 특정 유저
useQuery({ queryKey: [\047users\047, { status: \047active\047 }], ... }); // 필터useMutation - 데이터 변경
import { useMutation, useQueryClient } from \047@tanstack/react-query\047;
function CreateUser() {
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (newUser) => {
const response = await fetch(\047/api/users\047, {
method: \047POST\047,
body: JSON.stringify(newUser),
});
return response.json();
},
onSuccess: () => {
// 성공 시 users 쿼리 무효화 (재조회)
queryClient.invalidateQueries({ queryKey: [\047users\047] });
},
});
const handleSubmit = () => {
mutation.mutate({ name: \047홍길동\047 });
};
return (
<button
onClick={handleSubmit}
disabled={mutation.isPending}
>
{mutation.isPending ? \047저장 중...\047 : \047저장\047}
</button>
);
}낙관적 업데이트 (Optimistic Update)
const mutation = useMutation({
mutationFn: updateUser,
onMutate: async (newData) => {
// 진행 중인 쿼리 취소
await queryClient.cancelQueries({ queryKey: [\047user\047, id] });
// 이전 데이터 백업
const previousUser = queryClient.getQueryData([\047user\047, id]);
// 낙관적으로 캐시 업데이트
queryClient.setQueryData([\047user\047, id], newData);
return { previousUser };
},
onError: (err, newData, context) => {
// 에러 시 롤백
queryClient.setQueryData([\047user\047, id], context.previousUser);
},
onSettled: () => {
// 완료 후 재조회로 동기화
queryClient.invalidateQueries({ queryKey: [\047user\047, id] });
},
});페이지네이션
function UserList({ page }) {
const { data, isPlaceholderData } = useQuery({
queryKey: [\047users\047, page],
queryFn: () => fetchUsers(page),
placeholderData: keepPreviousData, // 이전 데이터 유지
});
return (
<div style={{ opacity: isPlaceholderData ? 0.5 : 1 }}>
{data?.users.map(user => ...)}
</div>
);
}무한 스크롤
import { useInfiniteQuery } from \047@tanstack/react-query\047;
function InfiniteUsers() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useInfiniteQuery({
queryKey: [\047users\047],
queryFn: ({ pageParam = 0 }) => fetchUsers(pageParam),
getNextPageParam: (lastPage) => lastPage.nextCursor,
});
return (
<>
{data?.pages.map(page =>
page.users.map(user => <UserCard user={user} />)
)}
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
더 보기
</button>
</>
);
}주요 옵션
| 옵션 | 설명 |
|---|---|
| staleTime | 데이터가 fresh로 유지되는 시간 |
| gcTime | 비활성 캐시 유지 시간 |
| refetchOnWindowFocus | 창 포커스 시 재조회 |
| retry | 실패 시 재시도 횟수 |
| enabled | 쿼리 활성화 여부 |
vs Redux/Zustand
React Query는 서버 상태용, Redux/Zustand는 클라이언트 상태용으로 함께 사용하는 것이 좋습니다.