TanStack Query (React Query)
React 애플리케이션에서 서버 데이터를 다루려면 API 호출, 로딩 상태, 에러 처리, 캐싱, 데이터 갱신 등 많은 것을 관리해야 한다. 이런 로직을 useState와 useEffect로 직접 구현하면 코드가 길어지고 반복이 많아진다. TanStack Query(구 React Query)는 서버 상태 관리를 위한 라이브러리로, 이런 복잡함을 선언적인 훅으로 해결한다.
클라이언트 상태 vs 서버 상태
상태관리를 이해하려면 먼저 상태의 종류를 구분해야 한다.
클라이언트 상태는 사용자의 브라우저에서만 존재하는 데이터다. 모달이 열려 있는지, 사이드바가 접혀 있는지, 어떤 탭이 선택되어 있는지 같은 UI 상태가 여기에 해당한다. 이 데이터의 소유자는 클라이언트이며, 다른 사용자와 공유되지 않는다.
서버 상태는 서버(DB)에 원본이 있는 데이터다. 매니저 목록, 매장 정보, 서비스 설정 같은 비즈니스 데이터가 여기에 해당한다. 이 데이터는 다른 사용자가 언제든 변경할 수 있으므로, 내가 가지고 있는 사본이 오래되었을 수 있다(stale).
클라이언트 상태 → Zustand (동기적, 내가 소유, 항상 최신)
서버 상태 → TanStack Query (비동기적, 서버가 소유, 오래될 수 있음)
TanStack Query는 서버 상태의 특성인 비동기성, 캐싱, 자동 갱신, 에러/로딩 처리를 전담하도록 설계되었다.
useQuery: 서버 데이터 조회
useQuery는 서버에서 데이터를 가져오는 훅이다. 컴포넌트가 마운트되면 자동으로 API를 호출하고, 로딩 상태와 에러를 함께 관리한다.
클라이언트 상태 → Zustand (동기적, 내가 소유, 항상 최신)
서버 상태 → TanStack Query (비동기적, 서버가 소유, 오래될 수 있음)
queryKey는 이 쿼리를 식별하는 고유한 키다. 배열 형태로 지정하며, TanStack Query는 이 키를 기준으로 캐싱과 갱신을 관리한다. queryFn은 실제 데이터를 가져오는 비동기 함수다.
컴포넌트에서는 이렇게 사용한다.
import { useQuery } from '@tanstack/react-query';
const managersQueryKey = ['managers'];
export const useManagersQuery = () => {
return useQuery({
queryKey: managersQueryKey,
queryFn: async (): Promise<Manager[]> => {
const response = await api.http.instance.get<MemberResponse[]>(
'/admin/members',
);
return response.data.map(toManager);
},
});
};
useQuery가 반환하는 isLoading, error, data를 사용하면 별도의 useState로 로딩 상태나 에러를 관리할 필요가 없다. 컴포넌트는 데이터의 세 가지 상태(로딩, 에러, 성공)를 깔끔하게 처리할 수 있다.
useMutation: 서버 데이터 변경
useMutation은 서버의 데이터를 변경(생성, 수정, 삭제)하는 훅이다. useQuery와 달리 자동으로 실행되지 않고, mutate 함수를 호출해야 실행된다.
function ManagerListPage() {
const { data, isLoading, error } = useManagersQuery();
if (isLoading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return (
<ul>
{data?.map((manager) => (
<ManagerCard key={manager.id} manager={manager} />
))}
</ul>
);
}
mutationFn은 실제 API 호출을 수행하는 함수다. onSuccess는 mutation이 성공한 후 실행되는 콜백으로, 여기서 관련 쿼리를 무효화(invalidate)하는 것이 일반적인 패턴이다.
컴포넌트에서는 이렇게 사용한다.
import { useMutation, useQueryClient } from '@tanstack/react-query';
export const useDeleteManagerMutation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
await api.http.instance.delete(`/admin/members/${id}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: managersQueryKey });
},
});
};
mutate를 호출하면 mutation이 실행되고, isPending으로 진행 상태를 확인할 수 있다.
useQuery vs useMutation
두 훅의 핵심 차이는 실행 시점과 용도다.
useQuery → 조회(GET) → 컴포넌트 마운트 시 자동 실행 → queryKey로 캐싱
useMutation → 변경(POST/PUT/DELETE) → mutate() 호출 시 수동 실행 → 캐시 무효화
useQuery는 "이 데이터를 보여줘"라는 선언이고, useMutation은 "사용자가 이 버튼을 누르면 이 작업을 해"라는 명령이다.
queryKey: 캐시의 핵심
queryKey는 TanStack Query의 캐싱 시스템에서 가장 중요한 개념이다. 같은 queryKey를 사용하는 쿼리는 같은 캐시 데이터를 공유한다.
function ManagerActions({ managerId }: { managerId: string }) {
const deleteMutation = useDeleteManagerMutation();
const handleDelete = () => {
deleteMutation.mutate(managerId);
};
return (
<button
onClick={handleDelete}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? '삭제 중...' : '삭제'}
</button>
);
}
키를 배열로 구성하는 이유는 계층적인 무효화가 가능하기 때문이다. ['managers']를 무효화하면 ['managers']뿐 아니라 ['managers', 'abc-123'] 같은 하위 키도 함께 무효화된다.
useQuery → 조회(GET) → 컴포넌트 마운트 시 자동 실행 → queryKey로 캐싱
useMutation → 변경(POST/PUT/DELETE) → mutate() 호출 시 수동 실행 → 캐시 무효화
무효화(invalidate)는 해당 캐시 데이터를 "오래됨(stale)"으로 표시한다. 화면에 해당 데이터를 사용하는 컴포넌트가 있으면 자동으로 다시 fetch한다. 화면에 없으면 다음에 해당 컴포넌트가 마운트될 때 새 데이터를 가져온다.
로그인에서의 useMutation 활용
로그인도 서버에 요청을 보내는 "변경" 작업이므로 useMutation을 사용한다.
// 매니저 목록
queryKey: ['managers']
// 특정 매니저 상세
queryKey: ['managers', managerId]
여기서 onSuccess는 queryClient를 사용하지 않고 Zustand 스토어의 setAuth를 호출한다. 로그인 응답으로 받은 토큰과 사용자 정보를 클라이언트 상태(Zustand)에 저장하는 것이다. 이처럼 mutation의 onSuccess에서 서버 상태 갱신(invalidateQueries)뿐 아니라 클라이언트 상태 갱신(Zustand set)도 할 수 있다.
요약
TanStack Query는 서버 상태를 관리하기 위한 라이브러리다. useQuery로 데이터를 조회하면 로딩, 에러, 캐싱이 자동으로 처리되고, useMutation으로 데이터를 변경한 뒤 invalidateQueries로 관련 캐시를 갱신한다. queryKey는 캐시를 식별하는 핵심 개념으로, 계층적 구조를 활용하면 관련 데이터를 효율적으로 관리할 수 있다. 클라이언트 상태는 Zustand에, 서버 상태는 TanStack Query에 맡기는 것이 현대 React 앱의 일반적인 패턴이다.