frontend/react

[React] React.js 강좌 16. React Hook Form × React Query – 폼 데이터 전송의 완벽한 조합

mirabo01 2025. 11. 10. 08:53

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;

작동 흐름

  1. handleSubmit()이 폼 데이터를 수집
  2. mutation.mutate(data)가 서버 요청 실행
  3. 성공 시 alert 후 reset()으로 초기화
  4. 실패 시 에러 메시지 표시

이 구조에서는 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. 실무 팁

  1. API 요청 로직 분리하기React Query의 mutationFn에서 이 함수를 불러오면 코드 재사용성이 높아집니다.
  2. // api.js export const createUser = (data) => axios.post('/api/signup', data); export const createPost = (data) => axios.post('/api/posts', data);
  3. 에러 메시지는 서버에서 내려받은 메시지를 그대로 표시
    • 사용자 경험 향상을 위해 try/catch보다 mutation.onError를 선호합니다.
  4. 폼 초기화는 항상 reset()으로 처리
    • React Hook Form의 내부 상태를 완전히 초기화합니다.

9. 마무리

React Hook Form과 React Query의 조합은
리액트에서 폼과 서버 상태를 관리하는 가장 현대적인 방식입니다.

핵심 요약:

  • React Hook Form은 “입력값 + 검증”
  • React Query는 “서버 요청 + 상태 관리”
  • 두 훅을 결합하면 “깔끔한 비동기 폼 구조” 완성
  • invalidateQueries로 서버 데이터와 실시간 동기화 가능

다음 강의에서는 이 두 라이브러리를 실제 프로젝트 구조에 통합해,
회원가입 → 로그인 → 대시보드 데이터 렌더링으로 이어지는
완성형 폼 흐름을 설계해보겠습니다.