| 서론
안녕하세요, 팡일입니다.
지난 포스팅에서는 useQuery와 캐시 생명주기를 중심으로, 서버 데이터를 ‘읽어오는(Read)’ 방식에 대해 깊이 있게 살펴보았습니다.
이번 포스팅에서는 그 데이터를 ‘변경하는(Change)’ 작업, 즉 생성(Create), 수정(Update), 삭제(Delete)를 담당하는 useMutation 훅에 대해 알아보겠습니다.
| 들어가며: 데이터 변경의 어려움과 useMutation
서버 데이터를 변경하는 작업은 단순히 API를 한 번 호출하는 것으로 끝나지 않습니다.
실제 서비스에서는 다음과 같은 여러 상황을 함께 고려해야 합니다.
- 요청이 진행 중일 때 사용자에게 로딩 상태 표시 (예: 스피너 노출, 버튼 비활성화)
- 요청이 성공했을 때 사용자에게 적절한 피드백 제공
- 성공 이후 화면의 데이터를 최신 상태로 동기화
- 요청이 실패했을 때 에러 처리 및 사용자 안내
- 성공/실패 여부와 관계없이 입력 폼 초기화 등의 후속 처리
이 모든 흐름을 직접 관리하려면 코드가 빠르게 복잡해집니다.
useMutation은 이러한 데이터 변경 과정을 훨씬 선언적이고 예측 가능한 방식으로 관리할 수 있도록 도와줍니다.
| 핵심 파헤치기: useMutation 기본 사용법
useMutation의 기본 구조는 useQuery와 매우 유사합니다.
- 실제 데이터 변경을 수행하는 비동기 함수를 mutationFn에 등록하고
- 반환된 mutate 함수를 호출하여 원하는 시점에 실행합니다.
1) Todo 추가 기능 예제 코드
프로젝트의 TodoList.jsx 파일에 있는 할 일(Todo) 추가 기능을 살펴보겠습니다.
// 서버에 새로운 할 일을 추가하는 비동기 함수입니다.
const addPost = async (post) => {
const response = await axios.post("http://localhost:3000/posts", post);
return response.data;
};
// useMutation 훅을 사용하여 할 일 추가 기능을 구현합니다.
const { mutate, isPending } = useMutation({
mutationFn: addPost,
// ... (생명주기 콜백)
});
const submitHandler = (e) => {
e.preventDefault();
// mutate 함수를 호출하여 새로운 할 일을 추가합니다.
mutate({
id: uuidv4(),
title,
});
};
각 속성의 역할은 다음과 같습니다.
- mutationFn
→ 실제 데이터 변경(POST, PUT, DELETE 등)이 수행되는 비동기 함수 - mutate
→ mutationFn을 실행시키는 트리거 함수
→ mutate에 전달한 인자는 그대로 mutationFn의 인자로 전달됩니다. - isPending
→ 현재 뮤테이션이 진행 중인지 나타내는 boolean 값
→ 버튼 비활성화, 로딩 UI 처리 등에 매우 유용합니다.
| useMutation의 생명주기(Side Effects) 활용하기
useMutation의 진짜 강력함은 사이드 이펙트를 선언적으로 처리할 수 있는 콜백 함수들에 있습니다.
const { mutate, isPending } = useMutation({
mutationFn: addPost,
// 뮤테이션이 성공하면 호출됩니다.
onSuccess: () => {
alert("등록 완료!");
// ... (데이터 동기화 코드)
},
// 뮤테이션이 실패하면 호출됩니다.
onError: () => {
alert("실패 ㅠㅠ");
},
// 성공/실패 여부와 관계없이 항상 호출됩니다.
onSettled: () => {
setTitle(""); // 입력 필드를 초기화합니다.
},
});
각 콜백의 역할은 다음과 같습니다.
- onSuccess
→ API 요청이 성공했을 때 실행
→ 사용자 피드백, 데이터 갱신 트리거에 적합 - onError
→ 요청 실패 시 실행
→ 에러 메시지 노출 등 에러 처리 담당 - onSettled
→ 성공/실패와 관계없이 항상 실행
→ 폼 초기화, 로딩 종료 등 클린업 로직에 적합
| useQuery + useMutation의 환상적인 조합: 데이터 동기화
데이터를 성공적으로 추가했다면, 화면에 보이는 목록 역시 최신 상태로 갱신되어야 합니다.
이때 가장 일반적이고 강력한 방법이 바로 invalidateQueries를 사용하는 방식입니다.
1) invalidateQueries 사용 예시
import { useQueryClient } from "@tanstack/react-query";
const queryClient = useQueryClient();
const { mutate, isPending } = useMutation({
mutationFn: addPost,
onSuccess: () => {
alert("등록 완료!");
queryClient.invalidateQueries({ queryKey: ["pstList"] });
},
});
invalidateQueries가 호출되면:
- queryKey: ["pstList"]를 가진 쿼리를 찾고
- 해당 쿼리를 Stale 상태로 변경한 뒤
- 화면에 렌더링 중이라면 즉시 백그라운드에서 refetch를 수행합니다.
이를 통해 우리는 “데이터 추가가 성공하면, 목록을 다시 불러와라” 라는 로직을 매우 선언적으로 표현할 수 있습니다.
| 실전 예제: 할 일 추가 및 삭제 기능 심층 분석
이제 TodoList.jsx 전체 흐름을 기준으로 추가 / 삭제 기능을 차례대로 살펴보겠습니다.
1) 할 일(Todo) 추가 기능
export default function TodoList() {
const queryClient = useQueryClient();
const [title, setTitle] = useState("");
// 1. 데이터 조회
const { data } = useQuery({ queryKey: ["pstList"], /* ... */ });
// 2. 데이터 추가
const { mutate, isPending } = useMutation({
mutationFn: (newPost) =>
axios.post("http://localhost:3000/posts", newPost),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["pstList"] });
},
onSettled: () => {
setTitle("");
},
});
const submitHandler = (e) => {
e.preventDefault();
// 3. mutate 호출
mutate({ id: uuidv4(), title });
};
return (
<form onSubmit={submitHandler}>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
{/* 4. 로딩 상태 처리 */}
{isPending ? "등록중.." : null}
<button type="submit" disabled={isPending}>
등록
</button>
</form>
);
}
2) 할 일(Todo) 삭제 기능
삭제 기능 역시 구조는 추가 기능과 거의 동일합니다.
다만 한 컴포넌트에서 여러 useMutation을 사용할 경우, mutate 함수의 이름 충돌을 피하기 위해 별칭(alias)을 사용합니다.
// 서버에서 할 일을 삭제하는 비동기 함수
const deletePost = async (postId) => {
await axios.delete(`http://localhost:3000/posts/${postId}`);
};
// mutate 이름을 deleteMutate로 변경
const { mutate: deleteMutate } = useMutation({
mutationFn: deletePost,
onSuccess: () => {
alert("삭제 완료!");
queryClient.invalidateQueries({ queryKey: ["pstList"] });
},
onError: () => {
alert("삭제 실패 ㅠㅠ");
},
});
return (
<ul>
{data?.map((item) => (
<li key={item.id}>
{item.id} - {item.title}
<button
type="button"
onClick={() => deleteMutate(item.id)}
>
삭제
</button>
</li>
))}
</ul>
);
정리하면:
- mutate를 deleteMutate로 이름 변경
- 삭제 버튼 클릭 시 id를 인자로 전달
- 삭제 성공 후에도 invalidateQueries로 목록 갱신
| 맺음말
useMutation을 사용하면 복잡해지기 쉬운 서버 데이터 변경 로직을 아주 간결하고 예측 가능하게 관리할 수 있습니다.
- 로딩 / 성공 / 실패 상태 처리
- 사이드 이펙트 관리
- invalidateQueries를 통한 데이터 동기화
이 모든 것을 선언적인 코드로 해결할 수 있습니다.
이제 여러분의 프로젝트에 남아 있는 ‘수정(Update)’ 기능을 useMutation으로 직접 구현해보세요.
'💻 개발 > 🦕 React' 카테고리의 다른 글
| [React] React Router 중첩 라우트에서 index 대신 Navigate를 선택한 이유 (0) | 2026.01.19 |
|---|---|
| [React] React + Vite + TS + Firebase Hosting + Github Action 구축하기 (0) | 2026.01.18 |
| [React] TanStack Query(React Query) 깊게 파고들기 : useQuery와 캐시 생명주기의 모든 것 (0) | 2026.01.15 |
| [React] 코드 스플리팅을 적용했을 뿐인데, 초기 로딩이 줄어들었습니다 (0) | 2025.12.27 |
| [React] public 이미지에서 import 구조로 (이미지 최적화 개선기) (1) | 2025.12.27 |
