Skip to main content

Command Palette

Search for a command to run...

react-hook-form을 알아보자

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

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

  • 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;

More from this blog

Tab Navigator에서 params에 따라 렌더링하기

React Navigation은 react native에서 화면이동(routing) 시 사용되는 라이브러리로, 페이지가 쌓이는 구조(stack 구조)를 제공한다. Stack 뿐만 아니라 Tab(하단탭), Drawer(옆에서 열리는 전체메뉴) 등의 화면 이동 방식(네비게이터)도 가지고 있다. 네비게이터들은 서로 중첩이 가능하다. 우리 프로젝트에서는 Stack과 Tab 네비게이터를 사용했고, Stack 내 하나의 화면에 Tab을 중첩하여 사용했다....

Apr 18, 20253 min read

npm run ios 에러 해결기

이전부터 리액트 네이티브 프로젝트에서 ios 시뮬레이터를 실행시키면 절대 한번에 실행되는 법이 없었다. 그래서 그때도 어떻게 에러를 해결했는지 기록해놨었는데, 그걸 참고해도 실행이 안되더라는😂 그래서 또 다시 작성한다. npm run ios로 스트레스 받지 않는 그날을 위해 1. pod install 깃에서 프로젝트를 받고 npm run ios를 하기 전 pod install(프로젝트에 필요한 라이브러리 설치)을 했다. 하지만 언제나 여기부터...

Feb 23, 20254 min read

axios 응답 인터셉터로 리턴 값 변경 시 타입에러 발생

🐞 문제상황 axios에서 interceptor를 사용해서 데이터를 바로 리턴해주도록 설정했다. 그런데 axios에서 리턴 받은 데이터 객체의 속성을 바로 사용하려고 하니 타입에러가 발생했다. 그리고 인터셉터를 적용하지 않은 것처럼 data.data.속성명을 작성했더니 타입에러가 뜨지 않았다. // utils/api.ts const api = axios.create({ baseURL: 'http://baseurl', timeout: 1...

Jan 20, 20253 min read

개발 블로그

6 posts