키모스토리

TanStack Query (react-query) - #2 (useMutation, 등록/수정/삭제) 본문

Web Devlopment/NextJs

TanStack Query (react-query) - #2 (useMutation, 등록/수정/삭제)

키모형 2026. 1. 18. 16:24
반응형

이전 강의에 이어서 테스트를 위해 json-server 설치 (useMutation 테스트용)

 

 json-server 설치

npm i json-server

 

db.json 파일 추가 (테스트용 db파일)

{
  "posts": [
    {
      "id": 1,
      "title": "첫 번째 포스트",
    },
    {
      "id": 2,
      "title": "두 번째 포스트",
    }
  ]
}

 

파일 저장후 npx json-server db.json 으로 json server 실행

npx json-server db.json


JSON Server started on PORT :3000
Press CTRL-C to stop
Watching db.json...

♡( ◡‿◡ )

Index:
http://localhost:3000/

Static files:
Serving ./public directory if it exists

Endpoints:
http://localhost:3000/posts

 

 

TodoListLocal.tsx

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

export default function TodoListLocalPage() {
  const { data, isPending, isFetching, isError, error, refetch } = useQuery({
    queryKey: ["todoListLocal"], // 각 쿼리를 식별하는 고유 키
    queryFn: async () => {
      const response = await fetch("http://localhost:3000/posts");
      return await response.json();
    },
  });

  // 에러 상태 처리
  if (isError) {
    return <span>애러발생: {error.message}</span>;
  }

  return (
    <div>
      <h1>Local Todo List</h1>
      <p>isPending: {isPending ? "Pending..." : "완료"}</p>
      <p>isFetching: {isFetching ? "Fetching..." : "완료"}</p>

      <button onClick={() => refetch()}>Refetch</button>
      <ul>
        {data?.map((todo: any) => (
          <li key={todo.id}>
            {todo.id} - {todo.title}
          </li>
        ))}
      </ul>
    </div>
  );
}

 

 

등록/삭제 추가 

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

type Post = { id: string; title: string };
type PostCreate = { title: string };

const api = axios.create({
  baseURL: "http://localhost:3000",
  headers: { "Content-Type": "application/json" },
});

export default function TodoListLocalPage() {
  const qc = useQueryClient();
  const [title, setTitle] = useState("");

  // 포스트 목록 조회 쿼리
  const {
    data = [],
    isPending,
    isFetching,
    isError,
    error,
    refetch,
  } = useQuery<Post[]>({
    queryKey: ["posts"],
    queryFn: async () => {
      const res = await api.get<Post[]>("/posts");
      return res.data;
    },
  });

  // 등록함수
  const addPost = async (input: PostCreate): Promise<Post> => {
    // 1초 지연 시뮬레이션
    await new Promise((resolve) => setTimeout(resolve, 1000));
    const payload: Post = { id: uuidv4(), title: input.title.trim() };
    const res = await api.post<Post>("/posts", payload);
    return res.data;
  };

  // 등록 뮤테이션
  const { mutate, isPending: isAdding } = useMutation({
    mutationFn: addPost,
    onSuccess: () => {
      setTitle("");
      // refetch() 대신 캐시 무효화 → 자동 재조회
      qc.invalidateQueries({ queryKey: ["posts"] });
    },
    onError: (err) => {
      const msg = axios.isAxiosError(err)
        ? (err.response?.data?.message ?? err.message)
        : (err as Error).message;
      console.error("등록 실패:", msg);
    },
  });

  // 등록 폼 제출 핸들러
  const submitHandler = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const t = title.trim();
    if (!t) return;
    mutate({ title: t });
  };

  // 삭제함수
  const deletePost = async (id: string): Promise<void> => {
    // 1초 지연 시뮬레이션
    await new Promise((resolve) => setTimeout(resolve, 1000));
    await api.delete("/posts/" + id);
  };

  // 삭제 뮤테이션
  const { mutate: deletePostMutate, isPending: isDeleting } = useMutation({
    mutationFn: deletePost,
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ["posts"] });
    },
    onError: (err) => {
      const msg = axios.isAxiosError(err)
        ? (err.response?.data?.message ?? err.message)
        : (err as Error).message;
      console.error("삭제 실패:", msg);
    },
  });

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

  return (
    <div>
      <h1>Local Todo List (UUID)</h1>
      <p>isPending: {isPending ? "Pending..." : "완료"}</p>
      <p>isFetching: {isFetching ? "Fetching..." : "완료"}</p>

      <form onSubmit={submitHandler}>
        <input
          type="text"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="새 포스트 입력"
        />
        <button type="submit" disabled={isAdding}>
          {isAdding ? "등록중..." : "등록"}
        </button>
      </form>

      <hr />
      <ul>
        {data.map((p) => (
          <li key={p.id}>
            {p.id} - {p.title}
            <button
              onClick={() => deletePostMutate(p.id)}
              disabled={isDeleting}
            >
              {isDeleting ? "삭제중..." : "삭제"}
            </button>
          </li>
        ))}
      </ul>
    </div>
  );
}

 

반응형