실전 예제로 알아보는 react hook form 사용법

안녕하세요. 요즘 실무에서 form을 다룰 때 react-hook-form library를 사용하고 있습니다.

form의 input을 상태로 관리하면 상태가 바뀔 때마다 리렌더링이 발생해 불필요한 리렌더링이 많아집니다. react-hook-form을 사용하면 각 form을 독립적으로 관리할 수 있어서 다른 form의 리렌더링을 방지할 수 있습니다. 또한 성능 최적화도 잘 되어 있고 쉽게 사용가능한 유용한 api들도 제공해줘서 form 관리가 편해졌습니다.

실무에서 다양한 요구사항을 구현하면서 많이 검색해봤지만, 검색해도 안 나오는 부분들이 많아서 api의 type을 보면서 스스로 터득한 것들을 sandbox와 함께 공유하려고 합니다. 코드의 각 줄에 주석을 달아 설명을 추가했습니다. 코드 아래에 sandbox 링크를 첨부할테니 활용해보시면 좋을 것 같습니다.

import React, { useEffect, useState } from 'react'
import ReactDOM from 'react-dom'
import { useController, useForm } from 'react-hook-form'

import './styles.css'

const formatPhoneInput = (value: string) => {
  return (
    value
      // 숫자를 제외한 입력값 제거
      .replace(/[^0-9]/g, '')
      // 휴대폰 번호 형식으로 하이픈 추가
      .replace(
        /(^02|^0505|^1[0-9]{3}|^0[0-9]{2})([0-9]+)?([0-9]{4})/,
        '$1-$2-$3'
      )
      // 하이픈이 여러개면 제거
      .replace('--', '-')
  )
}

function App() {
  const [includePhone, setIncludePhone] = useState(false)
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors },
    control,
  } = useForm({
    mode: 'onChange',
    // form value 초기값을 설정할 수 있습니다.
    // 해당 객체의 key값은 항목의 'name'입니다.
    defaultValues: {
      example: '',
      exampleRequired: '',
      exampleRequiredWithMessage: '',
      mobile: '',
      email: 'abc@abc.com',
    },
  })

  // errors는 각 항목 name들을 key로 가지는 map입니다.

  // 예를 들어 type은 다음과 같으며 등록된 항목에 따라 바뀝니다.
  // DeepMap<{example: string; exampleRequired: string; exampleRequiredWithMessage: string;}, FieldError>

  // error가 없을 땐 빈 map입니다.
  // {}

  // error가 발생하면 해당 항목에 에러 정보가 담깁니다. error가 없으면 해당 항목 error 정보는 없습니다.
  // message는 사용자가 지정한 메시지이고, type에는 어떤 validation rule인지가 담깁니다.
  // {
  //   phone: {
  //     message: '올바른 휴대폰 번호를 입력해주세요.",
  //     ref: HTMLInputElement,
  //     type: 'pattern'
  //   },
  //   exampleRequiredWithMessage: {
  //     message: 'example required',
  //     ref: HTMLInputElement,
  //     type: 'required'
  //   }
  // }

  // 유저가 입력할 때 하이푼이 자동으로 들어가도록 설정하고 싶음.
  // 커스텀하기 위해 controller를 사용합니다.
  const { field: mobileField } = useController({
    name: 'mobile',
    control,
    rules: {
      required: '휴대폰 번호를 입력해주세요.',
      pattern: {
        value: /^01(0|1|[6-9])-(?:\d{3}|\d{4})-\d{4}$/,
        message: '올바른 휴대폰 번호를 입력해주세요.',
      },
      maxLength: 13,
    },
  })

  useEffect(() => {
    console.info('errors : ', errors)
  }, [{ ...errors }])

  // watch에 항목의 'name'을 넘겨서 값이 바뀔 때마다 체크할 수 있습니다.
  console.log(watch('example'))

  return (
    <form
      // form 제출을 할 때 유효성 검사에 성공하면 handleSubmit callback의 parameter로 form data가 넘어옵니다.
      // 유효성 검사에 실패하면 실패한 input에 focus가 됩니다.
      onSubmit={handleSubmit(data => {
        alert(JSON.stringify(data))
      })}
    >
      <label>Example</label>
      {/* 항목을 등록할 때 register 함수를 사용합니다.
      'example' 항목을 등록합니다. */}
      <input {...register('example')} />

      <label>ExampleRequired</label>
      <input
        /* register의 두번째 인자로 ValidationRule option을 넘길 수 있습니다.
        require rule과 maxLength rule을 추가합니다. */
        {...register('exampleRequired', { required: true, maxLength: 10 })}
      />
      {/* validation을 통과하지 못하면 errors 객체에 정보가 넘어옵니다.
      message를 등록할 때 지정하지 않고 이렇게 임의로 보여줄 수 있습니다. */}
      {errors.exampleRequired && <p>This field is required</p>}

      <label>ExampleRequiredWithMessage</label>
      <input
        // required rule validation을 통과하지 못했을 때 메시지를 에러 정보에 담고 싶다면 메시지를 바로 넘기면 됩니다.
        {...register('exampleRequiredWithMessage', {
          required: 'example required',
        })}
        // validation rule에 조건을 주고 싶을 때가 있습니다. 그러면 ValidationRule의 형태로 넘기면 됩니다.
        {...register('exampleRequiredWithMessage', {
          required: {
            value: true, // 원하는 조건식
            message: 'example required',
          },
        })}
      />
      {/* errors에 담긴 message를 사용합니다. */}
      {errors.exampleRequiredWithMessage && (
        <p>{errors.exampleRequiredWithMessage?.message}</p>
      )}

      <label>ExampleRequiredWithMessage</label>
      <input
        // validation을 통과하지 못했을 때 메시지를 보여주고 싶다면 required rule에 string을 넘기면 됩니다.
        {...register('exampleRequiredWithMessage', {
          required: 'example required',
        })}
      />
      {errors.exampleRequiredWithMessage && (
        <p>{errors.exampleRequiredWithMessage?.message}</p>
      )}

      <label>Email</label>
      <input
        {...register('email', {
          required: '이메일을 입력해 주세요.',
          pattern: {
            value: /\S+@\S+\.\S+/,
            message: '올바른 이메일 주소를 입력해주세요',
          },
        })}
      />
      {errors.email && <p>{errors.email?.message}</p>}

      <div>
        <label htmlFor="includePhone">Include Phone</label>
        <input
          id="includePhone"
          type="checkbox"
          onChange={({ target: { checked } }) => setIncludePhone(checked)}
          style={{ width: 'auto' }}
        ></input>
      </div>
      {includePhone && (
        <>
          <label>Mobile</label>
          <input
            // 하이픈이 들어간 휴대폰 번호를 받고 싶다면
            {...mobileField}
            onChange={e => {
              const formattedValue = formatPhoneInput(e.target.value)
              mobileField.onChange(formattedValue)
            }}
          />
          {errors.mobile && <p>{errors.mobile?.message}</p>}
        </>
      )}
      <input type="submit" />
    </form>
  )
}

const rootElement = document.getElementById('root')
ReactDOM.render(<App />, rootElement)

sandbox link


Written by@Donghoon Song
사람들의 꿈을 이어주는 코멘토에서 일하고 있습니다.

InstagramGitHubTwitterLinkedIn