0398

FE

[React] TanStack Query(React Query) 활용하기 - useMutation으로 데이터 생성·수정·삭제 마스터하기

| 서론

안녕하세요, 팡일입니다.

지난 포스팅에서는 useQuery와 캐시 생명주기를 중심으로, 서버 데이터를 ‘읽어오는(Read)’ 방식에 대해 깊이 있게 살펴보았습니다.

이번 포스팅에서는 그 데이터를 ‘변경하는(Change)’ 작업, 즉 생성(Create), 수정(Update), 삭제(Delete)를 담당하는 useMutation 에 대해 알아보겠습니다.

| 들어가며: 데이터 변경의 어려움과 useMutation

서버 데이터를 변경하는 작업은 단순히 API를 한 번 호출하는 것으로 끝나지 않습니다.

실제 서비스에서는 다음과 같은 여러 상황을 함께 고려해야 합니다.

  • 요청이 진행 중일 때 사용자에게 로딩 상태 표시 (예: 스피너 노출, 버튼 비활성화)

  • 요청이 성공했을 때 사용자에게 적절한 피드백 제공

  • 성공 이후 화면의 데이터를 최신 상태로 동기화

  • 요청이 실패했을 때 에러 처리 및 사용자 안내

  • 성공/실패 여부와 관계없이 입력 폼 초기화 등의 후속 처리

이 모든 흐름을 직접 관리하려면 코드가 빠르게 복잡해집니다.

useMutation은 이러한 데이터 변경 과정을 훨씬 선언적이고 예측 가능한 방식으로 관리할 수 있도록 도와줍니다.

| 핵심 파헤치기: useMutation 기본 사용법

useMutation의 기본 구조는 useQuery와 매우 유사합니다.

  • 실제 데이터 변경을 수행하는 비동기 함수 mutationFn에 등록하고

  • 반환된 mutate 함수를 호출하여 원하는 시점에 실행합니다.

1) Todo 추가 기능 예제 코드

프로젝트의 TodoList.jsx 파일에 있는 할 일(Todo) 추가 기능을 살펴보겠습니다.

code
// 서버에 새로운 할 일을 추가하는 비동기 함수입니다.
const addPost = async (post) => {
  const response = await axios.post("http://localhost:3000/posts", post);
  return response.data;
};

// useMutation 훅을 사용하여 할 일 추가 기능을 구현합니다.
const { mutate, isPending } = useMutation({
  mutationFn: addPost,
  // ... (생명주기 콜백)
});

const submitHandler = (e) => {
  e.preventDefault();
  // mutate 함수를 호출하여 새로운 할 일을 추가합니다.
  mutate({
    id: uuidv4(),
    title,
  });
};

각 속성의 역할은 다음과 같습니다.

  • mutationFn→ 실제 데이터 변경(POST, PUT, DELETE 등)이 수행되는 비동기 함수

  • mutate mutationFn을 실행시키는 트리거 함수 mutate에 전달한 인자는 그대로 mutationFn의 인자로 전달됩니다.

  • isPending→ 현재 뮤테이션이 진행 중인지 나타내는 boolean 값→ 버튼 비활성화, 로딩 UI 처리 등에 매우 유용합니다.

| useMutation의 생명주기(Side Effects) 활용하기

useMutation의 진짜 강력함은 사이드 이펙트를 선언적으로 처리할 수 있는 콜백 함수들에 있습니다.

JavaScript
const { mutate, isPending } = useMutation({
  mutationFn: addPost,

  // 뮤테이션이 성공하면 호출됩니다.
  onSuccess: () => {
    alert("등록 완료!");
    // ... (데이터 동기화 코드)
  },

  // 뮤테이션이 실패하면 호출됩니다.
  onError: () => {
    alert("실패 ㅠㅠ");
  },

  // 성공/실패 여부와 관계없이 항상 호출됩니다.
  onSettled: () => {
    setTitle(""); // 입력 필드를 초기화합니다.
  },
});

각 콜백의 역할은 다음과 같습니다.

  • onSuccess→ API 요청이 성공했을 때 실행→ 사용자 피드백, 데이터 갱신 트리거에 적합

  • onError→ 요청 실패 시 실행→ 에러 메시지 노출 등 에러 처리 담당

  • onSettled→ 성공/실패와 관계없이 항상 실행→ 폼 초기화, 로딩 종료 등 클린업 로직에 적합

| useQuery + useMutation의 환상적인 조합: 데이터 동기화

데이터를 성공적으로 추가했다면, 화면에 보이는 목록 역시 최신 상태로 갱신되어야 합니다.

이때 가장 일반적이고 강력한 방법이 바로 invalidateQueries를 사용하는 방식입니다.

1) invalidateQueries 사용 예시

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

const queryClient = useQueryClient();

const { mutate, isPending } = useMutation({
  mutationFn: addPost,
  onSuccess: () => {
    alert("등록 완료!");
    queryClient.invalidateQueries({ queryKey: ["pstList"] });
  },
});

invalidateQueries가 호출되면:

  1. queryKey: ["pstList"]를 가진 쿼리를 찾고

  2. 해당 쿼리를 Stale 상태로 변경한 뒤

  3. 화면에 렌더링 중이라면 즉시 백그라운드에서 refetch를 수행합니다.

이를 통해 우리는 “데이터 추가가 성공하면, 목록을 다시 불러와라” 라는 로직을 매우 선언적으로 표현할 수 있습니다.

| 실전 예제: 할 일 추가 및 삭제 기능 심층 분석

이제 TodoList.jsx 전체 흐름을 기준으로 추가 / 삭제 기능을 차례대로 살펴보겠습니다.

1) 할 일(Todo) 추가 기능

JavaScript
export default function TodoList() {
  const queryClient = useQueryClient();
  const [title, setTitle] = useState("");

  // 1. 데이터 조회
  const { data } = useQuery({ queryKey: ["pstList"], /* ... */ });

  // 2. 데이터 추가
  const { mutate, isPending } = useMutation({
    mutationFn: (newPost) =>
      axios.post("http://localhost:3000/posts", newPost),

    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["pstList"] });
    },

    onSettled: () => {
      setTitle("");
    },
  });

  const submitHandler = (e) => {
    e.preventDefault();
    // 3. mutate 호출
    mutate({ id: uuidv4(), title });
  };

  return (
    <form onSubmit={submitHandler}>
      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      {/* 4. 로딩 상태 처리 */}
      {isPending ? "등록중.." : null}
      <button type="submit" disabled={isPending}>
        등록
      </button>
    </form>
  );
}

2) 할 일(Todo) 삭제 기능

삭제 기능 역시 구조는 추가 기능과 거의 동일합니다.

다만 한 컴포넌트에서 여러 useMutation을 사용할 경우, mutate 함수의 이름 충돌을 피하기 위해 별칭(alias)을 사용합니다.

JavaScript
// 서버에서 할 일을 삭제하는 비동기 함수
const deletePost = async (postId) => {
  await axios.delete(`http://localhost:3000/posts/${postId}`);
};

// mutate 이름을 deleteMutate로 변경
const { mutate: deleteMutate } = useMutation({
  mutationFn: deletePost,
  onSuccess: () => {
    alert("삭제 완료!");
    queryClient.invalidateQueries({ queryKey: ["pstList"] });
  },
  onError: () => {
    alert("삭제 실패 ㅠㅠ");
  },
});

return (
  <ul>
    {data?.map((item) => (
      <li key={item.id}>
        {item.id} - {item.title}
        <button
          type="button"
          onClick={() => deleteMutate(item.id)}
        >
          삭제
        </button>
      </li>
    ))}
  </ul>
);

정리하면:

  • mutate를 deleteMutate로 이름 변경

  • 삭제 버튼 클릭 시 id를 인자로 전달

  • 삭제 성공 후에도 invalidateQueries로 목록 갱신

| 맺음말

useMutation을 사용하면 복잡해지기 쉬운 서버 데이터 변경 로직을 아주 간결하고 예측 가능하게 관리할 수 있습니다.

  • 로딩 / 성공 / 실패 상태 처리

  • 사이드 이펙트 관리

  • invalidateQueries를 통한 데이터 동기화

이 모든 것을 선언적인 코드로 해결할 수 있습니다.

이제 여러분의 프로젝트에 남아 있는 ‘수정(Update)’ 기능을 useMutation으로 직접 구현해보세요.