[React] React-Hook-Form 으로 폼(Form)을 구현하자 ‼️

송수빈송수빈
6 min read

왜 React-Hook-Form 사용을 결정하게 되었는지 ? 👇🏻

저번 프로젝트에서도 반려동물 정보 입력 페이지를 담당 했었는데 그때는 리액트 훅 폼을 사용하지 않았다..!

그러다보니 모든 입력 필드를 useState로 관리했고 코드도 어마무시하게 길어졌다. (입력 + 수정 + 추가) 페이지 코드가 모두 260줄이 넘는 이슈…!! 이게 웃긴게 그 당시 하면서 느꼈지만 입력 + 수정 + 추가 모두 같은 폼을 사용하고 있다.(+같은 코드)

그때도 그랬지만 지금 생각해보면 말이 더 안 된다…🙂‍↔️(부정)

추가적으로 에러랑 예외처리가 힘들었다. 유효성 검사는 petValidation.js파일을 utils로 분리해둬서 사용했고 에러를 처리하는 방식은 조건문 또는 별도 상태로 에러 추적해줬다..😾

비효율이적이다.. 굉장히..엄청나게… 🤦🏻‍♀️

이런 불편함을 겪고

반드시 이번에는 React-Hook-Form을 사용해서 폼을 구현하고 싶다라는 생각을 갖게 되었고 다행히 팀원도 같은 생각을 갖고 있어서 사용하기로 결정했다.

🧩 React-Hook-Form

코드를 바로 설명하기보다는 먼저 React Hook Form이 무엇인지 그리고 어떻게 사용하는지 간단히 소개한 뒤 실제 구현 코드를 보여주는 것이 더 이해하기 쉬울 것 같아 이 순서로 작성하겠다.!

React Hook Form은 React에서 폼을 간편하고 효율적으로 다룰 수 있게 도와주는 라이브러리이다.

React의 기본 폼 처리 방식은 대부분 useState를 활용해 상태를 관리하는데 이 방식은 입력 필드가 많아질수록 코드가 길어지고 매 입력마다 컴포넌트가 리렌더링되어 성능 문제가 발생할 수 있다.

하지만 React Hook Form"비제어 컴포넌트(uncontrolled components)" 기반으로 동작해서 최소한의 리렌더링으로 성능을 최적화 해주는 장점이 있다. 또한 유효성 검사와 에러 처리 그리고 서브밋 관리까지 아주 간단하게 할 수 있다는 장점이 있다👍🏻

🛠️ 주요 특징

  • 입력 필드를 register 함수 하나로 연결

  • useForm()으로 폼 상태, 에러, 유효성 검사 관리

  • 선언형 유효성 검사 (required)

  • 내부적으로 ref를 활용해 값과 상태를 추적하기 때문에 입력할 때마다 리렌더링되지 않는다.

// useState로 관리할 경우
<input value={value} onChange={(e) => setValue(e.target.value)} />

// react-hook-form의 register
<input {...register("name")} />

💡 사용 방법

설치방법

//npm
npm install react-hook-form

//yarn
yarn add react-hook-form

기본 사용법

  • 가장 기본적인 형태는 useFormregister를 사용하는 방식이다.
const { register, handleSubmit, formState: { errors } } = useForm();

<input {...register("name", { required: "이름은 필수입니다" })} />

register를 통해 인풋에 바로 연결하면 되는데 이게 정말 간단하다. 유효성 검사도 객체 형태로 같이 전달하면 된다.

그런데 onChange 안에 로직이 필요하다면 register만으로는 부족하다.

그래서 등장한 것이 useController

import { Controller } from 'react-hook-form'

const { control } = useForm();
const { field } = useController({
  name: "username",
  control,
  rules: { required: "유저 이름은 필수입니다" }
});

<input
  {...field}
  onChange={(e) => {
    console.log("입력된 값:", e.target.value);
    field.onChange(e); // 기존 연결 유지
  }}
/>

여기서 field.onChange를 꼭 호출해줘야 React Hook Form과의 연결이 유지된다.

이 외에도 React-Hook-Form 특징이 있는데

  1. watch()로 실시간 값 추적 가능하다

    • 특정 필드의 값을 실시간으로 감지할 수 있다.

    • ex) 비밀번호 확인, 조건부 렌더링, 디버깅 등

// 'name' 필드 값이 바뀔 때마다 최신 값 반환 
const name = watch("name");
  1. useWatch – 특정 필드값 실시간으로 감시

    • watch()는 전체 rerender를 유발하지만, useWatch()선택된 필드만 감시하며 리렌더링도 최소화된다.
const name = useWatch({ control, name: "name" });
  1. .reset, setValue, trigger 등의 유틸 함수(커스텀 버튼이나 외부 이벤트로 폼 상태를 조작할 때 꼭 필요하다)

    • reset() – 폼 초기화

    • setValue() – 특정 필드 값 수동 설정

    • trigger() – 특정 필드 또는 전체 폼의 유효성 검사 실행

  2. Controller vs useController 차이 : 기능은 같지만 사용 방식이 다르다

    • Controller: 컴포넌트 형태

      • JSX 안에서 사용
    • useController: 훅 형태

      • 로직 안에서 사용
// Controller 방식
<Controller
  name="email"
  control={control}
  render={({ field }) => <input {...field} />}
/>

// useController 방식
const { field } = useController({ name: "email", control });
<input {...field} />

✅ 직접 사용해보자

나는 이번에 우리 프로젝트에서 메인 기능인 구인 & 구직 & 자유게시판 글 등록 폼 구현을 맡았다.

입력 해야 하는 내용들이 적지 않다.

(나는 현재 contents 부분에 editor를 사용하고 있다)

자유게시판

  1. useForm 초기 세팅
const WriteCommunityPostForm = () => {
  const {
    register,
    handleSubmit,
    reset,
    setValue,
    control,
    formState: { errors },
  } = useForm({
    defaultValues: {
      nickname: '',
      title: '',
      text: EditorState.createEmpty(),
    },
  })
  • register: 일반 input을 RHF에 등록할 때 사용

  • handleSubmit: 유효성 검사 후 폼 제출을 처리

  • reset: 폼 초기화

  • setValue: 특정 필드 값을 프로그래밍적으로 설정(우리는 자유게시판에서 닉네임 값을 수정할 수 있어서 넣어줌)

  • control: Controller를 사용할 때 필요

  • errors: 각 필드의 유효성 검사 에러 정보

  1. onSubmit: 글 작성 시 처리 로직
const onSubmit = async (data) => {
  const rawHtml = draftToHtml(convertToRaw(data.text.getCurrentContent()))
  const htmlText = DOMPurify.sanitize(rawHtml)

  const postData = {
    nickname: data.nickname,
    title: data.title,
    text: htmlText,
  }
}
  • DOMPurify.sanitize: HTML 내 악성 스크립트 제거

  • convertToRawdraftToHtml: HTML로 변환

  1. 폼 구조 (렌더링 부분)

    1) 닉네임 입력

     <JobInputBox
       {...register('nickname', { required: true })}
       placeholder='닉네임을 입력하세요'
     />
     {errors.nickname && <span>닉네임은 필수입니다</span>}
    
  • register로 닉네임 input을 등록

  • required: true 유효성 검사

    만약 required: false이면 유효성 검사를 안 한다!

  • react-hook-form에서 useForm() 훅을 사용할 때 formState.errors 안에 유효성 검사를 통과하지 못한 필드의 정보가 들어간다.

  • errors.nicknamenickname 입력 필드에 에러가 존재할 경우에만 값이 존재

    만약에 입력 폼을 다 채우지 않고 등록을 눌러주면 이런식으로 밑에 에러 text가 나오는 것을 확인할 수 있다.

    2) 에디터 (글 내용)

      <Controller
        name='text'
        control={control}
        rules={{
          required: '내용은 필수입니다',
          validate: (value) => {
            const content = value.getCurrentContent()
            return content.hasText() || '내용은 필수입니다'
          },
        }}
        render={({ field }) => (
          <WriteEditor
            editorState={field.value}
            onEditorStateChange={field.onChange}
            placeholder='내용을 입력하세요'
          />
        )}
      />
    
  • 커스텀 컴포넌트는 register로 직접 제어 불가 → Controller 사용했다

    • register는 일반적인 <input>, <textarea>, <select> 같은 기본 HTML 요소에만 직접 사용할 수 있다.

    • 나는 현재 editor를 사용하고 있어서 register로 직접 제어가 불가능한 상태이다

🔗 연결해주는 부분

render={({ field }) => (
  <WriteEditor
    editorState={field.value}             // 현재 에디터 상태를 넘겨줌
    onEditorStateChange={field.onChange}  // 에디터 상태가 바뀔 때 react-hook-form에게 알려줌
    placeholder='내용을 입력하세요'
  />
)}

나는 submit-button이 다른 파일에 있어서 form id로 연결해주었다.

//WriteCommunityPostForm.jsx
<form id='community-post-form' onSubmit={handleSubmit(onSubmit)}>
//WriteCommunityPost.jsx 
<PinkButton label='등록' type='submit' form='community-post-form' />

완성 !! 👍🏻

🌱 배운 점

  • React Hook Form은 기존의 form 관리 방식을 훨씬 간단하고 효율적으로 만들어준다는 걸 체감했다.
    특히 register로 input에 바로 연결하고 Controller를 통해 커스텀 컴포넌트와도 쉽게 연동할 수 있어서 편리했다.

  • Controller를 사용하면 Draft.js 같은 복잡한 에디터 상태도 리액트 훅폼과 잘 통합할 수 있다. 이를 통해 에디터의 값도 쉽게 관리하고 유효성 검사도 유연하게 구현할 수 있었다.

  • 유효성 검사에서 required: true 만으로 필수 입력 체크를 간편하게 처리할 수 있어서 굉장히 편리하다는 점을 배웠다.

✅ 느낀 점

  • React Hook Form을 처음 접했을 때는 Controller 같은 개념이 조금 어렵게 느껴졌다. 하지만 실제로 구현하면서 점차 구조가 명확해지고 관리가 훨씬 편해져서 매우 만족스러웠다.

  • 특히 커스텀 에디터와 같은 복잡한 입력 컴포넌트와 연동할 때 별도의 상태 관리 코드를 줄일 수 있어 개발 효율성이 크게 올랐다.

  • form 상태와 유효성 관리를 직접 구현할 때 생기는 실수를 줄일 수 있다는 점도 마음에 들었다. 덕분에 코드가 더 깔끔해지고 유지보수가 쉬워졌다.

  • 앞으로 React 프로젝트에서 form이 필요할 때마다 React Hook Form을 적극적으로 활용하고 더 고급 기능들도 공부하고 싶다는 동기부여가 되었다(아직은 기본만 사용할 줄 아는 수준…인 느낌..?!)

0
Subscribe to my newsletter

Read articles from 송수빈 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

송수빈
송수빈