본문 바로가기
개발 상자

TanStack Query 정리

by 리잼 2025. 11. 17.
반응형

TanStack Query 란

  • 리액트 어플리 케이션에서 서버 상태를 효율적으로 관리하는 라이브러리

Client State vs Server State

Client State

  • UI 와 관련된 상태
  • 모달의 오픈여부, 언어, 테마 등
  • useState
  • 서버에서 일어나는 일과 관련이 없음
  • 유저 액션에 의해 변경, 클라이언트 내부에서 관리되고 업데이트 됨

Server State

  • 서버에 저장되지만 클라이언트에 노출하는데 필요한 데이터
  • 서버 → 클라이언트로 전송 (GET)
  • 클라이언트 → 서버로 전송 (POST, PUT, DELETE)

TanStack Query 특징

  • 간펴한 데이터 fetching
    • 훅 이용, 데이터를 쉽게 가져옴
  • 자동 캐싱
    • 한번 가져온 데이터는 캐시에 저장되고, 동일한 요청이 반복되면 데이터를 재활용해서 네트워크 요청을 줄임
  • 동기화, 백그라운드 업데이트
    • 데이터가 오래되었거나, 다시 필요할 때 자동으로 백그라운드에서 데이터 갱신
  • Optimistic Update ( 낙관적 업데이트 )
    • 서버 응답 전에 UI먼저 업데이트, 좋은 UX제공
  • 성공, 에러, 로딩 상태 관리
    • 성공, 에러, 로딩 상태 쉽게 구분해서 처리

설치

npm i @tanstack/react-query

 

layout.tsx

import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Provider from "./Provider";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
        <Provider>{children}</Provider>
      </body>
    </html>
  );
}

Provider.tsx

"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

export default function Provider({ children }: { children: React.ReactNode }) {
  const queryClient = new QueryClient();
  return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
}
  • QueryClientProvider는 QueryClient 인스턴스를 받아서 자식 컴포넌트들이 React Query의 기능(데이터 패칭, 캐싱 등)을 사용할 수 있도록 환경을 제공
  • QueryClient는 React Query에서 데이터 캐싱, 패칭, 동기화 등의 핵심 로직을 담당하는 객체
    • new 를 이용해서 인스턴스 객체를 생성하고 client 라는 이름의 props 로 넘겨줌

app/page.tsx

import TodoList from "@/components/TodoList";

export default function Home() {
  return <TodoList />;
}
  • TodoList는 QueryClientProvider의 자식이기 때문에 내부에서 React Query의 훅(예: useQuery, useMutation 등)을 자유롭게 사용할 수 있다

TodoList.tsx

"use client";

import { useQuery } from "@tanstack/react-query";
import { useState } from "react";

export default function TodoList() {
  const [isEnabledQuery, setIsEnabledQuery] = useState(false);
  const { data, isFetching, isPending, refetch, isError, error } = useQuery({
    /*
    queryKey: 각 쿼리를 식별하기 위해 사용하는 고유한 값
    이 값을 기반으로 데이터를 캐싱하고, 필요할 때 다시 불러오거나 캐시된 데이터를 재사용
    */
    queryKey: ["todoList"],
    /*
    queryFn: 데이터를 가져오는 함수
    Promise를 반환하는 비동기 함수여야 하고, useQuery가 데이터를 필요로 할 때 자동으로 호출
    */
    queryFn: async () => {
      const response = await fetch("https://jsonplaceholder.typicode.com/todos");
      //   const response = await fetch("https://jsonplaceholdertypicasdsosde.com/todos");
      return await response.json();
    },
    retry: false, // 에러 발생 시 재시도 하지 않음
    retryDelay: 1000, // 재시도 딜레이
    // enabled: isEnabledQuery, // ex) 유저의 권한여부에 따라 쿼리 실행 여부 결정
    // refetchInterval: 3000, // 3초마다 데이터 재요청
    // refetchOnWindowFocus: false, // 윈도우 포커스 시 데이터 재요청 여부
    // staleTime: 3000, // 3초 동안 데이터를 캐시로 유지
    gcTime: 3000, // 메모리에 캐시된 데이터가 가비지 컬렉터에 의해 자동으로 제거되기 전까지의 시간
  });

  console.log("isPending", isPending);
  console.log("isFetching", isFetching);

  if (isError) {
    return <div>에러 발생 : {error.message}</div>;
  }

  return (
    <>
      <p>isPending: {isPending ? "pending..." : "done"}</p>
      <p>isFetching: {isFetching ? "fetching..." : "done"}</p>
      <button
        onClick={() => {
          setIsEnabledQuery(true);
        }}
      >
        리스트 보기
      </button>
      <button onClick={() => refetch()}>refetch</button>
      {isFetching && <div>Loading...</div>}
      <ul className="flex flex-col items-center justify-center">
        {data?.map((item: { id: number; title: string }) => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </>
  );
}

 

자주 사용되는 useQuery 객체

  • data
    • 기본값은 undefined
    • 쿼리에 대해 마지막으로 성공적으로 확인된 데이터
  • error
    • 기본값은 null
    • 오류가 발생한 경우 쿼리에 대한 오류 객체
  • isPending
    • 쿼리가 로딩중인지 알려주는 값
      • true → 데이터 가져오는중
      • false → 데이터 가져옴
    • 데이터가 존재하는지에 대해 초점
    • 데이터가 아직 없어서 기다리는 상태
    • 최초에 데이터를 가져올 때 사용
  • isFetching
    • isPending 과 같은 역할
    • 데이터 요청이 진행중인지에 대해 초점
    • 새로운 요청이 활성화 되어있으면 true
  • refetch
    • 가져온 데이터를 다시 fetch

useQuery 옵션

  • staleTime
    • 데이터가 신선하다고 간주되는 시간
    • 신선한 데이터는 네트워크 요청없이 캐시 된 데이터를 재사용
  • gcTime
    • 데이터가 캐시에서 삭제되기까지 걸리는 시간

react-query-devtools

npm i @tanstack/react-query-devtools
  • 쿼리 캐시가 어떻게 남아있는지, 어느 시점에 refetch 또는 삭제 되는지 확인
  • 에러와 재시도, 로딩 과정을 실시간 디버깅
  • 쿼리 옵션(staleTime, gcTime 등) 에 따른 데이터 생명 주기를 시각적으로 검증
  • 쿼리 무효화 / 삭제 / 강제 Refetch 등 테스트를 ui로 손쉽게 조작
  • 디버깅 효율 극대화: 눈으로 보며 서버 상태 변화를 즉각 확인이 가능하여 원인 추적 최적화가 빠름

useMutation

  • 서버의 데이터 변경 (c, u, d)

PostList.tsx

'use client';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
import { useState } from 'react';
import { v4 as uuidv4 } from 'uuid';

export default function PostList() {
  const { data, refetch } = useQuery({
    queryKey: ['posts'],
    queryFn: async () => {
      const response = await axios.get('http://localhost:3000/posts');
      return response.data;
    },
  });

  const queryClient = useQueryClient();
  const [title, setTitle] = useState('');
  const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    mutate({ id: uuidv4(), title: title });
  };

  const addPost = async (post: { id: string; title: string }) => {
    const response = await axios.post('http://localhost:3000/posts', post);
    return response.data;
  };

  const { mutate, isPending } = useMutation({
    mutationFn: addPost,
    onSuccess: () => {
      alert('등록 성공');
      /*
      refetch: 사용자 액션에 의한 강제 새로고침이 필요한경우 사용
      invalidateQueries: 캐시된 데이터를 무효화하고 새로운 데이터를 가져옴
      */
      // refetch();

      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
    onError: () => {
      alert('등록 실패');
    },
    onSettled: () => {
      setTitle('');
    },
  });

  const deletePost = async (id: string) => {
    const response = await axios.delete(`http://localhost:3000/posts/${id}`);
    return response.data;
  };

  const { mutate: deleteMutate } = useMutation({
    mutationFn: deletePost,
    onSuccess: () => {
      alert('삭제 성공');
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
    onError: () => {
      alert('삭제 실패');
    },
  });

  return (
    <>
      <form onSubmit={submitHandler}>
        <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} />
        {isPending ? '등록중...' : null}
        <button type="submit" disabled={isPending}>
          등록
        </button>
        <ul>
          {data?.map((post: { id: string; title: string }) => (
            <li key={post.id}>
              {post.title}
              <button onClick={() => deleteMutate(post.id)}>삭제</button>
            </li>
          ))}
        </ul>
      </form>
    </>
  );
}
반응형

'개발 상자' 카테고리의 다른 글

애플 소셜 로그인 for Cognito  (0) 2025.11.28
Samsung Smartthings Access Token 발급받기  (0) 2025.08.20