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()
를 여러 번 호출하더라도 마찬가지입니다. 하지만 뮤테이션에 scope
와 id
를 지정하면 병렬 실행을 피할 수 있습니다. 동일한 scope.id
를 가진 뮤테이션들은 직렬로 실행되며, 이는 해당 스코프에 대해 이미 진행 중인 뮤테이션이 있을 경우 isPaused: true
상태에서 시작됩니다. 이 뮤테이션들은 대기열에 추가되며, 대기 시간이 끝나면 자동으로 재개됩니다.
const mutation = useMutation({
mutationFn: addTodo,
scope: {
id: "todo",
},
});
Further reading
뮤테이션에 대한 자세한 내용은 #12: Mastering Mutations in React Query에서 확인하실 수 있습니다.