React 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는 클라이언트 상태용으로 함께 사용하는 것이 좋습니다.