2 분 소요

개요

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
로딩/오류 처리 직접 구현 자동 제공
캐싱 ✅ 자동 캐싱
중복 요청 방지 ✅ 자동
백그라운드 갱신 ✅ 자동
코드 양 많음 적음


관련 링크