데이터베이스에 새로운 값을 추가하거나 수정, 삭제하는 행위는 사이드 이펙트에 해당한다. 그리고 이렇게 사이드 이펙트가 발생하는 경우에 useMutation()
이라는 훅을 사용한다.
useMutation()
은 useQuery()
와 차이점이 있다. useQuery()
의 쿼리 함수는 컴포넌트가 마운트되면서 자동으로 실행되지만, useMutation()
은 실제로 뮤테이션하는 함수를 직접 실행해 줘야 한다. mutate()
함수를 통해 mutationFn
으로 등록했던 함수를 실행할 수 있고, 그래야만 백엔드 데이터를 실제로 수정하게 된다.
참고로 mutate()
를 하면 백엔드의 데이터는 변경이 되지만, 현재 캐시에 저장된 데이터는 refetch를 하지 않는 이상 기존의 데이터가 그대로 저장되어 있다. 따라서 refetch를 해줘야만 변경된 데이터를 화면에 제대로 반영할 수 있다.
// 포스트 업로드를 요청하는 API 함수
export async function uploadPost(newPost) {
const response = await fetch(`${BASE_URL}/posts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(newPost),
});
if (!response.ok) {
throw new Error('Failed to upload the post.');
}
return await response.json();
}
// HomePage.js
const [content, setContent] = useState('');
// ...
const handleInputChange = (e) => {
setContent(e.target.value);
}
const handleSubmit = (e) => {
e.preventDefault();
const newPost = { username: 'codeit', content };
// ...
};
return (
<>
<div>
<form onSubmit={handleSubmit}>
<textarea
name='content'
value={content}
onChange={handleInputChange}
/>
<button
disabled={!content}
type='submit'
>
업로드
</button>
</form>
</div>
<div>
<ul>
{posts.map((post) => (
<li key={post.id}>
{post.user.name}: {post.content}
</li>
))}
</ul>
</div>
</>
);
const uploadPostMutation = useMutation({
mutationFn: (newPost) => uploadPost(newPost),
});
const handleSubmit = (e) => {
e.preventDefault();
const newPost = { username: 'codeit', content };
uploadPostMutation.mutate(newPost);
setContent('');
};
useMutation()
을 작성하고, 업로드 버튼을 눌렀을 때 mutate()
함수를 실행하도록 함위와 같은 상황에서 쿼리 클라이언트의 invalidateQueries()
라는 함수를 사용하면 업로드가 끝난 이후에 자동으로 refetch를 하도록 설정할 수 있다.
invalidateQueries()
는 말 그대로 캐시에 있는 모든 쿼리 혹은 특정 쿼리들을 invalidate하는 함수이다. (캐시에 저장된 쿼리를 '무효화한다'는 의미)
쿼리를 invalidate하면 해당 쿼리를 통해 받아 온 데이터를 stale time이 지났는지 아닌지에 상관없이 무조건 stale 상태로 만들고, 해당 데이터를 백그라운드에서 refetch하게 된다.
쿼리 클라이언트는 useQueryClient()
훅을 사용해서 가져올 수 있고, 원하는 시점에 queryClient.invalidateQueries()
함수를 실행하면 된다.
import { useQueryClient } from '@tanstack/react-query'
const queryClient = useQueryClient();
// ...
queryClient.invalidateQueries();
그럼 언제 쿼리를 invalidate해야 할까? 뮤데이션 객체에는 onMutate
, onSuccess
, onError
, onSettled
와 같은 주요 옵션들이 있어서 뮤테이션 사이클에 따라 적절한 동작을 추가할 수 있다.
뮤테이션이 성공한 시점(onSuccess
)에 ['post']
쿼리를 invalidate해 주는 함수를 콜백으로 등록해 주면 된다.
const queryClient = useQueryClient();
// ...
const uploadPostMutation = useMutation({
mutationFn: (newPost) => uploadPost(newPost),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
onSuccess
, onError
, onSettled
와 같은 옵션은 useMutation()
에서도 사용할 수 있고 mutate()
함수에서도 사용할 수 있다. 이때 useMutation()
에 등록한 콜백 함수들이 먼저 실행되고, 그다음에 mutate()
에 등록한 콜백 함수들이 실행된다.
const uploadPostMutation = useMutation({
mutationFn: (newPost) => uploadPost(newPost),
onSuccess: () => {
console.log('onSuccess in useMutation');
},
onSettled: () => {
console.log('onSettled in useMutation');
},
});
...
uploadPostMutation.mutate(newPost, {
onSuccess: () => {
console.log('onSuccess in mutate');
},
onSettled: () => {
console.log('onSettled in mutate');
},
});
한 가지 주의할 점은 useMutaion()
에 등록된 콜백 함수들은 컴포넌트가 언마운트되더라도 실행이 되지만, mutate()
의 콜백 함수들은 만약 뮤테이션이 끝나기 전에 해당 컴포넌트가 언마운트되면 실행이 되지 않는다.
따라서 query invalidation과 같이 뮤테이션 과정에서 꼭 필요한 로직은 useMutation()을 통해 등록하고, 그 외에 다른 페이지로 리다이렉트한다든가, 혹은 결과를 토스트로 띄워주는 것과 같이 해당 컴포넌트에 종속적인 로직은 mutate()
를 통해 등록해 주면 된다.
...
const uploadPostMutation = useMutation({
mutationFn: (newPost) => uploadPost(newPost),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
const handleUploadPost = (newPost) => {
uploadPostMutation.mutate(newPost, {
onSuccess: () => {
toast('포스트가 성공적으로 업로드 되었습니다!');
},
});
};
포스트가 업로드되는 중에는 중복해서 업로드 되면 안 되므로 버튼을 비활성화한다.
뮤테이션에는 isPending
이라는 값이 있다. 아래와 같이 uploadPostMutation.isPending
값을 이용하면 간단히 구현할 수 있다.
const uploadPostMutation = useMutation({
mutationFn: (newPost) => uploadPost(newPost),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['posts'] });
},
});
// ...
<button
disabled={uploadPostMutation.isPending || !content}
type='submit'
>
업로드
</button>