본문 바로가기

프론트엔드/react-hook-form

react-hook-form(1) : Performant, flexible and extensible forms with easy-to-use validation

리액트에서 폼 태그를 더 용이하게 사용할 수 있다는.. 도구.. 한번 사용해보겠습니다.

 

기존에 들어만 봤었던 formik이라는 라이브러리도 있었는데, 아래 블로그를 보고 react-hook-form을 사용하기로 했다.

https://blog.songc.io/react/react-form-control/

 

[React] Form 상태 다루기 (Formik vs react-hook-form)

이 포스트에서는 form 상태관리를 하는 방법중에서 React-hook-form 과 Formik을 다루면서 두 라이브러리를 비교하는 글입니다. Form 상태를 다루기 위해서 어떤 것을 사용하고 선호하시나요? Form 상태를

blog.songc.io

 


https://react-hook-form.com/get-started

 

Get Started

Performant, flexible and extensible forms with easy-to-use validation.

react-hook-form.com

 

Get Started부터 보고 시작해보려고 하는데, 일단 register, handleSubmit 등에 대한 설명은 전혀 없다.

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

type Inputs = {
  example: string,
  exampleRequired: string,
};

export default function App() {
  const { register, handleSubmit, watch, formState: { errors } } = useForm<Inputs>();
  const onSubmit: SubmitHandler<Inputs> = data => console.log(data);

  console.log(watch("example")) // watch input value by passing the name of it

  return (
    /* "handleSubmit" will validate your inputs before invoking "onSubmit" */
    <form onSubmit={handleSubmit(onSubmit)}>
      {/* register your input into the hook by invoking the "register" function */}
      <input defaultValue="test" {...register("example")} />
      
      {/* include validation with required or other standard HTML validation rules */}
      <input {...register("exampleRequired", { required: true })} />
      {/* errors will return when field validation fails  */}
      {errors.exampleRequired && <span>This field is required</span>}
      
      <input type="submit" />
    </form>
  );
}

 

만약 라이브러리를 사용하지 않고 form태그를 사용하려면, input마다 attribute를 다 설정해줘야 하는데, 일단 그건 편하다.

 


 

React Hook Form의 키 컨셉은 우리의 컴포넌트를 hook에 register하는 것이다.

register를 통해 input의 value에 대한 유효성 검사나 제출이 가능해진다.

 

각각의 input은 등록할 때 이름(ex. 아래 예제 코드의 "firstName", "gender")이 필요하다.

import React from "react";
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>
  );
}

React Hook Form의 Validation

React Hook Form은 form 유효성 검사를 위한 기존 HTML 유효성 검사 표준에 맞춰 form의 유효성 검사를 쉽게 만든다.

 

예시는 다음과 같고,

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

interface IFormInput {
  firstName: string;
  lastName: string;
  age: number;
}

export default function App() {
  const { register, handleSubmit } = useForm<IFormInput>();
  const onSubmit: SubmitHandler<IFormInput> = data => console.log(data);
   
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName", { required: true, maxLength: 20 })} />
      <input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} />
      <input type="number" {...register("age", { min: 18, max: 99 })} />
      <input type="submit" />
    </form>
  );
}

 

지원하는 유효성 검사 규칙은 다음과 같다:

  • required
  • min
  • max
  • minLength
  • maxLength
  • pattern
  • validate

더 자세하게 알고 싶으면 register section을 보라고 한다. 그래서 Get Started 순서와 다르게 register 섹션을 먼저 보자.


그래서 register가 뭐냐

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

register 메서드를 사용하면 React Hook Form에 input이나 select 엘리먼트를 등록하고, 유효성 검사 규칙을 등록할 수 있다. 모든 유효성 검사 규칙은 (위에서 말했듯이) HTML 표준을 따르고, 또한 유효성 검사 메서드를 커스터마이징 할 수 있다.

 

input의 이름을 인자로 register 함수를 호출하면, 다음의 메서드들을 반환받는다.

Name Type Description
onChange ChangeHandler onChange prop to subscribe the input change event
onBlur ChangeHandler onBlur prop to subscribe the input blur event
ref React.Ref<any> input reference for hook form to register
name string input's name being registered

 

다음과 같이 쓸 수 있는 것이다.

const { onChange, onBlur, name, ref } = register('firstName'); 
// include type check against field path with the name you have supplied.
        
<input 
  onChange={onChange} // assign onChange event 
  onBlur={onBlur} // assign onBlur event
  name={name} // assign name prop
  ref={ref} // assign ref prop
/>

그리고 위 코드는, 다음과 똑같다

<input {...register('firstName')} />

 

내가 궁금한 건 register의 또 다른 인자인 RegisterOptions인데, 내가 하고 싶은 건 단순히 input의 최대 길이나 input의 패턴 같은, input의 타입이 text일 때 할 검사가 아니라 input의 타입이 file일 때, 파일 크기나 확장자, 여러개의 파일에 대한 용량 검사 같은 게 하고 싶은 것이기 때문이다.

 

이 페이지에 required나 pattern, maxLength 등 제공하는 옵션들에 대한 자세한 설명과 예시가 나와있다.

 

나는 그 중에서 validate라는 옵션이 눈에 띄었는데, 유효성 검사를 위한 콜백을 인자로 넘겨줄 수 있다고 한다. 단순히 콜백 함수만 넘기는 게 아니라 콜백함수로 이뤄진 객체도 인자로 넘길 수 있다. 페이지에 나와있는 예제 코드는 다음과 같다.

<input
  {...register("test", {
    validate: value => value === '1'
  })}
/>
// object of callback functions
<input
  {...register("test1", {
    validate: {
      positive: v => parseInt(v) > 0,
      lessThanTen: v => parseInt(v) < 10,
      checkUrl: async () => await fetch(),
    }
  })}
/>

 

다른 옵션들은 페이지에서 확인하고, 잠시 register를 사용할 때 유의해야할 규칙들(역시 공식 문서에 잘 정리돼있지만)을 짚고 넘어가자.

Rules

  • register의 첫 번째 인자 name은 꼭 넣어야 하고(required), 다른 name과 겹치면 안 된다(unique). Input name은 점과 괄호를 모두 허용한다. 점과 괄호를 사용하면 중첩된 form field를 쉽게 만들 수 있다.
register("firstName")	{firstName: 'value'}
register("name.firstName")	{name: { firstName: 'value' }}
register("name.firstName.0")	{name: { firstName: [ 'value' ] }}
  • name은 숫자로 시작해서는 안 되고, key name도 숫자가 될 수 없다.(예시를 작성해봐야 알 것 같다)
  • 타입스크립트 사용의 일관성을 위해서 dot syntax를 사용하고 있고, 그래서 배열 form value에 []를 사용할 수 없다.
register('test.0.firstName'); // ✅
register('test[0]firstName'); // ❌
  • disabled input은 정의되지 않은 form value가 될 수 있다. 따라서 입력을 못하게 하려면 input에 readOnly라는 attribute를 설정해주거나, <filedSet /> 전체를 disabled로 만들면 된다.(예제 코드 전체는 여기서 확인)
...
      <form onSubmit={handleSubmit(onSubmit)}>
        <input readOnly {...register("firstName")} placeholder="First Name" />
        <fieldset disabled>
          <input {...register("lastName")} placeholder="First Name" />
        </fieldset>
        <input type="submit" />
      </form>
...
  • 배열로 된 field를 생성하려면, input name에 점과 숫자가 와야 된다. ex) test.0.data
  • 매 렌더링마다 이름을 변경하면 새 input이 등록된다. 따라서 input의 name은 static한 게 좋다.
  • input value와 reference는 언마운트 시에 제거되지 않으므로, unregister 해줘야 한다.
  • 각각의 register option은 undefined 또는 {}로 제거할 수 없다. 대신 각각의 attribute를 업데이트 할 수 있다.
register('test', { required: true });
register('test', {}); // ❌ 
register('test', undefined); // ❌ 
register('test', { required: false });  // ✅

 

한글로 번역하면서도 잘 모르겠는 부분도 있고, 페이지 중에서 옮기지 않은 부분이 있으므로 호오오옥시나 이 글을 보신다면 꼭 공식문서를 참고해주세요..

그래서 register 함수는 어디서 어떻게 불러와야 하는 건가 하는 의문이 생길 수 있다.

 

register는 React Hook Form이 제공하는 useForm()이라는 훅을 호출함으로써 사용할 수 있다.

import {useForm} from 'react-hook-form';

...

type FormInputs = {
  firstName: string;
  lastName: string;
};

// 이르케!
const { register } = useForm<FormInputs>({
  mode: 'onSubmit',
  reValidateMode: 'onChange',
  defaultValues: {},
  resolver: undefined,
  context: undefined,
  criteriaMode: "firstError",
  shouldFocusError: true,
  shouldUnregister: false,
  delayError: undefined
})
...

 

useForm()에 대해서는 다른 게시글에 정리해야지..

registerOptions의 validate로 내가 원하는 파일 검사를 할 수 있는지 확인해보러가야게따!