React

react-hook-form을 알아보자

hyriver(강화영) 2025. 8. 13. 15:57

React-hook-form에 대해서는 input 입력 시 렌더링을 최소화하는 라이브러리 정도로만 알고 있었다. 현 회사에 입사해서 계속 리액트 훅 폼을 사용하고 있는데, 기계적으로 붙여넣기(..)만 하다가 제대로 공부해야겠다는 생각이 들어 작성하는 리액트 훅 폼 정리 글이다.

 

 

1. 제어 컴포넌트 vs 비제어 컴포넌트

제어/비제어 컴포넌트는 리액트가 실시간으로 state를 제어할 수 있는지 없는지에 따라 나눠진다. 제어 컴포넌트를 사용하게 되면 과도한 리렌더링이 발생하게 되는데 이러한 특성을 보완하기 위해 비제어 컴포넌트인 리액트 훅 폼 라이브러리를 사용하기 시작했다.

 

🔗 제어 컴포넌트

  • 기존 리액트에서 input창에 입력을 하면 onChange함수에 state 변경 함수를 연결하여 실시간으로 상태 변화가 일어나도록 했다. 이렇게 리액트가 해당 상태를 바로 인식하는 즉, 리액트에 의해 값이 제어되는 입력 폼 엘리먼트 제어 컴포넌트(controlled componen)라고 한다.

HTML에서 <input>, <textarea>, <select>와 같은 폼 엘리먼트는 일반적으로 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트합니다. React에서는 변경할 수 있는 state가 일반적으로 컴포넌트의 state 속성에 유지되며 setState()에 의해 업데이트됩니다.

우리는 React state를 “신뢰 가능한 단일 출처 (single source of truth)“로 만들어 두 요소를 결합할 수 있습니다. 그러면 폼을 렌더링하는 React 컴포넌트는 폼에 발생하는 사용자 입력값을 제어합니다. 이러한 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 “제어 컴포넌트 (controlled component)“라고 합니다.

  • 제어 컴포넌트는 input에 값을 입력할 때마다 setState가 발생하고 상태 변경으로 인한 리렌더링이 발생한다. 이런 제어 컴포넌트는 실시간 유효성검사 조건부 버튼 활성화 등에 유용하다.

 

type Info = {
  name: string;
  age: number;
};

const Form = () => {
  // 1. 리액트에서 state를 만들어 input 값에 연결한다
  const [formData, setFormData] = useState<Info>({    
    name: "",
    age: 0,
  });

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  // 2. input 값이 변경될 때마다 상태변경함수가 작동하여 리액트가 state를 알 수 있다
    setFormData({
      ...formData,
      [e.target.name]: e.target.value,
    });
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    console.log(`Name: ${formData.name}, Age: ${formData.age}`);
  };

  return (
    <div>
      <h3>리액트 - 제어컴포넌트</h3>
      <form onSubmit={handleSubmit}>
        이름 :
        <input
          type="text"
          name="name"
          value={formData.name}
          onChange={handleChange}
          required
        />
        나이 :
        <input
          type="number"
          name="age"
          value={formData.age}
          onChange={handleChange}
          required
        />
        <p>
          Name: {formData.name}, Age: {formData.age}
        </p>
        <button type="submit">제출</button>
      </form>
    </div>
  );
};

 

 

⛓️‍💥 비제어 컴포넌트

  • 비제어 컴포넌트(uncontrolled component)는 리액트에 의해 값이 제어되지 않는 컴포넌트를 말한다. state로 값을 관리하는 것이 아니라 ref를 사용하여 DOM 노드에서 값을 관리하고 가져온다.
  • 리액트 훅 폼은 이러한 비제어 컴포넌트를 활용한다. register를 사용해 input과 연결하면 입력필드에 ref가 연결되고, 입력 값이 변경되더라도 DOM 상태만 업데이트되므로 불필요한 렌더링을 방지한다. 아래처럼 input 값이 변경되더라도 렌더링되지 않는다.

 

 

 

type Info = {
  name: string;
  age: number;
};

const ReactHookForm = () => {
  // register로 input과 연결한다. 따로 state를 만들지 않아도 된다.
  const { register, handleSubmit } = useForm<Info>()

  const onSubmit= (v: Info) => {
    console.log(v);
  };

  return (
    <div style={{ backgroundColor: "ButtonFace" }}>
      <h3>리액트훅폼 - 비제어컴포넌트</h3>
      <form onSubmit={handleSubmit(onSubmit)}>
        이름 : <input {...register("name", { required: true })} />
        나이 : <input {...register("age", { required: true })} />
        <button type="submit">
          제출
        </button>
      </form>
    </div>
  );
};

 

 

2. useForm vs Controller

리액트 훅폼은 useForm을 사용해서 비제어 컴포넌트를 쉽게 구현하고, <Controller> 컴포넌트를 사용하여 커스텀 컴포넌트를 연결하여 사용할 수 있다.

 

⛓️‍💥 useForm

useForm의 주요 옵션 및 메서드

  • register : 입력 필드를 폼 상태에 등록하는 함수
    • required : 필수 입력과 미입력 시 에러메세지
    • setValueAs : 값을 저장하기전 변환 ex) 미입력 시 ““ 로 저장 시 사용
  • handleSubmit : 폼 제출 시 데이터를 처리하는 함수
  • formState : 폼 상태 제공(에러, 유효성, 제출 여부 등)
  • pattern, minLength 등 유효성 검사
  • defaultValues : 기본 값 설정
  • reset : 폼 데이터 초기화
  • setValue, getValue : 데이터 설정 및 가져오기
    - getValue는 watch처럼 폼의 값을 읽어오지만 리렌더링을 일으키지 않고, 값의 변화를 알지 못한다
  • watch : 입력 값 실시간 감지

 

type Info = {
  id: string;
  pw: string;
  email: string;
};

const ReactHookForm = () => {
  const { register, handleSubmit, watch, formState: {error} } = useForm<Info>({
    defaultValues: { id: "", pw: "", email: "" },
  });

  const onSubmit = (data: FormValues) => {
    // 제출 시 실행할 작업
    // onSubmit에서 받는 매개변수는 폼 데이터 객체이다
  };

  return (
    <div style={{ backgroundColor: "ButtonFace" }}>
      <h3>리액트훅폼 - 비제어컴포넌트</h3>
      <form onSubmit={handleSubmit(onSubmit)}>
        아이디 : <input {...register("id", { required: true })} />    // required: true이면 필수항목
        {errors.id&& <p>{errors.id.message}</p>}

        비밀번호 : <input 
           {...register("pw", { 
              required: '비밀번호는 필수항목 입니다',   // require의 텍스트는 errors.pw.message가 된다
              minLength: {
                value: 8,
                message: '비밀번호는 최소 8자 이상이어야 합니다.'     
            }
          })}
        />
        {errors.pw&& <p>{errors.pw.message}</p>}

        이메일 : <input
          {...register('email', {
            required: '이메일은 필수 항목입니다.',
            pattern: {
              value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
              message: '올바른 이메일 형식을 입력해주세요.'
            }
          })}
        />
        {errors.email&& <p>{errors.email.message}</p>}

        <button
          style={{
            backgroundColor:
              watch("name") !== "" && watch("age") !== "" ? "green" : "gray",
          }}
          type="submit"
        >
          제출
        </button>
      </form>
    </div>
  );
};
  • watch 또는 useWatch를 사용하여 실시간으로 값을 감지한다는 것은 값의 변경에 의한 리렌더링이 일어난다는 의미이다. watch를 사용하여 제어컴포넌트의 기능을 수행할 수 있다. 사실 이렇게 리렌더링이 계속 일어나면 리액트 훅 폼을 사용하는 큰 의미가 없기 때문에 적절한 상황에 알맞게 사용해야한다.

 

 

 

  • 그리고 watch에 대한 새로운 발견!
  • watch("name") !== "" && watch("age") !== "" ? "green" : "gray" 이렇게 조건부 렌더링에 watch를 넣어주면 첫번째 watch의 값이 변경되어야만 렌더링 된다. 그러니까 나이 input 창의 값이 먼저 변경될 때는 리렌더링 되지 않는다!

 

🔗 Controller

<Controller>는 커스텀 컴포넌트를 연결할 수 있어서 MUI, react-native 등 외부 라이브러리와 함께 사용할 때 유용하다. 그런데 이 컨트롤러는 제어 컴포넌트이기때문에 입력할 때마다 input 컴포넌트가 렌더링된다.

 

그렇다면 Controller를 사용하는 input 렌더링과 리액트에서 만든 input 렌더링의 차이는?

리액트에서 state를 만들어서 controlled input을 만들면 입력할때마다 state가 업데이트되고 컴포넌트 전체가 같이 재렌더링되어 폼이 커지면 무거워진다.

 

 

Controller의 주요 속성

  • name : 필드의 이름. useForm의 defaultValues의 값과 일치하게 전달된다.
  • control : useForm에서 반환하는 control을 전달하는데 Controller와 리액트 훅 폼을 연결한다.
  • rules : 필드의 유효성 검사
  • render : 입력 필드를 렌더링하는 함수
  • defaultValues : 필드의 기본 값

 

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

const ControllerCompo = () => {
  const { control, handleSubmit } = useForm({
    defaultValues: { name: "", age: "" },
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <h3>CONTROLLER 사용</h3>
      <Controller
        name="name"
        control={control}
        render={({ field }) => <input {...field} placeholder="이름 입력" />}
      />
      <Controller
        name="age"
        control={control}
        render={({ field }) => <input {...field} placeholder="나이 입력" />}
      />
      <button type="submit">제출</button>
    </form>
  );
};
export default ControllerCompo;