useMutation: 데이터 변경하기

useQuery가 데이터를 읽는 것이라면, useMutation은 데이터를 변경하는 것입니다. POST, PUT, PATCH, DELETE 같은 서버 데이터 변경 작업에 사용합니다.

기본 사용법

import { useMutation } from '@tanstack/react-query';
 
function CreateTodoForm() {
  const mutation = useMutation({
    mutationFn: (newTodo) =>
      fetch('/api/todos', {
        method: 'POST',
        body: JSON.stringify(newTodo),
        headers: { 'Content-Type': 'application/json' },
      }).then(res => res.json()),
  });
 
  function handleSubmit(e) {
    e.preventDefault();
    mutation.mutate({ title: '새 할 일', completed: false });
  }
 
  return (
    <form onSubmit={handleSubmit}>
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? '저장 중...' : '추가'}
      </button>
      {mutation.isError && <p>오류: {mutation.error.message}</p>}
      {mutation.isSuccess && <p>저장 완료!</p>}
    </form>
  );
}

useMutationmutate 함수를 반환합니다. 이 함수를 호출할 때 데이터가 전달됩니다.

콜백: onSuccess, onError, onSettled

뮤테이션 결과에 따라 다른 동작을 실행할 수 있습니다.

const mutation = useMutation({
  mutationFn: createTodo,
 
  onSuccess: (data) => {
    // data: 서버가 반환한 생성된 할 일
    console.log('생성됨:', data);
    alert('할 일이 추가됐습니다!');
  },
 
  onError: (error) => {
    console.error('실패:', error.message);
    alert('저장에 실패했습니다.');
  },
 
  onSettled: () => {
    // 성공/실패 모두 실행
    console.log('뮤테이션 완료');
  },
});

또는 mutate 호출 시 직접 콜백을 넘길 수 있습니다.

mutation.mutate(newTodo, {
  onSuccess: (data) => {
    console.log('성공:', data);
  },
  onError: (error) => {
    console.error('실패:', error);
  },
});

useMutation 옵션과 mutate 옵션 둘 다 있으면 모두 실행됩니다. 전역 처리는 useMutation에, 호출별 처리는 mutate에 작성하면 됩니다.

뮤테이션 후 쿼리 무효화

뮤테이션이 성공하면 관련 쿼리 캐시를 무효화해서 최신 데이터를 다시 가져오게 합니다. 이것이 React Query에서 가장 중요한 패턴 중 하나입니다.

import { useMutation, useQueryClient } from '@tanstack/react-query';
 
function CreateTodo() {
  const queryClient = useQueryClient();
 
  const mutation = useMutation({
    mutationFn: createTodo,
    onSuccess: () => {
      // 'todos' 쿼리를 무효화 → 자동으로 다시 fetch
      queryClient.invalidateQueries({ queryKey: ['todos'] });
    },
  });
 
  return (
    <button onClick={() => mutation.mutate({ title: '새 항목' })}>
      추가
    </button>
  );
}

새 할 일을 만들면 → onSuccess에서 ['todos'] 쿼리 무효화 → 할 일 목록이 자동으로 최신 데이터로 갱신됩니다.

mutateAsync: Promise 사용

mutate는 fire-and-forget 방식입니다. 결과를 await하고 싶다면 mutateAsync를 사용합니다.

async function handleSubmit() {
  try {
    const newTodo = await mutation.mutateAsync({ title: '새 할 일' });
    // 성공 후 다음 작업
    router.push(`/todos/${newTodo.id}`);
  } catch (error) {
    console.error('실패:', error);
  }
}

mutateAsync는 에러를 throw하므로 반드시 try/catch로 감싸야 합니다.

실전 예제: CRUD

function TodoApp() {
  const queryClient = useQueryClient();
 
  // 목록 조회
  const { data: todos } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  });
 
  // 생성
  const createMutation = useMutation({
    mutationFn: (title) => api.post('/todos', { title }),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
  });
 
  // 수정
  const updateMutation = useMutation({
    mutationFn: ({ id, ...data }) => api.put(`/todos/${id}`, data),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
  });
 
  // 삭제
  const deleteMutation = useMutation({
    mutationFn: (id) => api.delete(`/todos/${id}`),
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
  });
 
  return (
    <div>
      <button onClick={() => createMutation.mutate('새 할 일')}>추가</button>
      {todos?.map(todo => (
        <div key={todo.id}>
          <span>{todo.title}</span>
          <button onClick={() => updateMutation.mutate({ id: todo.id, completed: true })}>
            완료
          </button>
          <button onClick={() => deleteMutation.mutate(todo.id)}>삭제</button>
        </div>
      ))}
    </div>
  );
}

반환값 정리

const {
  mutate,       // 뮤테이션 실행 (결과 무시)
  mutateAsync,  // 뮤테이션 실행 (Promise 반환)
  isPending,    // 실행 중
  isSuccess,    // 성공
  isError,      // 에러 발생
  error,        // 에러 객체
  data,         // 서버 응답 데이터
  reset,        // 상태를 idle로 초기화
} = useMutation({ mutationFn: ... });

useQuery vs useMutation

useQueryuseMutation
목적데이터 읽기 (GET)데이터 변경 (POST/PUT/DELETE)
실행 시점자동 (마운트 시)수동 (mutate 호출 시)
캐싱있음없음
재시도자동기본적으로 없음
로딩 상태isLoading, isFetchingisPending