
프론트엔드 개발을 하다 보면 서버와의 데이터 통신은 빼놓을 수 없는 작업이다. 특히 데이터를 수정, 추가, 삭제하는 작업은 대부분의 애플리케이션에서 필수적으로 사용되는데, TanStack Query(구 react-query)에서는 이러한 '변경성 작업'을 useMutation 훅을 통해 간편하고 효율적으로 처리할 수 있다.
먼저 용어부터 짚고 넘어가 보자.
데이터를 '읽는(Fetching)' 것이 아닌, '변경(Create, Update, Delete)'하는 모든 작업을 의미한다.
예를 들어 사용자를 추가하거나(POST), 게시글을 수정하거나(PUT), 상품을 삭제하는(DELETE) 작업 등이 모두 mutation이다.
반면, 데이터를 조회하는(GET) 작업은 useQuery를 통해 처리한다.
TanStack Query의 useMutation 훅은 이러한 mutation 작업을 훨씬 더 간단하고, 효율적으로 처리할 수 있도록 도와준다.
import { useMutation } from '@tanstack/react-query'
import axios from 'axios'
const addUser = (newUser) => axios.post('/api/users', newUser)
function AddUserForm() {
const mutation = useMutation({
mutationFn: addUser,
onSuccess: () => {
alert('유저가 성공적으로 추가되었습니다!')
},
onError: (error) => {
console.error('에러 발생:', error)
}
})
const handleSubmit = (e) => {
e.preventDefault()
const newUser = { name: 'Sara', age: 25 }
mutation.mutate(newUser)
}
return <button onClick={handleSubmit}>유저 추가</button>
}
mutationFn: 실제 데이터를 변경하는 비동기 함수onSuccess: 성공 시 실행할 콜백onError: 에러 발생 시 실행할 콜백onSettled: 성공/실패 여부와 관계없이 항상 실행되는 콜백데이터를 변경한 후에는 기존 useQuery로 가져온 데이터를 새로 고쳐야 할 경우가 많다. 이럴 때는 queryClient.invalidateQueries로 관련 데이터를 무효화(invalidate)하면 된다.
import { useQueryClient } from '@tanstack/react-query'
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: addUser,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] })
}
})
useMutation은 다음과 같은 상태 값을 제공한다.
isLoading: 요청 중 여부isSuccess: 요청 성공 여부isError: 에러 발생 여부error: 에러 객체data: 성공 시 응답 데이터if (mutation.isLoading) return <p>추가 중입니다...</p>
if (mutation.isSuccess) return <p>추가 완료!</p>
if (mutation.isError) return <p>에러 발생: {mutation.error.message}</p>
이쯤에서 드는 의문이 있다.
"
axios로 직접 호출하면 되잖아? 굳이TanStack Query까지 써야 하나?"
물론 아래처럼 직접 상태를 관리하면서 API를 호출하는것도 가능하다.
const handleSubmit = async () => {
try {
setLoading(true)
await axios.post('/api/user', newUser)
setSuccess(true)
} catch (e) {
setError(e)
} finally {
setLoading(false)
}
}
하지만 이 방식은 몇 가지 단점이 있다.
loading, error, success 상태를 직접 구현해야 함
API 호출 후 목록을 새로 불러오는 등의 캐시 갱신 작업도 수동으로 해야 함
여러 곳에서 같은 mutation 로직을 사용할 경우 중복된 코드가 생기기 쉬움
isLoading, isError, isSuccess 같은 상태를 TanStack Query가 알아서 제공해준다.
로딩 스피너, 에러 메시지, 성공 메시지 등을 쉽게 처리할 수 있다.
const mutation = useMutation(...)
mutation.isLoading // 로딩 중인지
mutation.isError // 에러 발생했는지
mutation.error // 에러 정보
데이터가 바뀌었으면 기존 목록도 새로 받아와야 하잖아요?
queryClient.invalidateQueries를 활용하면 자동으로 해당 데이터 갱신이 가능하다.
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['userList'] })
}
여러 컴포넌트에서 동일한 mutation 로직을 재사용할 수 있다.
훅으로 캡슐화해 놓으면 가독성과 유지보수성도 훨씬 좋아진다.
// hooks/useCreateUser.ts
export const useCreateUser = () => {
return useMutation({ mutationFn: createUserAPI })
}
TanStack Query Devtools를 사용하면 요청 흐름과 상태를 시각적으로 확인할 수 있다.
(단순 fetch만 썼다면 직접 콘솔 로그를 찍거나 디버깅해야함)
실패 시 자동 재시도, delay 조절 등 다양한 옵션을 사용할 수 있다.
useMutation({
mutationFn,
retry: 3,
retryDelay: 1000,
})
| 상황 | 추천 방식 |
|---|---|
| 간단한 데모, 빠른 테스트용 | 그냥 axios/fetch |
| 데이터 변경이 복잡하지 않고, 재사용성이 낮을 때 | axios + 상태 직접 처리도 OK |
| ✅ 실서비스에서 상태 관리, 캐시 갱신, 재사용성, 일관된 UX가 필요할 때 | useMutation |
useMutation은 단순한 API 호출을 넘어, 상태 관리, 응답 처리, 캐시 동기화까지 한 번에 해결할 수 있는 강력한 도구다. 특히 실무에서는 반복적으로 마주치는 "추가 / 수정 / 삭제" 요청을 깔끔하게 추상화해주기 때문에, 일관된 UX를 유지하면서도 유지보수가 쉬운 코드를 작성할 수 있게 해준다.
직접 상태를 관리하는 방식(fetch/axios)도 가능하지만,
반면, useMutation을 사용하면
📌 실서비스에서 효율적인 데이터 변경 로직을 구현하고 싶다면, useMutation을 사용해보기를 적극추천한다!