https://ldd6cr-adness.tistory.com/303
Tanstack Query๋ ๋ญ๊น
๋ชฉ์ฐจ1. TanStack Query๋2. ์ค์น ๋ฐ ๊ธฐ๋ณธ ์ค์ 2-1. TanStack ํจํค์ง ์ค์น 2-2. Query Client ์์ฑ ๋ฐ Provider ์ค์ 3. ๋ฐ์ดํฐ ์กฐํ (Fetching)4. ๋ฐ์ดํฐ ๋ณ๊ฒฝ (Mutations)5. ์ฟผ๋ฆฌ ๋ฌดํจํ ๋ฐ ๋ฆฌํจ์นญ6. ๊ธฐํ ๊ธฐ๋ฅ๋ค7. ํด1. TanSt
ldd6cr-adness.tistory.com
๋ชฉ์ฐจ
1. ์ค์ต ํ๋ก์ ํธ ๋ด์ฉ
2. ๊ธฐ๋ณธ ์ค์
2-1. ๊ธฐ๋ณธ ์ค์น
2-2. ๋ผ์ฐํ ์ค์
2-3. TanStack Query ๊ธฐ๋ณธ ์ค์
---< ์ค์ต ์์ >---
1. useQuery๋ก ๋ฐ์ดํฐ ์กฐํ(fetching)
2. useMutation์ผ๋ก ๋ฐ์ดํฐ ๋ณ๊ฒฝ
3. ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์ฟผ๋ฆฌ ๋ฌดํจํ/๋ฆฌํจ์น ๊ตฌํ
4. ๋ฐ์ดํฐ์ ์ ์ ๋์ ์บ์ ์ ์ง ์๊ฐ ์ค์
5. React Query Devtools
0. ์ค์ต ํ๋ก์ ํธ ๋ด์ฉ
- ๊ฒ์ํ ๋ทฐ์ด : ๊ฒ์๊ธ ๋ชฉ๋ก์ ๋ถ๋ฌ์ค๊ณ , ์์ธ ๋ด์ฉ์ ํ์ธํจ
- ๊ธฐ์ ์คํ
- React
- TanStack Query
- JSONPlaceholder API (/posts, /posts/:id, /posts/:comment)
- React Router
- ํ๋ก์ ํธ ๊ตฌ์กฐ
- src/
-- App.tsx
-- index.tsx
-- components/
ใด PostList.tsx
ใด PostDetail.tsx
ใด CommentForms
-- api/
ใด posts.ts
ใด comments.ts
- src/
2. ๊ธฐ๋ณธ ์ค์
2-1. ๊ธฐ๋ณธ ์ค์น
npm install
npm install @tanstack/react-query react-router-dom
npm ํจํค์ง๋ฅผ ์ค์นํ๊ณ , tanstak๊ณผ ํ์ด์ง ์ด๋์ ์ํ react-router-dom์ ์ค์นํ๋ค.
2-2. ๋ผ์ฐํ ์ค์
import React from 'react';
import {Route, Routes} from 'react-router-dom';
import { PostList } from './components/PostList.tsx';
import { PostDetail } from './components/PostDetail.tsx';
function App() {
return (
<div>
<h1 style={{ color: 'black', textAlign: 'center' }}>๊ฒ์ํ</h1>
<Routes>
<Route path="/" element={<PostList />} />
<Route path="/posts/:id" element={<PostDetail />} />
</Routes>
</div>
);
}
export default App;
์ด ๋ถ๋ถ ํ๋ฉด์ด ์๋์์ ๋ณด๋๊น ์ฒ์์ div๊ฐ ์๋๋ผ BrowserRouter๋ก ๊ฐ์ธ๊ณ ์์๋๋ฐ,
index.tsx์์ ์ด๋ฏธ BrowserRouter๋ก ๊ฐ์ธ์ ์ด์ค์ผ๋ก ๊ฐ์ธ๋ฒ๋ฆฌ๋ ๋ฐ๋์ ํ๋ฉด์ด ์๋์๋ ๊ฑฐ์๋ค.
** BrowserRouter๋ ํญ์ ์ ํ๋ฆฌ์ผ์ด์ ์ ์ต์์์ ์์นํ๊ฒ ํ๋ฉฐ, ์ฌ๋ฌ ๋ฒ ์ฌ์ฉํ์ง ์๋๋ก ํ์~ **
2-3. TanStack Query ๊ธฐ๋ณธ ์ค์
// index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter as Router } from 'react-router-dom';
const queryClient = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<Router>
<App />
</Router>
</QueryClientProvider>
</React.StrictMode>
);
์ ํ๋ฆฌ์ผ์ด์ ์ ์ง์ ์ ์ธ index.tsx ํ์ผ์ QueryClient, QueryClientProvider๋ฅผ ์ง์ ํ๋ค.
QueryClient ์ธ์คํด์ค ์์ฑ: TanStack Query๊ฐ ์ํ, ์บ์, ๋ฆฌํจ์น ๋ฑ ๋ค์ํ ๊ธฐ๋ฅ์ ๊ด๋ฆฌํ ์ ์๋๋ก ํจ
QueryClientProvider ์ปดํฌ๋ํธ: React ์ปดํฌ๋ํธ ํธ๋ฆฌ ์ ์ฒด์์ TanStack Query ํ ์ด ์๋ํ๋๋ก QueryClient ์ ๋ฌ
๋ฐ์ดํฐ ๊ด๋ฆฌ๋ฅผ ์ ์ญ์์ ํ๊ธฐ ์ํด QueryClientProvider๋ฅผ ์ต์์ ์ปดํฌ๋ํธ(App) ์ธ๋ถ์ ๋๋ค.
๊ธฐ๋ณธ์ ์ธ ๋ถ๋ถ๋ค์ ์์ฑํ์ผ๋ ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก TanStack Query๋ฅผ ์ฌ์ฉํด๋ณผ ์๊ฐ~!
1. useQuery๋ก ๋ฐ์ดํฐ ์กฐํ(fetching)
1. API ํจ์ ์ ์
// api/posts.ts
export const fetchPosts = async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts');
if (!response.ok) {
throw new Error('failed to fetch posts');
}
return response.json();
}
export const fetchPostById = async (id: number) => {
const response = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
if (!response.ok) {
throw new Error('failed to fetch post by id');
}
return response.json();
}
posts์ ๊ด๋ จ๋ ์์ฒญ์ ์ ์ํ ํ์ผ์ด๋ค.
fetchPosts : ๋ชจ๋ ๊ฒ์๊ธ ๋ชฉ๋ก์ ์๋ฒ์ ์์ฒญํด์ ๋ฐ์์ค๋ ํจ์
jsonplaceholder ์๋ฒ์ ์์ฒญํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊ณ , js ๊ฐ์ฒด๋ก ๋ณํํ๋ค.
fetchPostById : ํน์ ID๋ฅผ ๊ฐ์ง ๊ฒ์๊ธ ํ๋๋ง ์๋ฒ์ ์์ฒญํด์ ๋ฐ์์ค๋ ํจ์
์ธ์ id: number๋ฅผ ํตํด ์ซ์ ํ๋(=id)๋ฅผ ์ ๋ ฅ๋ฐ๋๋ค.
2. PostList ์ปดํฌ๋ํธ
// components/PostList.tsx
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { fetchPosts } from "../api/posts.ts";
import { Link } from "react-router-dom";
export function PostList() {
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
if (isLoading) return <p>Loading...</p>;
if (error instanceof Error) return <p>error: {error.message}</p>;
return (
<ul>
{data.slice(0, 10).map((post: any) => (
<li key={post.id}>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
)
}
fetchPosts api ํจ์๋ก ์๋ฒ๋ก๋ถํฐ ๋ฐ์์จ ๊ฐ์ post ์ ๋ชฉ 10๊ฐ๋ฅผ ๋์ฐ๋ ์ปดํฌ๋ํธ์ด๋ค.
useQuery ํ ์ ์ฌ์ฉํ์ฌ ์๋ฒ์ ์์ฒญํด ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๊ณ , ์ํ๋ฅผ ์๋์ผ๋ก ๊ด๋ฆฌํ๋ค.
queryKey: ['posts'] => ์ด ์์ฒญ์ ์ด๋ฆํ๋ก, TanStack Query๊ฐ 'posts'๋ฅผ ๊ธฐ์ตํด์ ๋์ค์ ๋ค์ ์ฌ์ฉํ ์ ์๋ค.
queryFn: fetchPosts => ์ค์ ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์๋ก, ์ ์ํด๋ fetchPosts๋ฅผ ์ฌ์ฉํ๋ค.
useQuery ํ ์ด ๊ธฐ๋ณธ ์ ๊ณตํ๋ ์์ฑ์ธ isLoading๊ณผ error ์์ฑ๋ค์ ์ฌ์ฉํด์ ํด๋น ์ํ์ผ ๊ฒฝ์ฐ ๋ณด์ฌ์ค ์ปจํ ์ธ ๋ฅผ ์์ฑํ๋ค.
3. PostDetail ์ปดํฌ๋ํธ
import {useParams} from 'react-router-dom';
import {useQuery} from '@tanstack/react-query';
import { fetchPostById } from '../api/posts.ts';
import React from 'react';
export function PostDetail() {
const {id} = useParams<{ id: string }>();
const postId = Number(id);
const { data, isLoading, error } = useQuery({
queryKey: ['posts', id],
queryFn: () => fetchPostById(postId!),
enabled: !!id, // id๊ฐ ์์ ๋๋ง ์ฟผ๋ฆฌ ์คํ
});
if (isLoading) return <p>Loading...</p>;
if (error instanceof Error) return <p>error: {error.message}</p>;
return (
<div>
<h2>{data.title}</h2>
<p>{data.body}</p>
</div>
)
}
PostList ์ปดํฌ๋ํธ์ ๋ค๋ฅด๊ฒ key๋ก id๋ ๊ฐ์ด ์ฌ์ฉํ๋ค.
๋ํ enabled ์์ฑ์ ์ฌ์ฉํ์ฌ id๊ฐ ์กด์ฌํ ๋๋ง ์ฟผ๋ฆฌ๋ฅผ ์คํํ๋๋ก ํ๋ค.
๊ฒฐ๊ณผ ํ๋ฉด
2. useMutation์ผ๋ก ๋ฐ์ดํฐ ๋ณ๊ฒฝ
1. API ํจ์ ์ ์ : ๋๊ธ ์์ฑ
// api/comments.ts
export const postComment = async (comment: {
postId: number;
name: string;
body: string;
}) => {
const response = await fetch(
"https://jsonplaceholder.typicode.com/comments",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(comment),
}
);
if (!response.ok) {
throw new Error("Failed to post comment");
}
return response.json();
};
- ์ธ๋ถ API์ ๋๊ธ POST๋ฅผ ์์ฒญํ๊ณ ์ ์ฅํ๋ ํจ์๋ค.
jsonplaceholder ์ํ API๋ (comment: {postId: number; name: string; body: string }) ๊ตฌ์กฐ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ฐ๋๋ค.
ํด๋น ํ์์ jsonplaceholder ์น์์ ํ์ธํ ์ ์๋ค.
๊ฐ ์์ฑ๋ค์ ์๋์ ๊ฐ์ ๋ฐ์ดํฐ๋ฅผ ๋ํ๋ธ๋ค.
- postId => ์ด๋ค ๊ธ(post)์ ๋ํ ๋๊ธ์ธ์ง
- name => ๋๊ธ ์์ฑ์์ ์ด๋ฆ
- body => ๋๊ธ ๋ด์ฉ
method: "POST"
: ๋ฐ์ดํฐ๋ฅผ ๋ณด๋ธ๋ค. ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ฌ ๋๋ "GET"์ ์ฌ์ฉํ๋ค.
headers; { "Content-Type: "application/json" }
: JSON ํ์์ผ๋ก ์์ฒญํจ์ ์๋ฒ์ ์๋ ค์ค๋ค.
body: JSON.stringify(comment)
์ฐ๋ฆฌ๊ฐ ๋ง๋ comment ๊ฐ์ฒด(json ํํ)๋ฅผ ๋ฌธ์๋ก ๋ณํ
2. ๋๊ธ ์์ฑ ํผ ์ปดํฌ๋ํธ ์์ฑ
// components/CommentForm.tsx
import React, { useState } from "react";
import { useMutation } from "@tanstack/react-query";
import { postComment } from "../api/comments";
interface CommentFormProps {
postId: number;
}
export function CommentForm({ postId }: CommentFormProps) {
const [name, setName] = useState("");
const [body, setBody] = useState("");
const mutation = useMutation({
mutationFn: postComment,
onSuccess: (data) => {
alert("๋๊ธ์ด ๋ฑ๋ก๋์์ต๋๋ค๐");
console.log(data); // ์๋ต ํ์ธ
setName("");
setBody("");
},
onError: (error: Error) => {
alert(`error: ${error.message}`);
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({ postId, name, body });
};
return (
<form onSubmit={handleSubmit}>
<h3>add Comment</h3>
<input
type="text"
placeholder="name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<br />
<textarea
placeholder="add your comment"
value={body}
onChange={(e) => setBody(e.target.value)}
required
/>
<br />
<button type="submit" disabled={mutation.isPending}>
write
</button>
</form>
);
}
3. PostDetail์ ๋๊ธ ์์ฑ ํผ ์ถ๊ฐ
import { useParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import { fetchPostById } from "../api/posts.ts";
import React from "react";
import { CommentForm } from "./CommentForm.tsx";
export function PostDetail() {
const { id } = useParams<{ id: string }>();
const postId = Number(id);
const { data, isLoading, error } = useQuery({
queryKey: ["posts", id],
queryFn: () => fetchPostById(postId!),
enabled: !!id, // id๊ฐ ์์ ๋๋ง ์ฟผ๋ฆฌ ์คํ
});
if (isLoading) return <p>Loading...</p>;
if (error instanceof Error) return <p>error: {error.message}</p>;
return (
<div>
<h2>{data.title}</h2>
<p>{data.body}</p>
<CommentForm postId={postId} /> {/*๋๊ธ ์ถ๊ฐ ํผ */}
</div>
);
}
๊ฒฐ๊ณผ ํ๋ฉด
3. ๋ฉ์๋๋ฅผ ์ฌ์ฉํ์ฌ ์ฟผ๋ฆฌ ๋ฌดํจํ/๋ฆฌํจ์นญ ๊ตฌํ
1. api ํจ์ createPost ์ ์
// api/posts.ts
// ...
export const createPost = async (newPost: { title: string; body: string; userId: number }) => {
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newPost),
});
if(!response.ok) {
throw new Error('failed to create post');
}
return response.json();
};
2. PostList.tsx์์ queryClient.invalidateQueries๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ ๋ฌดํจํ+์๋๋ฆฌํจ์นญ
// components/PostList.tsx
import React from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; // ๊ฒ์๊ธ ๋ชฉ๋ก ์์ ์ ์ํด ํ
import
import { fetchPosts, createPost } from "../api/posts.ts"; // ํ์ํ api ํจ์ createPost import
import { Link } from "react-router-dom";
export function PostList() {
// queryClient ํ
์ฌ์ฉ์ ์ํด queryClient ์ ์
const queryClient = useQueryClient();
// ๊ฒ์๊ธ ๋ชฉ๋ก ๊ฐ์ ธ์ค๊ธฐ
const { data, isLoading, error } = useQuery({
queryKey: ['posts'],
queryFn: fetchPosts,
});
// ๊ฒ์๊ธ ์ถ๊ฐ mutation
const mutation = useMutation({
mutationFn: createPost,
onSuccess: () => {
// ๊ฒ์๊ธ ์ถ๊ฐํ posts ์ฟผ๋ฆฌ ๋ฌดํจํ ๋ฐ (์๋)๋ฆฌํจ์น
queryClient.invalidateQueries({queryKey:['posts']});
alert('Post added successfully!๐');
},
});
const handleAddPost = () => {
const newPost = {
title: 'New Post',
body: 'This is a new post.',
userId: 1,
};
mutation.mutate(newPost);
};
if (isLoading) return <p>Loading...</p>;
if (error instanceof Error) return <p>error: {error.message}</p>;
return (
<div>
<h1>Post List</h1>
<button onClick={handleAddPost}>Add Post</button>
<ul>
{data.slice(0, 10).map((post: any) => (
<li key={post.id}>
<Link to={`/posts/${post.id}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
)
}
๊ฒฐ๊ณผ ํ๋ฉด
4. ๋ฐ์ดํฐ์ ์ ์ ๋์ ์บ์ ์ ์ง ์๊ฐ ์ค์
1. staleTime, cacheTime ์ต์ ์ผ๋ก ๋ฐ์ดํฐ์ ์ ์ ๋ ๋ฐ ์บ์ ์ ์ง ์๊ฐ์ ์ค์ ํ ์ ์๋ค
์ฒซ ๋ง์ดํธ ์, ๋ก๊ทธ ํ์ธ(์์ฒญ ๋ฐ์)์ด ๋๋ค.
- staleTime
- 1000*5(5์ด) ๋์์ ๋ฐ์ดํฐ๊ฐ ์ ์ ํ ๊ฒ์ผ๋ก ๊ฐ์ฃผ๋์ด, ํ์ด์ง๋ฅผ ๋ฒ์ด๋ฌ๋ค๊ฐ 5์ด ์์ ๋ค์ด์ค๋ฉด ์์ฒญ์ด ์๋ค.
- staleTime์ ์ค์ ํ์ง ์์๊ฒฝ์ฐ, ํ์ด์ง๋ฅผ ๋ฒ์ด๋ฌ๋ค ๋์์ฌ ๋๋ง๋ค ํญ์ ์์ฒญ์ด ๋ฐ์ํ๋ค(fetchPosts ํธ์ถ)
- cacheTime
- ์บ์๋ ์ฟผ๋ฆฌ ๋ฐ์ดํฐ๊ฐ 15์ด๋ง๋ค ์ ๊ฑฐ๋๋ค.
- ์บ์๋ ์ฟผ๋ฆฌ๊ฐ ์ข ๋ฃ๋ ๋ ๋ฐ๋ก ์ ๊ฑฐ๋๋, ์ ์ฝ๋์์๋ 15์ด๋์์ ์บ์๋ ๋ฐ์ดํฐ๊ฐ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ง๋๋ค.
- React Query Devtools๋ฅผ ์ฌ์ฉํ๋ฉด ์บ์ ์ํ๋ฅผ ๋์ผ๋ก ํ์ธํ ์ ์์ผ๋, Network ํญ์์๋ ์บ์์ ์ ๊ฑฐ ์ฌ๋ถ๋ฅผ ์ ์ ์๋ค.
5. React Query Devtools
1. ์ค์น
npm install @tanstack/react-query-devtools
2. Devtool ๋ ๋๋ง
// index.tsx ( QueryClientProvider ํ์)
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
function App() {
return (
<QueryClientProvider client={queryClient}>
{/* application components */}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
๊ฒฐ๊ณผ ํ๋ฉด
ํ๋ฉด ๊ตฌ์์ ๋ณด์ด๋ TanStack Query DevTool ๋ฒํผ์ ๋๋ฅด๋ฉด ๋ค์๊ณผ ๊ฐ์ ํ๋ฉด์ ๋ณผ ์ ์๋ค.
์๊น๋ณ๋ก ์๋์ ๊ฐ์ ์ํ๋ฅผ ๋ํ๋ธ๋ค.
Fresh(์ด๋ก), Fetching(ํ๋), Paused(๋ณด๋ผ), Stale(๋ ธ๋), Inactive(ํ์)
์ฆ, ํ๋ฉด์ ์ฒซ ๋ง์ดํธ, ํน์ 5์ด ์์ ๋ค์ ๋์์์ ๋๋ posts ์์ฒญ์ด ์ด๋ก์์ด๋, 5์ด ํ์๋ ๋ ธ๋์์ด ๋๋ค.
์ฒซ ํ๋ฉด์์ ๋ค๋ฅธ ํ๋ฉด์ผ๋ก ๋์ด๊ฐ ํ 15์ด๊ฐ ์ง๋๋ฉด(์๋ก PostLists์์ PostDetails ํ๋ฉด์ผ๋ก)
์ ์ฌ์ง๊ณผ ๊ฐ์ด Inactive ์์ฒญ(PostLists์์์ ์์ฒญ)์ด ์ญ์ = cache ์ญ์ ๊ฐ ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
๐ฅบ๋ฌธ์ ์๊ฒผ๋ ๋ถ๋ถ
: ๊ณ์ cacheTime์์ ์๋ฌ๊ฐ ๋๊ธธ๋ ์ฐพ์๋ดค๋๋ ์ ๋ฐ์ดํธ ํด์ ์ด์ cacheTime์ ์์ฐ๊ณ gcTime (garbage collection)์ ์ด๋ค๊ณ ํ๋ค. ์ฝ์ง...ใ ใ
cacheTime์ผ๋ก ์์ฑํ์ ๋๋ ์บ์ ์ญ์ ๊ฐ ์ ๋๋ก ์๋๋๋ฐ gcTime์ผ๋ก ๋ณ๊ฒฝํ๋ฉด ์ ๋๋ค.
'ํ๋ก ํธ์๋(Front-end) > Tools' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
axios๋ ๋ญ๊น (1) | 2025.05.22 |
---|---|
Zustand ์ฌ์ฉํด๋ณด๊ธฐ (0) | 2025.05.15 |
Tanstack Query๋ ๋ญ๊น (0) | 2025.05.08 |
Zustand๋ ๋ญ๊น (0) | 2025.04.04 |
Next.js๋ ๋ญ๊น (0) | 2025.03.20 |