Docs
GUIDES & CONCEPTS
Mutations

Mutations

쿼리와는 달리, 뮤테이션은 일반적으로 데이터를 생성하거나 업데이트하거나 삭제하거나 서버 측 효과를 수행하는 데 사용됩니다. 이러한 용도로 TanStack Query는 useMutation 훅을 제공합니다.

다음은 서버에 새 todo를 추가하는 뮤테이션의 예입니다:

function App() {
  const mutation = useMutation({
    mutationFn: (newTodo) => {
      return axios.post("/todos", newTodo);
    },
  });
 
  return (
    <div>
      {mutation.isPending ? (
        "Adding todo..."
      ) : (
        <>
          {mutation.isError ? (
            <div>An error occurred: {mutation.error.message}</div>
          ) : null}
 
          {mutation.isSuccess ? <div>Todo added!</div> : null}
 
          <button
            onClick={() => {
              mutation.mutate({ id: new Date(), title: "Do Laundry" });
            }}
          >
            Create Todo
          </button>
        </>
      )}
    </div>
  );
}

뮤테이션은 언제든지 다음 중 하나의 상태에만 있을 수 있습니다:

  • isIdle 또는 status === 'idle' - 뮤테이션이 현재 유휴 상태이거나 초기화/리셋된 상태
  • isPending 또는 status === 'pending' - 뮤테이션이 현재 실행 중
  • isError 또는 status === 'error' - 뮤테이션이 오류를 만남
  • isSuccess 또는 status === 'success' - 뮤테이션이 성공적이며 뮤테이션 데이터가 사용 가능

이 주요 상태 외에도 뮤테이션의 상태에 따라 추가 정보를 얻을 수 있습니다:

  • error - 뮤테이션이 error 상태에 있는 경우, error 속성을 통해 오류를 확인할 수 있습니다.
  • data - 뮤테이션이 success 상태에 있는 경우, data 속성을 통해 데이터를 확인할 수 있습니다.

위의 예제에서는 단일 변수 또는 객체를 사용하여 mutate 함수로 뮤테이션 함수에 변수를 전달하는 방법도 보셨습니다.

변수만으로는 뮤테이션이 특별할 게 없지만, onSuccess 옵션과 Query Client의 invalidateQueries 메서드Query Client의 setQueryData 메서드와 함께 사용하면, 뮤테이션은 매우 강력한 도구가 됩니다.

중요: mutate 함수는 비동기 함수이므로, React 16 및 이전 버전에서는 이벤트 콜백에서 직접 사용할 수 없습니다. onSubmit에서 이벤트에 접근해야 하는 경우, mutate를 다른 함수로 래핑해야 합니다. 이는 React 이벤트 풀링 (opens in a new tab) 때문입니다. //: # "Info1" //: # "Example2"

// This will not work in React 16 and earlier
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (event) => {
      event.preventDefault();
      return fetch("/api", new FormData(event.target));
    },
  });
 
  return <form onSubmit={mutation.mutate}>...</form>;
};
 
// This will work
const CreateTodo = () => {
  const mutation = useMutation({
    mutationFn: (formData) => {
      return fetch("/api", formData);
    },
  });
  const onSubmit = (event) => {
    event.preventDefault();
    mutation.mutate(new FormData(event.target));
  };
 
  return <form onSubmit={onSubmit}>...</form>;
};

Resetting Mutation State

때때로 뮤테이션 요청의 error 또는 data를 지워야 할 필요가 있습니다. 이를 처리하기 위해 reset 함수를 사용할 수 있습니다:

const CreateTodo = () => {
  const [title, setTitle] = useState("");
  const mutation = useMutation({ mutationFn: createTodo });
 
  const onCreateTodo = (e) => {
    e.preventDefault();
    mutation.mutate({ title });
  };
 
  return (
    <form onSubmit={onCreateTodo}>
      {mutation.error && (
        <h5 onClick={() => mutation.reset()}>{mutation.error}</h5>
      )}
      <input
        type="text"
        value={title}
        onChange={(e) => setTitle(e.target.value)}
      />
      <br />
      <button type="submit">Create Todo</button>
    </form>
  );
};

Mutation Side Effects

useMutation은 뮤테이션 생애 주기 동안 어느 단계에서나 빠르고 쉽게 사이드 이펙트를 처리할 수 있는 몇 가지 헬퍼 옵션을 제공합니다. 이러한 옵션들은 뮤테이션 후 쿼리 무효화 및 재패칭낙관적 업데이트에도 유용합니다.

useMutation({
  mutationFn: addTodo,
  onMutate: (variables) => {
    // A mutation is about to happen!
 
    // Optionally return a context containing data to use when for example rolling back
    return { id: 1 };
  },
  onError: (error, variables, context) => {
    // An error happened!
    console.log(`rolling back optimistic update with id ${context.id}`);
  },
  onSuccess: (data, variables, context) => {
    // Boom baby!
  },
  onSettled: (data, error, variables, context) => {
    // Error or success... doesn't matter!
  },
});

콜백 함수에서 프로미스를 반환하면, 다음 콜백이 호출되기 전에 먼저 프로미스가 해결될 때까지 기다립니다:

useMutation({
  mutationFn: addTodo,
  onSuccess: async () => {
    console.log("I'm first!");
  },
  onSettled: async () => {
    console.log("I'm second!");
  },
});

mutate를 호출할 때 useMutation에서 정의한 콜백 외에도 추가 콜백을 트리거하고 싶을 수 있습니다. 이는 컴포넌트 특정의 사이드 이펙트를 트리거하는 데 유용할 수 있습니다. 이를 위해서는 뮤테이션 변수 뒤에 mutate 함수에 동일한 콜백 옵션을 제공하면 됩니다. 지원되는 옵션으로는 onSuccess, onError, onSettled가 있습니다. 다만, 컴포넌트가 뮤테이션이 완료되기 전에 언마운트되면 이러한 추가 콜백은 실행되지 않는다는 점을 유의하세요.

useMutation({
  mutationFn: addTodo,
  onSuccess: (data, variables, context) => {
    // I will fire first
  },
  onError: (error, variables, context) => {
    // I will fire first
  },
  onSettled: (data, error, variables, context) => {
    // I will fire first
  },
});
 
mutate(todo, {
  onSuccess: (data, variables, context) => {
    // I will fire second!
  },
  onError: (error, variables, context) => {
    // I will fire second!
  },
  onSettled: (data, error, variables, context) => {
    // I will fire second!
  },
});

Consecutive mutations

연속적인 뮤테이션에서 onSuccess, onError, onSettled 콜백 처리에 약간의 차이가 있습니다. mutate 함수에 전달될 때, 이 콜백들은 한 번만 실행되며, 컴포넌트가 여전히 마운트되어 있을 때만 실행됩니다. 이는 mutate 함수가 호출될 때마다 뮤테이션 옵저버가 제거되고 다시 구독되기 때문입니다. 반면, useMutation의 핸들러는 각 mutate 호출마다 실행됩니다.

대부분의 경우, useMutation에 전달된 mutationFn은 비동기 함수입니다. 이 경우, 뮤테이션이 이행되는 순서는 mutate 함수 호출의 순서와 다를 수 있습니다.

useMutation({
  mutationFn: addTodo,
  onSuccess: (data, error, variables, context) => {
    // Will be called 3 times
  },
});
 
const todos = ["Todo 1", "Todo 2", "Todo 3"];
todos.forEach((todo) => {
  mutate(todo, {
    onSuccess: (data, error, variables, context) => {
      // Will execute only once, for the last mutation (Todo 3),
      // regardless which mutation resolves first
    },
  });
});

Promises

mutateAsync를 사용하여 성공 시에는 해결되고 오류 발생 시에는 예외를 던지는 프로미스를 얻을 수 있습니다. 이를 통해 부수 효과를 구성할 수 있습니다.

const mutation = useMutation({ mutationFn: addTodo });
 
try {
  const todo = await mutation.mutateAsync(todo);
  console.log(todo);
} catch (error) {
  console.error(error);
} finally {
  console.log("done");
}

Retry

기본적으로 TanStack Query는 오류 발생 시 뮤테이션을 재시도하지 않지만, retry 옵션을 통해 재시도가 가능합니다.

const mutation = useMutation({
  mutationFn: addTodo,
  retry: 3,
});

장치가 오프라인 상태로 인해 뮤테이션이 실패한 경우, 장치가 다시 연결되면 동일한 순서로 재시도됩니다.

Persist mutations

필요에 따라 뮤테이션을 저장소에 영속화하고 나중에 재개할 수 있습니다. 이는 hydration 함수들을 사용하여 수행할 수 있습니다.

const queryClient = new QueryClient();
 
// Define the "addTodo" mutation
queryClient.setMutationDefaults(["addTodo"], {
  mutationFn: addTodo,
  onMutate: async (variables) => {
    // 현재 todos 리스트 쿼리 취소
    await queryClient.cancelQueries({ queryKey: ["todos"] });
 
    // 낙관적 todo 생성
    const optimisticTodo = { id: uuid(), title: variables.title };
 
    // todos 리스트에 낙관적 todo 추가
    queryClient.setQueryData(["todos"], (old) => [...old, optimisticTodo]);
 
    // 낙관적 todo와 함께 컨텍스트 반환
    return { optimisticTodo };
  },
  onSuccess: (result, variables, context) => {
    // 낙관적 todo를 결과로 교체
    queryClient.setQueryData(["todos"], (old) =>
      old.map((todo) => (todo.id === context.optimisticTodo.id ? result : todo))
    );
  },
  onError: (error, variables, context) => {
    // 낙관적 todo를 todos 리스트에서 제거
    queryClient.setQueryData(["todos"], (old) =>
      old.filter((todo) => todo.id !== context.optimisticTodo.id)
    );
  },
  retry: 3,
});
 
// 컴포넌트에서 뮤테이션 시작:
const mutation = useMutation({ mutationKey: ["addTodo"] });
mutation.mutate({ title: "title" });
 
// 장치가 오프라인 상태로 인해 뮤테이션이 일시 중지된 경우,
// 어플리케이션 종료 시 일시 중지된 뮤테이션을 탈수할 수 있습니다:
const state = dehydrate(queryClient);
 
// 어플리케이션이 시작되면 뮤테이션을 다시 수화할 수 있습니다:
hydrate(queryClient, state);
 
// 일시 중지된 뮤테이션 재개:
queryClient.resumePausedMutations();

Persisting Offline mutations

오프라인 뮤테이션을 persistQueryClient 플러그인으로 영속화할 경우, 페이지를 새로 고침했을 때 뮤테이션을 재개할 수 없으므로 기본 뮤테이션 함수를 제공해야 합니다.

이는 기술적인 제한으로, 외부 저장소에 저장할 때는 뮤테이션의 상태만 저장되며, 함수는 직렬화할 수 없기 때문입니다. 수화 후에는 뮤테이션을 트리거하는 컴포넌트가 마운트되지 않을 수 있으므로, resumePausedMutations를 호출할 때 No mutationFn found 오류가 발생할 수 있습니다.

const persister = createSyncStoragePersister({
  storage: window.localStorage,
});
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 60 * 24, // 24시간
    },
  },
});
 
// 페이지 새로 고침 후 일시 중지된 뮤테이션을 재개할 수 있도록 기본 뮤테이션 함수가 필요합니다
queryClient.setMutationDefaults(["todos"], {
  mutationFn: ({ id, data }) => {
    return api.updateTodo(id, data);
  },
});
 
export default function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister }}
      onSuccess={() => {
        // 로컬 스토리지에서 초기 복원이 성공한 후 뮤테이션 재개
        queryClient.resumePausedMutations();
      }}
    >
      <RestOfTheApp />
    </PersistQueryClientProvider>
  );
}

또한, 오프라인 예제 (opens in a new tab)에서는 쿼리와 뮤테이션 모두를 다루는 광범위한 예제를 제공합니다.

Mutation Scopes

기본적으로 모든 뮤테이션은 병렬로 실행됩니다. 동일한 뮤테이션의 .mutate()를 여러 번 호출하더라도 마찬가지입니다. 하지만 뮤테이션에 scopeid를 지정하면 병렬 실행을 피할 수 있습니다. 동일한 scope.id를 가진 뮤테이션들은 직렬로 실행되며, 이는 해당 스코프에 대해 이미 진행 중인 뮤테이션이 있을 경우 isPaused: true 상태에서 시작됩니다. 이 뮤테이션들은 대기열에 추가되며, 대기 시간이 끝나면 자동으로 재개됩니다.

const mutation = useMutation({
  mutationFn: addTodo,
  scope: {
    id: "todo",
  },
});

Further reading

뮤테이션에 대한 자세한 내용은 #12: Mastering Mutations in React Query에서 확인하실 수 있습니다.