ํฐ์คํ ๋ฆฌ ๋ทฐ
[โ๏ธ] Tanstack Query(1) - useQuery, mutate, invalidateQueries
์ฑ._. 2025. 3. 8. 19:29์คํ ๋ค๋๋ฐ์์ ์งํํ๋ 90๋ถ์ ํ์์ดํ ๊ณผ์ .
1. ํฌ๋๋ฆฌ์คํธ๋ฅผ tanstack query๋ก ๋ฆฌํฉํ ๋ง
2. ์ข์์ ๋ฒํผ์ ๋๊ด์ ์ ๋ฐ์ดํธ๋ก ๋ฆฌํฉํ ๋ง
๋ณต์ตํ๋ฉฐ tanstack์ ๊ฐ๋ ์ ๋ํด ์ ๋ฆฌํด๋ณด๊ณ ์ ํ๋ค.
โ๏ธ ํฌ๋๋ฆฌ์คํธ๋ฅผ tanstack query๋ก ๋ฆฌํฉํ ๋ง

useQuery
๐๊ธฐ์กด ๋ก์ง
- fetchData ํจ์์์ ๋น๋๊ธฐ์ ์ผ๋ก (async - await) ์ฒ๋ฆฌ ์ค
- try, catch, finally๋ก ๋ฐ์ดํฐ, ์๋ฌ, ๋ก๋ฉ ์ํ๋ฅผ ๊ด๋ฆฌํจ
- useEffect๋ฅผ ์ฌ์ฉํ์ฌ axios ์์ฒญ์ ๋ฐ์ ์ด
// todos.js
import axios from "axios";
export const todoApi = axios.create({
baseURL: "http://localhost:4000",
});
// Home.jsx
import { useState, useEffect } from "react";
import { todoApi } from "../api/todos";
import TodoForm from "../components/TodoForm";
import TodoList from "../components/TodoList";
export default function Home() {
// TODO: ํ์: useQuery ๋ก ๋ฆฌํฉํฐ๋ง ํ์ธ์.
// TODO: ์ ํ: useQuery ๋ก ๋ฆฌํฉํฐ๋ง ํ, ์ปค์คํ
ํ
useTodosQuery ๋ก ์ ๋ฆฌํด ๋ณด์ธ์.
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [data, setData] = useState([]);
const fetchData = async () => {
try {
const response = await todoApi.get("/todos");
setData(response.data);
} catch (err) {
setError(err);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
if (isLoading) {
return <div style={{ fontSize: 36 }}>๋ก๋ฉ์ค...</div>;
}
if (error) {
console.error(error);
return (
<div style={{ fontSize: 24 }}>์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค: {error.message}</div>
);
}
return (
<>
<h2>์๋ฒํต์ ํฌ๋๋ฆฌ์คํธ by useState</h2>
<TodoForm fetchData={fetchData} />
<TodoList todos={data} />
</>
);
}
๐useQuery ์ ์ฉ ๋ก์ง
- App.jsx์์ new QueryClient ์์ฑ ํ Provider๋ก ๊ฐ์ธ๊ธฐ ๐๐ป Router(์ฌ๊ธฐ์๋ ์ ์ญ)์ tanstack query๋ฅผ ์ ์ฉ
- useQuery๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ, ๋ก๋ฉ, ์๋ฌ ์ํ๋ฅผ ํ ๋ฒ์ ๊ด๋ฆฌ ๐๐ป ๋ก์ง ๊ฐํธ, ์ฌ๋ฌ ์ํ๋ฅผ ์์์ ์ฒ๋ฆฌํด ์ค
- queryKey๋ก queryFn ์ ์บ์ฑํ๋ค. ๐๐ป "todos" ๋ผ๋ key๋ก fetchData(todoApi์์ ๊ฐ์ ธ์จ ๊ฐ)์ ์บ์ฑํ์ฌ ๊ด๋ฆฌ
// App.jsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import Router from "./shared/Router";
const queryClient = new QueryClient();
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router />
</QueryClientProvider>
);
}
export default App;
// Home.jsx
...
const fetchData = async () => {
const response = await todoApi.get("/todos");
return response.data;
};
const { data, isLoading, error } = useQuery({
queryKey: ["todos"],
queryFn: fetchData,
});
if (isLoading) {
return <div style={{ fontSize: 36 }}>๋ก๋ฉ์ค...</div>;
}
if (error) {
console.error(error);
return (
<div style={{ fontSize: 24 }}>์๋ฌ๊ฐ ๋ฐ์ํ์ต๋๋ค: {error.message}</div>
);
}
...
๐จqueryKey ๋ฅผ ๋ค๋ฅด๊ฒ ์ฃผ์ด์ผ ํ๋ ์ด์
๋ํ ์ผ ํ์ด์ง์์๋ ํด๋ฆญํ ์นด๋์ ์์ธ ๋ด์ฉ์ ๋ถ๋ฌ์์ผ ํ๊ธฐ ๋๋ฌธ์ ํด๋น id๋ฅผ params๋ก ๋ฐ์์จ๋ค.
๐์๋ 1. Home.jsx์ queryKey๋ฅผ ๋๊ฐ์ด ์ค์
// Detail.jsx
export default function Detail() {
const { id } = useParams();
const fetchDetail = async () => {
const response = await todoApi(`/todos/${id}`);
return response.data;
};
const { data, isLoading, error } = useQuery({
queryKey: ["todos"], โญ๏ธ
queryFn: fetchDetail,
});

- params๋ก id๋ฅผ ๋ฐ์์์ API์ ์ ์ฉ์ ํ์ง๋ง ์ฟผ๋ฆฌํค๋ฅผ Home๊ณผ ๋์ผํ๊ฒ ๋์๊ธฐ ๋๋ฌธ์
fetchDetail์ data๋ ์บ์ฑ๋ ๋ฐ์ดํฐ(todos ๋ฐฐ์ด)์์ id์ ํด๋นํ๋ ๊ฐ์ฒด๋ฅผ ๋ฐํ - ๋ฆฌ์คํธ ํ์ด์ง๋ Home์์ ์บ์ฑํ "todos"์ ๋ฐ์ดํฐ๋ฅผ ๊ทธ๋ ค๋ด์ง๋ง
๋ํ ์ผ ํ์ด์ง์์ "todos" ๋ฐ์ดํฐ๋ฅผ ํด๋น id์ ๊ฐ์ฒด๋ก ๋ฐ๊ฟจ๊ธฐ ๋๋ฌธ์ ๋ํ ์ผ -> ํ์ผ๋ก ์ด๋ํ ๋ ์ํฅ - ๊ฐ์ฒด.map ์ ํ ์ ์๊ธฐ ๋๋ฌธ์ TypeError
๐์๋ 2. queryKey๋ฅผ ๋ค๋ฅด๊ฒ ์ ์ฉ
// Detail.jsx
export default function Detail() {
const { id } = useParams();
const fetchDetail = async () => {
const response = await todoApi(`/todos/${id}`);
return response.data;
};
const { data, isLoading, error } = useQuery({
queryKey: ["detail"], โญ๏ธ
queryFn: fetchDetail,
});

- Home๊ณผ๋ ๋ค๋ฅธ ์ฟผ๋ฆฌํค๋ฅผ ์ฃผ์ด ์๋ก ๋ค๋ฅธ ๋ฐ์ดํฐ๋ฅผ ์บ์ฑํ๊ธฐ ๋๋ฌธ์ ๋ํ ์ผ -> ํ์ผ๋ก ์ด๋ ์์ ์ค๋ฅ๊ฐ ๋์ง ์์
- 3G ํ๊ฒฝ์์ ์ดํด๋ณด๋ฉด ๋ํ
์ผ ํ์ด์ง๋ผ๋ฆฌ ์ด๋ ์, ์ด์ ์ ํด๋ฆญํ๋ ๋ํ
์ผ ๋ฐ์ดํฐ๊ฐ ๊ทธ๋ ค์ง๋ค๊ฐ ์ดํ์ ์ ๋๋ก ๋ฐ์
- ์ฒ์์ "detail" ์ด๋ผ๋ ๋ฐ์ดํฐ๋ 1๋ฒ ๋ํ ์ผ ๋ด์ฉ์ ๋ก๋ฉํ์ฌ ๊ฐ์ ธ์๊ณ , ๊ทธ ๊ฐ์ ์บ์ฑํด ๋
- 2๋ฒ ๋ํ ์ผ ํ์ด์ง๋ก ๋ค์ด๊ฐ์ ๋, ์บ์ฑ๋ 1๋ฒ ๋ํ ์ผ์ ๋ณด์ฌ์ค
- ๊ทธ ์ฌ์ด์ ๋ณด์ด์ง ์๋ ๊ณณ์์๋ params์ id ๊ฐ์ ์ฐพ๋ ์ค - isLoding ์ํ
- ์ด์ ๊ณผ ๋ค๋ฅธ ๊ฐ์ ์ฐพ์๊ธฐ ๋๋ฌธ์ isLoding state์ ๋ณํ๊ฐ ์๊ฒผ๊ณ , ๋ฆฌ๋ ๋๋ง์ด ๋์ด 2๋ฒ ๋ํ ์ผ ํ์ด์ง๋ฅผ ๋ณด์ฌ์ค
๐ ์๋ 3. queryKey์ ํด๋น id๋ฅผ ํจ๊ป ๋ถ์ฌํจ
// Detail.jsx
export default function Detail() {
const { id } = useParams();
const fetchDetail = async () => {
const response = await todoApi(`/todos/${id}`);
return response.data;
};
const { data, isLoading, error } = useQuery({
queryKey: ["todos", id], โญ๏ธ
queryFn: fetchDetail,
});

- ์ฟผ๋ฆฌํค๋ฅผ ["todos", id]๋ก ์ฃผ์๊ธฐ ๋๋ฌธ์ ๋ํ ์ผ ํ์ด์ง์ ๋ค์ด๊ฐ ๋๋ง๋ค todos ๋ฐ์ดํฐ์์ id์ ํด๋นํ๋ ๊ฐ์ ๊ฐ์ ธ์ด
- ๊ทธ ๊ฐ์ ์บ์ฑ๋์ด ์๊ธฐ ๋๋ฌธ์ ํ์ด์ง๋ฅผ ๋ค์ ๋ค์ด๊ฐ๋ ์ด์ ์ ์บ์ฑํด ๋ ๊ฐ์ ๋ณด์ฌ์ฃผ์ด '๋ก๋ฉ ์ค...' ์ด ๋จ์ง ์์
- isLoding ์ํ๊ฐ ์ง๋ ํ ์ด์ ์บ์ฑ๊ฐ๊ณผ ๋ณํ๋ ๊ฒ ์๊ธฐ ๋๋ฌธ์ ๋ฆฌ๋ ๋๋ง์ด ์ผ์ด๋์ง ์์
โจ๊ฒฐ๋ก
- ์ฟผ๋ฆฌํค๋ฅผ ๋ค๋ฅด๊ฒ ์ฃผ์ด ๊ฐ์ ์บ์ฑ๋๋ ๋ฐ์ดํฐ์ ์ํฅ์ ์ฃผ์ง ์๋๋ก ํ๋ค.
- id์ ๊ฐ์ด ํน์ ํ ๋ฐ์ดํฐ๋ฅผ ๋ณด์ฌ์ฃผ์ด์ผ ํ ๋๋ ์ฟผ๋ฆฌํค์ ํน์ ํ ๊ฐ์ ์ถ๊ฐํ์ฌ ํด๋นํ๋ ๋ฐ์ดํฐ๋ฅผ ์๋กญ๊ฒ ์ฐพ๋๋ก ํ๋ค.
mutation & invalidate Queries
๐๊ธฐ์กด ๋ก์ง
- Home.jsx์์ fetchData๋ฅผ props ๋ก ์ ๋ฌ๋ฐ์์ ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌ
- post(๊ฐ์ ์ถ๊ฐ)ํ ํ, ๋ค์ fetchData๋ฅผ ๋ถ๋ฌ ์๋์ผ๋ก ๊ด๋ฆฌ
// TodoForm.jsx
import { useState } from "react";
import { todoApi } from "../api/todos";
export default function TodoForm({ fetchData }) {
const [title, setTitle] = useState("");
const [contents, setContents] = useState("");
// TODO: ํ์: useMutation ์ผ๋ก ๋ฆฌํฉํฐ๋ง ํ์ธ์.
// TODO: ์ ํ: useMutation ์ผ๋ก ๋ฆฌํฉํฐ๋ง ํ, useTodoMutation ์ปค์คํ
ํ
์ผ๋ก ์ ๋ฆฌํด ๋ณด์ธ์.
const handleAddTodo = async (e) => {
e.preventDefault();
setTitle("");
setContents("");
await todoApi.post("/todos", {
id: Date.now().toString(),
title,
contents,
isCompleted: false,
createdAt: Date.now(),
});
await fetchData();
};
return (
<form onSubmit={handleAddTodo}>
<label htmlFor="title">์ ๋ชฉ:</label>
<input
type="text"
id="title"
name="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<label htmlFor="contents">๋ด์ฉ:</label>
<input
id="contents"
name="contents"
value={contents}
onChange={(e) => setContents(e.target.value)}
required
/>
<button type="submit">์ถ๊ฐํ๊ธฐ</button>
</form>
);
}
๐mutation ์ ์ฉ ๋ก์ง
- useMutation ํ ์ ์ฌ์ฉํ์ฌ { mutate } ์ ์ ๋ฐ ์ ์ฉ ๐๐ป mutationFn๋ฅผ ์คํํ์ฌ ์๋ก์ด ๊ฐ์ ์ถ๊ฐ (์ธ์๋ CUD ๊ฐ๋ฅ)
- onSuccess (์์ฒญ ์ฑ๊ณต) ์ invalidateQueries ๋ฅผ ์ ์ฉํ์ฌ ํด๋น key์ ์ฟผ๋ฆฌ๋ฅผ ๋ฌดํจํ ์ํด ๐๐ป ์บ์ฑํด๋์๋ ์ฟผ๋ฆฌ๊ฐ ์ฌ๋ผ์ง๋ฉด์ ๋ฐ์ดํฐ๋ฅผ ๋ค์ ๋ถ๋ฌ์ด (ํ๋ฉด์ ์๋ก์ด ๋ฐ์ดํฐ ๊ฐ ๋ฐ์)
// TodoForm.jsx
...
export default function TodoForm() {
const [title, setTitle] = useState("");
const [contents, setContents] = useState("");
const queryClient = useQueryClient();
// TODO: ํ์: useMutation ์ผ๋ก ๋ฆฌํฉํฐ๋ง ํ์ธ์.
// TODO: ์ ํ: useMutation ์ผ๋ก ๋ฆฌํฉํฐ๋ง ํ, useTodoMutation ์ปค์คํ
ํ
์ผ๋ก ์ ๋ฆฌํด ๋ณด์ธ์.
const handleAddTodo = async (e) => {
e.preventDefault();
const newTodo = {
id: Date.now().toString(),
title,
contents,
isCompleted: false,
createdAt: Date.now(),
};
mutate(newTodo);โญ๏ธ
setTitle("");
setContents("");
};
const addTodo = async (newTodo) => {
await todoApi.post("/todos", newTodo);
};
const { mutate } = useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries(["todos"]);
},
});
...
useMutation ํ ์ผ๋ก mutationFn์ ๊ด๋ฆฌํ๊ณ CUD๋ฅผ ํ๋ ๊ฑด ์๊ฒ ์ด,
๊ทธ๋ผ โญ๏ธmutate() ์ ์ญํ ์ด ๋ญ๊น??
mutate๋ useMutation ํ ์์ ๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ๋ ํจ์์ด๋ค.
์ฃผ๋ก POST(C), PUT(U), DELETE(D) ๊ฐ์ ์์ฒญ์ ๋ณด๋ผ ๋ ์ฌ์ฉํ๋ฉฐ,
๋ฐ์ดํฐ๋ฅผ ๋ณ๊ฒฝํ ํ ์บ์๋ฅผ ๊ฐฑ์ ํ๋ ์ญํ ์ ํ๋ค.
์ฆ, ์์ ๋ก์ง์์ addTodo ํจ์(๋น๋๊ธฐ ์์ฒญ)์ ์คํํ์ฌ ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํ๋ ์ญํ ์ ํ๋ค.
'Language > React' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [โ๏ธ] ์ฟผ๋ฆฌ ๋ฌดํจํ, ๊ทธ๋ฆฌ๊ณ staleTime & gcTime (0) | 2025.03.09 |
|---|---|
| ์ฌ๊ธฐ์ state๋ฅผ ๊ตณ์ด? - ์ฌ๋ฆผํฝ ์ ๋ ฌ(sort) (0) | 2025.02.11 |
| [Pokemon PJ_Day 4] RTK (2) - ์ฌ์ฉํ๊ธฐ (0) | 2025.02.06 |
| [Pokemon PJ_Day 4] RTK (1) - ์ธํ ํ๊ธฐ (0) | 2025.02.06 |
| [โ๏ธ] Redux - counter ์ฑ ์์ฑ, redux ์ฌ์ดํด, dispatch (0) | 2025.02.03 |