React Hook Form 간단 정리

Rosevillage·2025년 2월 23일

1. React Hook Form

React Hook Form은 React에서 폼을 쉽게 관리할 수 있도록 도와주는 라이브러리다. 비제어 컴포넌트 방식을 기반으로, 제어 컴포넌트 방식보다 간결한 코드로 폼을 관리할 수 있다.

공식 홈페이지에서는 가벼운 패키징, 직관적인 api, 리렌더링 최소화를 통한 성능 향상 등을 장점으로 소개하고 있다.

React Hook Form 특징

  • 비제어 컴포넌트 기반: 기본적으로 ref를 활용한 비제어 방식으로 동작
  • 최소한의 리렌더링: 입력값이 변경될 때마다 리렌더링이 발생하지 않는다.
  • 유효성 검사 지원: 기본적으로 HTML의 유효성 검사와 함께 yup, zod 등의 스키마 기반 검증 라이브러리를 쉽게 통합가능

2. 비제어 컴포넌트와 제어 컴포넌트

React에서 제어 컴포넌트와 비제어 컴포넌트는 폼 데이터(입력값) React에서 다루는지, DOM에서 다루는지에 따라 구분된다.

비제어 컴포넌트

비제어 컴포넌트는 React에서 상태(state)를 직접 관리하지 않고, DOM 요소의 ref를 통해 값을 제어하는 방식으로 입력값 변경에 따른 리렌더링을 유발하지 않기 때문에 React Hook Form은 이 방식을 기본으로 채택하여 불필요한 렌더링을 방지한다.

import { useRef } from "react";

function UncontrolledForm() {
  const inputRef = useRef();

  const handleSubmit = (event) => {
    event.preventDefault();
    alert(`입력된 값: ${inputRef.current.value}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" ref={inputRef} />
      <button type="submit">제출</button>
    </form>
  );
}

제어 컴포넌트

제어 컴포넌트는 React의 useState를 사용하여 입력값을 상태로 관리하는 방식으로 입력값이 변경될 때마다 상태가 업데이트되고, 리렌더링이 발생한다.

import { useState } from "react";

function ControlledForm() {
  const [value, setValue] = useState("");

  const handleSubmit = (event) => {
    event.preventDefault();
    alert(`입력된 값: ${value}`);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="text" value={value} onChange={(e) => setValue(e.target.value)} />
      <button type="submit">제출</button>
    </form>
  );
}

3. useForm을 통한 비제어 컴포넌트

React Hook Form은 기본적으로 useForm 훅을 사용해 비제어 컴포넌트를 다루는 방법을 소개한다. 이는 본인들이 내세우는 장점이 극대화 되는 상황이 비제어 컴포넌트를 사용하는 상황이기 때문이다.

useForm 반환값

  • register: value ref onChange 등 입력 필드 등록 함수
  • handleSubmit: 콜백 함수를 받아 폼 제출 시 실행
  • watch: 입력값에 대한 실시간 감지 함수
  • setValue: 특정 필드 값을 설정하는 함수
import { useForm } from "react-hook-form";

function FormExample() {
  const { register, handleSubmit } = useForm();

  const onSubmit = (data) => {
    alert(`입력된 값: ${JSON.stringify(data)}`);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} placeholder="이름 입력" />
      <button type="submit">제출</button>
    </form>
  );
}

4. useController, Controller를 통한 제어 컴포넌트

React Hook Form은 기본적으로 비제어 컴포넌트를 내세우지만 UI라이브러리 통합 등 제어 컴포넌트가 필요한 상황 또한 상당히 잘 지원한다.

Controller

Controller 컴포넌트는 React Hook Form에서 제어 컴포넌트가 필요할때 사용된다.

useController

useController는 Controller와 동일한 기능을 제공하지만, 함수형 방식으로 사용할 수 있다.

useController 반환값

  • field: 입력 필드의 제어 객체
  • fieldState: 필드 상태 (예: 에러 여부)
  • formState: 전체 폼 상태
Controller
import { useForm, Controller } from "react-hook-form";

interface FormData {
  name: string;
}

function ControlledInputForm() {
  const { control, handleSubmit } = useForm<FormData>();

  const onSubmit = (data: FormData) => {
    alert(JSON.stringify(data));
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="name"
        control={control}
        render={({ field }) => <input {...field} placeholder="이름 입력" />}
      />
      <button type="submit">제출</button>
    </form>
  );
}

export default ControlledInputForm;
useController
import { Control } from "react-hook-form";

declare interface FormData {
  name: string;
}

function ControlledHookInputForm({control}:{control: Control<FormData>}) {
	const { field } = useController({ name: "name", control }); 
	
	const onSubmit = (data: FormData) => { alert(JSON.stringify(data)); }; 
	
	return ( 
		<form onSubmit={handleSubmit(onSubmit)}> 
			<input {...field} placeholder="이름 입력" />
			<button type="submit">제출</button>
		</form> 
	);
}

5. 유효성 검사

React Hook Form의 register의 옵션값을 통해 유효성 검사를 쉽게 설정할 수 있다.

import { useForm } from "react-hook-form";

function ValidationForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const onSubmit = (data) => {
    alert(JSON.stringify(data));
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name", { required: "이름을 입력하세요" })} />
      {errors.name && <p>{errors.name.message}</p>}
      <button type="submit">제출</button>
    </form>
  );
}

yup을 적용한 유효성 검사

React Hook Form은 자체적인 유효성 검사 외에도 외부 라이브러리(yup, zod 등)를 통한 더욱 간단한 방법도 지원한다.

import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";

const schema = yup.object().shape({
  name: yup.string().required("이름을 입력하세요"),
});

function YupValidationForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: yupResolver(schema),
  });

  const onSubmit = (data) => {
    alert(JSON.stringify(data));
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} />
      {errors.name && <p>{errors.name.message}</p>}
      <button type="submit">제출</button>
    </form>
  );
}

종속성이 있는 유효성 검사

특정 필드의 값에 따라 다른 필드의 유효성을 검사해야 하는 경우가 있다. 회원 가입을 예로들면 passwordConfirm 필드(비밀번호 확인)는 password(비밀번호)와 동일해야 한다.

React Hook Form으로만 구현

password필드에 deps속성, passwordConfirm필드에 validate속성을 사용하고
watch를 통해 두 필드 값이 같은지 확인하는 방식으로 구현할 수 있다.

import { useForm } from "react-hook-form";

type FormData = {
  password: string;
  passwordConfirm: string;
};

function DependentValidationForm() {
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors },
  } = useForm<FormData>({
    mode: "all",
  });

  const onSubmit = (data: FormData) => {
    alert(JSON.stringify(data));
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input
        type="password"
        {...register("password", {
          required: "비밀번호를 입력하세요",
          minLength: {
            value: 6,
            message: "비밀번호는 최소 6자리 이상이어야 합니다",
          },
          deps: ["passwordConfirm"]
        })}
        placeholder="비밀번호"
      />
      {errors.password && <p>{errors.password.message}</p>}

      <input
        type="password"
        {...register("passwordConfirm", {
          required: "비밀번호 확인을 입력하세요",
          validate: (value) => value === watch("password") || "비밀번호가 일치하지 않습니다",
        })}
        placeholder="비밀번호 확인"
      />
      {errors.passwordConfirm && <p>{errors.passwordConfirm.message}</p>}

      <button type="submit">제출</button>
    </form>
  );
}

yup과 함께 더 간단하게 구현

schema선언 당시 oneOf를 사용해 구현할 수 있다.

  • oneOf(values: array, message): 배열에 해당하는 값들만 허용
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";

const schema = yup.object().shape({
  password: yup.string().required("비밀번호를 입력하세요").min(6, "비밀번호는 최소 6자리 이상이어야 합니다"),
  passwordConfirm: yup
    .string()
    .oneOf([yup.ref("password"), null], "비밀번호가 일치하지 않습니다")
    .required("비밀번호 확인을 입력하세요"),
});

type FormData = {
  password: string;
  passwordConfirm: string;
};

function DependentValidationForm() {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<FormData>({
    resolver: yupResolver(schema),
  });

  const onSubmit = (data: FormData) => {
    alert(JSON.stringify(data));
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input type="password" {...register("password")} placeholder="비밀번호" />
      {errors.password && <p>{errors.password.message}</p>}

      <input type="password" {...register("passwordConfirm")} placeholder="비밀번호 확인" />
      {errors.passwordConfirm && <p>{errors.passwordConfirm.message}</p>}

      <button type="submit">제출</button>
    </form>
  );
}

yup을 사용하면 schema에서만 설정하면 되기 때문에 뷰와 분리하기도 수월하고, 수정하기도 편해지기에 유효성 검증에서 외부 라이브러리를 사용하는 것도 좋은 선택지가 될 것 같다.


Reference

React Hook Form

Yup

React-비제어 컴포넌트

0개의 댓글