react-hook-form을 알아보자

화영화영
5 min read

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 : 입력 필드를 폼 상태에 등록하는 함수

  • 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의 주요 속성

  • 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;
0
Subscribe to my newsletter

Read articles from 화영 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

화영
화영