1. 폼과 서버 통신, 왜 어려운가?
리액트 프로젝트에서 회원가입, 로그인, 게시글 작성 같은 폼은 필수입니다.
하지만 많은 개발자가 다음과 같은 문제를 겪습니다.
- 입력 검증과 서버 요청 코드가 섞임
- 제출 중 로딩 상태나 에러 처리 누락
- 성공 후 데이터 리페칭이나 페이지 이동이 복잡함
이 문제를 해결하기 위한 가장 강력한 조합이 바로
React Hook Form + React Query입니다.
React Hook Form은 “입력값을 관리”하고,
React Query는 “서버와의 통신 상태를 관리”합니다.
두 라이브러리를 함께 사용하면
폼 로직과 네트워크 로직을 완벽히 분리할 수 있습니다.
2. 준비하기
두 라이브러리를 설치합니다.
npm install react-hook-form @tanstack/react-query axios
프로젝트 루트에서 React Query의 Provider를 설정합니다.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
const client = new QueryClient();
export default function Root() {
return (
<QueryClientProvider client={client}>
<App />
</QueryClientProvider>
);
}
3. 기본 예제 – 회원가입 폼
이제 실제로 React Hook Form과 React Query를 연결해봅시다.
import { useForm } from 'react-hook-form';
import { useMutation } from '@tanstack/react-query';
import axios from 'axios';
function SignupForm() {
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm();
// React Query Mutation
const mutation = useMutation({
mutationFn: async (formData) => {
const response = await axios.post('/api/signup', formData);
return response.data;
},
onSuccess: (data) => {
alert('회원가입이 완료되었습니다!');
reset(); // 폼 초기화
},
onError: (error) => {
alert(error.response?.data?.message || '서버 요청 실패');
},
});
const onSubmit = (data) => {
mutation.mutate(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)} style={{ width: '300px', margin: 'auto' }}>
<h3>회원가입</h3>
<div>
<label>이메일</label>
<input
{...register('email', {
required: '이메일은 필수 입력입니다.',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '올바른 이메일 형식을 입력해주세요.',
},
})}
/>
{errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}
</div>
<div>
<label>비밀번호</label>
<input
type="password"
{...register('password', {
required: '비밀번호는 필수입니다.',
minLength: {
value: 6,
message: '비밀번호는 최소 6자 이상이어야 합니다.',
},
})}
/>
{errors.password && <p style={{ color: 'red' }}>{errors.password.message}</p>}
</div>
<button type="submit" disabled={mutation.isLoading}>
{mutation.isLoading ? '처리 중...' : '회원가입'}
</button>
</form>
);
}
export default SignupForm;
작동 흐름
- handleSubmit()이 폼 데이터를 수집
- mutation.mutate(data)가 서버 요청 실행
- 성공 시 alert 후 reset()으로 초기화
- 실패 시 에러 메시지 표시
이 구조에서는 UI, 입력 검증, 서버 통신이 각각 독립적으로 동작합니다.
4. 로딩, 성공, 실패 상태 관리
React Query는 서버 요청의 상태를 자동으로 관리해줍니다.
상태 속성 설명
| 로딩 중 | isLoading | 요청 중일 때 true |
| 성공 | isSuccess | 요청 성공 시 true |
| 실패 | isError | 요청 실패 시 true |
이를 활용하면 폼의 상태를 시각적으로 표시할 수 있습니다.
{mutation.isLoading && <p>요청 중입니다...</p>}
{mutation.isError && <p style={{ color: 'red' }}>에러가 발생했습니다!</p>}
{mutation.isSuccess && <p style={{ color: 'green' }}>완료되었습니다!</p>}
5. React Query의 invalidateQueries로 데이터 새로고침
예를 들어, 회원가입 성공 후 사용자 목록을 다시 불러오고 싶다면
useQueryClient()의 invalidateQueries()를 사용할 수 있습니다.
import { useQueryClient } from '@tanstack/react-query';
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (formData) => axios.post('/api/signup', formData),
onSuccess: () => {
queryClient.invalidateQueries(['users']); // 사용자 목록 다시 불러오기
},
});
이렇게 하면 서버의 최신 데이터를 자동으로 동기화할 수 있습니다.
6. React Hook Form과 React Query의 궁합이 좋은 이유
항목 React Hook Form React Query
| 역할 | 입력값 관리 및 검증 | 서버 통신 및 상태 관리 |
| 핵심 기능 | register, handleSubmit, errors | useQuery, useMutation, invalidateQueries |
| 공통점 | 훅 기반, 불필요한 리렌더링 최소화 | 동일 |
| 장점 | 단순한 로직, 명확한 책임 분리 | 실시간 데이터 갱신, 캐싱 |
두 라이브러리를 결합하면
“입력 → 서버 요청 → 응답 처리” 전 과정을
단 30줄 이내로 명확하게 제어할 수 있습니다.
7. 예제: 게시글 작성 폼
function PostForm() {
const { register, handleSubmit, reset } = useForm();
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: (data) => axios.post('/api/posts', data),
onSuccess: () => {
queryClient.invalidateQueries(['posts']);
alert('게시글이 등록되었습니다.');
reset();
},
});
const onSubmit = (data) => mutation.mutate(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('title', { required: true })} placeholder="제목" />
<textarea {...register('content', { required: true })} placeholder="내용" />
<button type="submit" disabled={mutation.isLoading}>
{mutation.isLoading ? '등록 중...' : '등록'}
</button>
</form>
);
}
이 예제에서는 글을 등록한 뒤 자동으로 목록 쿼리를 새로고침(invalidateQueries)하여
최신 상태가 즉시 반영됩니다.
8. 실무 팁
- API 요청 로직 분리하기React Query의 mutationFn에서 이 함수를 불러오면 코드 재사용성이 높아집니다.
- // api.js export const createUser = (data) => axios.post('/api/signup', data); export const createPost = (data) => axios.post('/api/posts', data);
- 에러 메시지는 서버에서 내려받은 메시지를 그대로 표시
- 사용자 경험 향상을 위해 try/catch보다 mutation.onError를 선호합니다.
- 폼 초기화는 항상 reset()으로 처리
- React Hook Form의 내부 상태를 완전히 초기화합니다.
9. 마무리
React Hook Form과 React Query의 조합은
리액트에서 폼과 서버 상태를 관리하는 가장 현대적인 방식입니다.
핵심 요약:
- React Hook Form은 “입력값 + 검증”
- React Query는 “서버 요청 + 상태 관리”
- 두 훅을 결합하면 “깔끔한 비동기 폼 구조” 완성
- invalidateQueries로 서버 데이터와 실시간 동기화 가능
다음 강의에서는 이 두 라이브러리를 실제 프로젝트 구조에 통합해,
회원가입 → 로그인 → 대시보드 데이터 렌더링으로 이어지는
완성형 폼 흐름을 설계해보겠습니다.