[React] TanStack Query
개요
TanStack Query (구 React Query)는 서버에서 가져오는 데이터(서버 상태)를 관리하는 라이브러리입니다. API 호출, 캐싱, 로딩/에러 처리, 자동 갱신 등을 간편하게 처리합니다.
설치
npm install @tanstack/react-query
초기 설정
// src/main.tsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1분간 데이터를 최신으로 간주
retry: 1, // 실패 시 1회 재시도
},
},
});
ReactDOM.createRoot(document.getElementById("root")!).render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
);
useQuery - 데이터 조회 (GET)
import { useQuery } from "@tanstack/react-query";
interface Post {
id: number;
title: string;
body: string;
userId: number;
}
// API 함수 분리 (권장)
const fetchPosts = async (): Promise<Post[]> => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10");
if (!res.ok) throw new Error("데이터를 불러오지 못했습니다");
return res.json();
};
function PostList() {
const {
data: posts,
isLoading,
isError,
error,
refetch,
} = useQuery({
queryKey: ["posts"], // 캐시 키 (배열로 지정)
queryFn: fetchPosts, // API 함수
});
if (isLoading) return <p>로딩 중...</p>;
if (isError) return <p>오류: {(error as Error).message}</p>;
return (
<div>
<button onClick={() => refetch()}>새로고침</button>
<ul>
{posts?.map((post) => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
동적 쿼리 키 (ID 기반)
const fetchPost = async (id: number): Promise<Post> => {
const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
return res.json();
};
function PostDetail({ postId }: { postId: number }) {
const { data: post, isLoading } = useQuery({
queryKey: ["posts", postId], // postId가 바뀌면 자동으로 재요청
queryFn: () => fetchPost(postId),
});
if (isLoading) return <p>로딩 중...</p>;
return <div>{post?.title}</div>;
}
useMutation - 데이터 변경 (POST, PUT, DELETE)
import { useMutation, useQueryClient } from "@tanstack/react-query";
interface NewPost {
title: string;
body: string;
userId: number;
}
const createPost = async (newPost: NewPost): Promise<Post> => {
const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newPost),
});
return res.json();
};
function CreatePost() {
const queryClient = useQueryClient();
const [title, setTitle] = useState("");
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
// 캐시 무효화: posts 조회 쿼리 자동 재요청
queryClient.invalidateQueries({ queryKey: ["posts"] });
setTitle("");
alert("게시글이 생성되었습니다!");
},
onError: (error) => {
alert(`오류: ${error.message}`);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({ title, body: "내용", userId: 1 });
};
return (
<form onSubmit={handleSubmit}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="제목 입력"
/>
<button type="submit" disabled={mutation.isPending}>
{mutation.isPending ? "저장 중..." : "저장"}
</button>
</form>
);
}
API 함수 파일 분리 (권장 구조)
// src/api/posts.ts
const BASE = "https://jsonplaceholder.typicode.com";
export const postsApi = {
getAll: async (): Promise<Post[]> => {
const res = await fetch(`${BASE}/posts?_limit=10`);
if (!res.ok) throw new Error("Network error");
return res.json();
},
getById: async (id: number): Promise<Post> => {
const res = await fetch(`${BASE}/posts/${id}`);
return res.json();
},
create: async (post: NewPost): Promise<Post> => {
const res = await fetch(`${BASE}/posts`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(post),
});
return res.json();
},
delete: async (id: number): Promise<void> => {
await fetch(`${BASE}/posts/${id}`, { method: "DELETE" });
},
};
주요 옵션 정리
useQuery({
queryKey: ["posts"],
queryFn: fetchPosts,
staleTime: 1000 * 60, // 1분간 캐시를 최신으로 간주 (재요청 안 함)
gcTime: 1000 * 60 * 5, // 5분 후 캐시 삭제 (구 cacheTime)
enabled: !!userId, // false면 자동 실행 안 함
refetchOnWindowFocus: false, // 창 포커스 시 자동 갱신 끄기
});
useEffect로 직접 구현 vs TanStack Query 비교
| 기능 | useEffect 직접 | TanStack Query |
|---|---|---|
| 로딩/오류 처리 | 직접 구현 | 자동 제공 |
| 캐싱 | ❌ | ✅ 자동 캐싱 |
| 중복 요청 방지 | ❌ | ✅ 자동 |
| 백그라운드 갱신 | ❌ | ✅ 자동 |
| 코드 양 | 많음 | 적음 |