반응형

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 |