React Hook Form을 쓰다 보면 결국 마주치게 되는 단계가 있습니다.
“이제 폼 데이터를 서버에 보내야 하는데, 어디서 API 요청을 처리하지?”
이 지점에서 React Query를 함께 사용하면 폼과 서버 간의 흐름이 놀랄 만큼 깔끔해집니다.
이 강의에서는 React Hook Form의 handleSubmit과 React Query의 useMutation을 결합해,
폼 제출 시 비동기 요청을 어떻게 구조적으로 다룰 수 있는지 정리해보겠습니다.
예전엔 이렇게 했다가 코드가 엉망이 됐다
처음에는 React Hook Form만 쓰고, fetch나 axios로 직접 요청을 보냈습니다.
const onSubmit = async (data) => {
try {
const res = await axios.post('/api/signup', data);
console.log(res.data);
} catch (err) {
console.error(err);
}
};
이 방식이 문제 되는 건 아닙니다.
하지만 실무에서는 같은 API 요청을 여러 곳에서 써야 하고,
성공/실패에 따라 토스트 메시지를 띄우거나 리다이렉트도 해야 합니다.
이 모든 로직을 각 폼마다 직접 넣으면, 유지보수가 거의 불가능해집니다.
그때 React Query의 useMutation을 쓰기 시작하면서 구조가 확 달라졌습니다.
React Query와 결합하는 기본 패턴
React Hook Form과 React Query는 의외로 궁합이 아주 좋습니다.
핵심은 handleSubmit 안에서 mutation.mutate를 호출하는 것입니다.
import { useForm } from 'react-hook-form';
import { useMutation } from '@tanstack/react-query';
import axios from 'axios';
function SignupForm() {
const { register, handleSubmit, reset } = useForm();
const signupMutation = useMutation({
mutationFn: (data) => axios.post('/api/signup', data),
onSuccess: () => {
alert('회원가입이 완료되었습니다!');
reset();
},
onError: (error) => {
alert(`에러가 발생했습니다: ${error.message}`);
},
});
const onSubmit = (data) => signupMutation.mutate(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} placeholder="이메일" />
<input {...register('password')} placeholder="비밀번호" type="password" />
<button type="submit" disabled={signupMutation.isLoading}>
{signupMutation.isLoading ? '처리 중...' : '가입하기'}
</button>
</form>
);
}
이 패턴을 쓰면,
isLoading, isError, onSuccess를 전부 React Query가 관리해줍니다.
React Hook Form은 입력값만 책임지고,
서버 통신은 완전히 분리되는 거죠.
이걸 몰라서 한참 헤맸던 부분
React Query의 useMutation은 “데이터를 수정하는 요청”에만 쓰는 게 맞습니다.
저는 초반에 useQuery로도 폼 제출을 시도했는데,
그건 완전히 잘못된 접근이었습니다.
useQuery는 데이터를 가져오는(fetch) 용도이고,
useMutation은 데이터를 보내거나 변경하는(post/put/delete) 용도입니다.
한 번은 회원정보 수정 페이지에서 실수로 useQuery로 요청을 보냈는데,
서버에서는 계속 GET으로 처리돼서 405 에러가 떴던 적이 있습니다.
그때 “React Query는 역할을 구분해야 한다”는 걸 확실히 배웠습니다.
성공, 실패, 로딩 상태를 자연스럽게 UI에 녹이는 법
React Query의 진짜 매력은 상태 기반 UI 연동입니다.
{signupMutation.isLoading && <p>잠시만 기다려주세요...</p>}
{signupMutation.isError && <p>회원가입 중 문제가 발생했습니다.</p>}
{signupMutation.isSuccess && <p>가입이 완료되었습니다!</p>}
이렇게만 해도
비동기 흐름이 UI에 깔끔하게 드러납니다.
별도의 상태 변수(useState)를 추가하지 않아도 되니
코드가 훨씬 단순해지고 유지보수도 쉬워집니다.
실무에서 적용해보며 느낀 점
최근에 사내 어드민 프로젝트에서
React Hook Form + React Query 조합으로 사용자 등록 페이지를 만들었는데,
이전보다 코드량이 40%는 줄었습니다.
예전엔 try-catch 안에 토스트, 로딩 상태, 에러 핸들링을 다 넣었는데,
지금은 useMutation의 옵션에만 정의해두면 됩니다.
또한, onSuccess 안에서 React Query의 invalidateQueries를 호출하면
자동으로 캐시가 새로고침돼서
“등록 후 목록이 갱신되지 않는다”는 문제도 사라졌습니다.
결국 핵심은 ‘역할 분리’
React Hook Form은 입력값과 검증,
React Query는 데이터 전송과 상태 관리를 맡습니다.
이 둘을 섞지 않고 명확히 분리하는 게
코드를 읽기 쉽게 만드는 핵심입니다.
결국 “폼은 폼답게, 요청은 요청답게”라는 원칙을 지키는 구조가
유지보수에 가장 큰 힘이 됩니다.
다음 글에서는 이 구조를 한 단계 더 확장해서,
React Query와 Toast 시스템을 결합해 사용자에게 즉각적인 피드백을 주는 패턴을 다뤄보겠습니다.
UX 측면에서 실무에서 가장 자주 고민하는 부분이라
꼭 한 번 짚고 넘어갈 필요가 있습니다.