ํ‹ฐ์Šคํ† ๋ฆฌ ๋ทฐ

์Šคํƒ ๋‹ค๋“œ๋ฐ˜์—์„œ ์ง„ํ–‰ํ–ˆ๋˜ 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 ํ™˜๊ฒฝ์—์„œ ์‚ดํŽด๋ณด๋ฉด ๋””ํ…Œ์ผ ํŽ˜์ด์ง€๋ผ๋ฆฌ ์ด๋™ ์‹œ, ์ด์ „์— ํด๋ฆญํ–ˆ๋˜ ๋””ํ…Œ์ผ ๋ฐ์ดํ„ฐ๊ฐ€ ๊ทธ๋ ค์ง€๋‹ค๊ฐ€ ์ดํ›„์— ์ œ๋Œ€๋กœ ๋ฐ˜์˜
    1. ์ฒ˜์Œ์— "detail" ์ด๋ผ๋Š” ๋ฐ์ดํ„ฐ๋Š” 1๋ฒˆ ๋””ํ…Œ์ผ ๋‚ด์šฉ์„ ๋กœ๋”ฉํ•˜์—ฌ ๊ฐ€์ ธ์™”๊ณ , ๊ทธ ๊ฐ’์„ ์บ์‹ฑํ•ด ๋‘ 
    2. 2๋ฒˆ ๋””ํ…Œ์ผ ํŽ˜์ด์ง€๋กœ ๋“ค์–ด๊ฐ”์„ ๋•Œ, ์บ์‹ฑ๋œ 1๋ฒˆ ๋””ํ…Œ์ผ์„ ๋ณด์—ฌ์คŒ
    3. ๊ทธ ์‚ฌ์ด์— ๋ณด์ด์ง€ ์•Š๋Š” ๊ณณ์—์„œ๋Š” params์˜ id ๊ฐ’์„ ์ฐพ๋Š” ์ค‘ - isLoding ์ƒํƒœ
    4. ์ด์ „๊ณผ ๋‹ค๋ฅธ ๊ฐ’์„ ์ฐพ์•˜๊ธฐ ๋•Œ๋ฌธ์— 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 ํ•จ์ˆ˜(๋น„๋™๊ธฐ ์š”์ฒญ)์„ ์‹คํ–‰ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

๊ณต์ง€์‚ฌํ•ญ
์ตœ๊ทผ์— ์˜ฌ๋ผ์˜จ ๊ธ€
์ตœ๊ทผ์— ๋‹ฌ๋ฆฐ ๋Œ“๊ธ€
Total
Today
Yesterday
๋งํฌ
TAG
more
ยซ   2026/06   ยป
์ผ ์›” ํ™” ์ˆ˜ ๋ชฉ ๊ธˆ ํ† 
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
๊ธ€ ๋ณด๊ด€ํ•จ