React-hook-form 잘 쓰고 싶다.

React-hook-form 잘 쓰고 싶다.

이럴거면 사용하wl 말아.. - ( 🙅 react-hook-form )

·

8 min read

일반적으로 form 객체를 사용해서 서비스에 필요한 데이터들을 입력받고 해당 데이터들의 유효성을 검증한다면 각각의 데이터를 담을 useState, 데이터들의 유효성을 검증하기 위한 함수 등 많은 코드들이 필요하고 서비스의 규모가 커질수록 해당 로직을 담당하는 코드는 복잡해지고 길어지게 될 것이다.

아래의 코드를 예로 들어보면,

현재는 value 값 만 입력받고 있지만 추가적으로 제목, 종류, 기간 등 여러 요소를 추가하게 될 경우 그에 따른 useState 가 필요하고 각각의 유효성 검증을 위한 코드가 onSubmit 함수안에 추가되어야 할 것이다 💩

const [value, setValue] = useState("");
const onChange = useCallback((e) => {
  setValue(e.target.value);
}, []);

const onSubmit = useCallback(
  (e) => {
    if (value === "") {
      e.preventDefault();
      alert("데이터가 없습니다");
      return;
    }
    onInsert(value);
    setValue("");
    e.preventDefault();
  },
  [onInsert, value]
);

----------------------------------------------------------------

<form className="TodoInsert" onSubmit={onSubmit}>
  <input
    type="text"
    placeholder="할 일을 입력하세요."
    value={value}
    onChange={onChange}
  />
  <button>
    <MdAdd />
  </button>
</form>

또한 관리되는 useState가 늘어남에 따라, 각각의 input의 유효성 상태 변경 때문에 모든 컴포넌트가 리렌더링 되는 문제도 발생하게 됩니다. 이를 해결하기 위해 colocationref를 이용할 수 있겠지만 근본적인 해결법은 되지 못한다고 생각했습니다.

colocation 이란?

input 요소가 여러 개일 때, 한 input의 유효성 상태 변경 때문에 모든 컴포넌트가 리렌더링 될 필요는 없습니다. 때문에 리렌더링이 필요한 컴포넌트만 분리하여, 리렌더링되는 범위를 좁히는 방법이 많이 쓰입니다.

이런식으로 컴포넌트의 상태를 서로 관련이 있는 것들끼리만 모아 분리하여 위치시키는 방법을 state colocation 이라고 합니다.

그러던 중 react-hook-form에 대해 알게되었고, 간단한 투두 리스트, 게시글 작성 기능을 만들어 테스트 해보면서 많은 이점들을 발견하였고 이후 프로젝트에 적용하게 되었다.


React-hook-form

Installation

npm install react-hook-form

React Hook Form에서의 주요 개념 중 하나는 컴포넌트를 훅에 등록하는 것입니다. 이렇게 하면 해당 값이 폼 유효성 검사와 제출 모두에 사용할 수 있게 됩니다.

참고: 각 필드는 등록 프로세스의 키로 사용할 이름이 필요합니다.

import ReactDOM from "react-dom"
import { useForm, SubmitHandler } from "react-hook-form"

enum GenderEnum {
  female = "female",
  male = "male",
  other = "other",
}

interface IFormInput {
  firstName: string
  gender: GenderEnum
}

export default function App() {
  const { register, handleSubmit } = useForm<IFormInput>()
  const onSubmit: SubmitHandler<IFormInput> = (data) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>First Name</label>
      <input {...register("firstName")} />
      <label>Gender Selection</label>
      <select {...register("gender")}>
        <option value="female">female</option>
        <option value="male">male</option>
        <option value="other">other</option>
      </select>
      <input type="submit" />
    </form>
  )
}

useForm

useForm은 폼을 쉽게 관리하기 위한 사용자 정의 훅입니다. 선택적으로 하나의 객체를 인수로 취합니다.

일반적인 속성

옵션설명
mode제출 전 유효성 검사 전략
reValidateMode제출 후 유효성 검사 전략
defaultValues기본값
values폼 값의 업데이트를하는데 사용되는 반응형 값
errors폼 오류를 업데이트하는데 사용되는 *반응형 오류
resetOptions새로운 폼 값을 업데이트 할 때 폼 상태를 재설정하는 옵션
criteriaMode모든 유효성 오류를 표시할지 한 번에 하나씩 표시할지 선택
shouldFocusError폼 요소에서 오류가 발생했을 때 자동으로 해당 폼 요소에 포커스를 설정하는 기능
delayError오류를 즉시 표시하지 않고 지연
shouldUseNativeValidation브라우저 내장 폼 제약 API를 사용할지 여부를 설정
shouldUnregister언마운트 후 입력 등록 해제를 활성화 또는 비활성화

반응형 오류 - 폼의 유효성 검사 중 발생하는 오류를 의미합니다. 이것은 사용자가 입력을 하거나 수정할 때마다 자동으로 업데이트되어 사용자에게 실시간으로 오류를 보여줍니다.

Props

🚀 대표적으로 자주 사용되는 mode, defaultValues 에 대해서 알아보자


mode : onChange | onBlur | onSubmit | onTouched | all = 'onSubmit'

사용자가 폼을 제출하기 전에 유효성 검사 전략을 구성할 수 있게 합니다. 유효성 검사는 handleSubmit 함수를 호출하여 트리거되는 onSubmit 이벤트 중에 발생합니다.

이름타입설명
onSubmitstring유효성 검사는 제출 이벤트에서 발생하며, 사용자가 입력 값을 변경할 때마다 해당 입력이 다시 유효성을 검사하도록 하기 위해 onChange 이벤트 리스너가 활성화
onBlurstring블러 이벤트 발생 시 동작
onChangestring유효성 검사는 각 입력의 change 이벤트에서 발생하며, 여러 번의 렌더링을 유발하므로 주의 필요.
onTouchedstring처음 블러 이벤트에서 동작됩니다. 그 후에는 모든 change 이벤트에서 동작합니다.
allstring블러 및 change 이벤트에서 모두 동작합니다.

defaultValues : FieldValues | () ⇒ Promise<FieldValues>

defaultValues prop은 전체 폼에 기본값을 채웁니다. 동기적 및 비동기적으로 기본값을 할당하는 데 모두 지원합니다. 입력의 기본값은 defaultValue 또는 defaultChecked를 사용하여 설정할 수 있지만 (공식 React 문서), 전체 폼에는 defaultValues를 사용하는 것이 좋습니다.

useForm({
  defaultValues: {
    firstName: '',
    lastName: ''
  }
})

// async를 활용한 기본값 세팅
useForm({
  defaultValues: async () => fetch('/api-endpoint');
})

💬 RULES

  • undefined를 기본값으로 제공하는 것은 피해야 합니다. 이는 제어 컴포넌트의 기본 상태와 충돌할 수 있습니다.

  • defaultValues는 캐시됩니다. 이를 재설정하려면 reset API를 사용하세요.

  • defaultValues는 기본적으로 제출 결과에 포함됩니다.

  • Moment나 Luxon과 같은 프로토타입 메서드를 포함하는 사용자 지정 객체를 defaultValues로 사용하는 것을 피하는 것이 좋습니다.


자주 사용되는 메서드

🚗 Register

</> register: (name: string, RegisterOptions?) => ({ onChange, onBlur, name, ref })

register를 사용하면 입력 또는 선택 요소를 등록하고 React Hook Form에 유효성 검사 규칙을 적용할 수 있습니다. 유효성 검사 규칙은 모두 HTML 표준을 기반으로 하며 사용자 정의 유효성 검사 방법도 허용합니다.

1️⃣ Options

  • required

  • pattern

  • validate

  • setValueAs

  • valueAsNumber

  • valueAsDate

  • maxLength

  • minLength

  • max

  • min

  • shouldUnregister

  • disabled

  • onBlur

  • onChange

  • deps

  • value

required

이 값이 true인 경우, 입력 필드가 폼을 제출하기 전에 반드시 값을 가져야 함을 나타냅니다. 오류 객체에 오류 메시지를 반환하려면 문자열을 할당할 수 있습니다.

참고 : 이 구성은 필수 입력 유효성 검사에 대한 웹 제한 API와 일치하며, 객체 또는 배열 유형의 입력에는 대신 validate 함수를 사용합니다.

maxLength

이 입력에 허용되는 값의 최대 길이입니다.

<input {...register("test", { maxLength : { value: 2, message: 'error message' } })} />

max

이 입력이 허용하는 최대 값입니다.

<input type="number" {...register("test", { max : { value: 2, message: 'error message' }})} />

pattern

이 입력에 대한 정규식 패턴입니다.

참고 : /g 플래그가 있는 RegExp 객체는 일치가 발생한 lastIndex를 추적합니다.

<input {...register("test", { pattern: { value: /[A-Za-z]{3}/, message: 'error message' }})} />

validate

validate에 인수로 콜백 함수를 전달할 수 있으며, 모두를 유효성 검사하기 위해 콜백 함수의 객체를 전달할 수도 있습니다. 이 함수는 required 속성에 포함된 다른 유효성 검사 규칙에 의존하지 않고 독립적으로 실행됩니다.

참고 : 객체나 배열 입력 데이터의 경우, 대부분의 규칙이 문자열, 문자열[], 숫자 및 부울 데이터 유형에 적용되므로 유효성 검사를 위해 validate 함수를 사용하는 것이 좋습니다.

<input {...register("test", { validate: value => value === '1' || 'error message' })} />

// 콜백 함수 객체
<input
  {...register("test1", {
    validate: {
      positive: v => parseInt(v) > 0 || 'should be greater than 0',
      lessThanTen: v => parseInt(v) < 10 || 'should be lower than 10',
      validateNumber: (_: number, formValues: FormValues) => {
        return formValues.number1 + formValues.number2 === 3 || 'Check sum number';
      },
      checkUrl: async () => await fetch() || 'error message',
      messages: v => !v && ['test', 'test2']
    }
  })}
/>

2️⃣ Rules

  • 이름은 필수이며 고유해야 합니다(기본적으로 라디오 및 체크박스는 제외됩니다). 입력 이름은 점(dot) 및 괄호(bracket) 구문을 모두 지원하며, 이를 통해 중첩된 폼 필드를 쉽게 생성할 수 있습니다.

  • 이름은 숫자로 시작하거나 키 이름으로 숫자를 사용할 수 없습니다. 특수 문자는 피해야 합니다.

  • 타입스크립트 사용의 일관성을 위해 점(dot) 구문만 사용하고 있으므로 배열 형태의 폼 값에는 괄호(bracket) [] 구문이 작동하지 않습니다.

      register('test.0.firstName'); // ✅
      register('test[0]firstName'); // ❌
    
  • 비활성화된 입력은 정의되지 않은 폼 값으로 이어집니다. 사용자가 입력을 업데이트하는 것을 방지하려면 readOnly를 사용하거나 전체 <fieldset />을 비활성화하세요. 예시는 다음과 같습니다.

  • 필드 배열을 생성하려면 입력 이름 뒤에 점(dot)과 숫자를 추가해야 합니다. 예를 들어: test.0.data

  • 각 렌더링마다 이름을 변경하면 새로운 입력이 등록됩니다. 각 등록된 입력에는 정적 이름을 유지하는 것이 좋습니다.

  • 입력 값과 참조는 이제 언마운트에 따라 제거되지 않습니다. 그 값을 제거하려면 unregister를 호출할 수 있습니다.

  • 개별 register 옵션은 undefined 또는 {}로 제거할 수 없습니다. 대신 개별 속성을 업데이트할 수 있습니다.

      register('test', { required: true });
      register('test', {}); // ❌
      register('test', undefined); // ❌
      register('test', { required: false });  // ✅
    
  • 일부 키워드는 유형 검사와 충돌을 피해야 합니다. ref, _f입니다.


3️⃣ Examples

import * as React from "react";
import { useForm } from "react-hook-form";

export default function App() {
  const { register, handleSubmit } = useForm({
    defaultValues: {
      firstName: '',
      lastName: '',
      category: '',
      checkbox: [],
      radio: ''
    }
  });

  return (
    <form onSubmit={handleSubmit(console.log)}>
      <input {...register("firstName", { required: true })} placeholder="First name" />

      <input {...register("lastName", { minLength: 2 })} placeholder="Last name" />

      <select {...register("category")}>
        <option value="">Select...</option>
        <option value="A">Category A</option>
        <option value="B">Category B</option>
      </select>

      <input {...register("checkbox")} type="checkbox" value="A" />
      <input {...register("checkbox")} type="checkbox" value="B" />
      <input {...register("checkbox")} type="checkbox" value="C" />

      <input {...register("radio")} type="radio" value="A" />
      <input {...register("radio")} type="radio" value="B" />
      <input {...register("radio")} type="radio" value="C" />

      <input type="submit" />
    </form>
  );
}

🚕 handleSubmit

</> handleSubmit: ((data: Object, e?: Event) => Promise<void>, (errors: Object, e?: Event) => void) => Promise<void>

이 함수는 양식 유효성 검사가 성공하면 양식 데이터를 받습니다.

1️⃣ Props

💬 RULES

  • handleSubmit를 사용하여 양식을 비동기적으로 쉽게 제출할 수 있습니다.

      handleSubmit(onSubmit)()
    
      // You can pass an async function for asynchronous validation.
      handleSubmit(async (data) => await fetchAPI(data))
    
  • 비활성화된 입력란은 양식 값으로 정의되지 않습니다. 입력을 업데이트하지 못하도록 하고 양식 값을 유지하려면 readOnly를 사용하거나 전체 <fieldset />을 비활성화할 수 있습니다.

  • handleSubmit 함수는 onSubmit 콜백 내에서 발생한 오류를 무시하지 않습니다. 따라서 비동기 요청 내에서 try와 catch를 사용하여 이러한 오류를 고객에게 적절하게 처리하는 것이 좋습니다.

      const onSubmit = async () => {
        // async request which may result error
        try {
          // await fetch()
        } catch (e) {
          // handle your error
        }
      }
    
      <form onSubmit={handleSubmit(onSubmit)} />
    

2️⃣ Examples

import React from "react"
import { useForm, SubmitHandler } from "react-hook-form"

type FormValues = {
  firstName: string
  lastName: string
  email: string
}

export default function App() {
  const { register, handleSubmit } = useForm<FormValues>()
  const onSubmit: SubmitHandler<FormValues> = (data) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName")} />
      <input {...register("lastName")} />
      <input type="email" {...register("email")} />

      <input type="submit" />
    </form>
  )
}

마무리

이렇게 오늘은 간단히 react-hook-form 을 사용하는 방법과 간단한 유효성 검증을 수행할 수 있는 방법들에 대해서만 적어보았는데 유효성 검증을 위한 Validation 로직과 제출을 위한 Validation 로직, 메시지를 결정하는 함수 등 react-hook-form 에서 제공되는 기능들을 효과적으로 사용하기 위해 고려해야 할 부분은 차고 넘친다.. 추후에 useForm 말고도 react-hook-form 에서 제공하는 여러 API 에 대해서도 정리해보려고 한다. 킵 고잉 🛴 ~


참고

https://react-hook-form.com/docs/useform

https://react-hook-form.com/docs/useform/register

https://react-hook-form.com/docs/useform/handlesubmit

https://react.dev/reference/react-dom/components/input

https://tech.inflab.com/202207-rallit-form-refactoring/react-hook-form/