React Hook Form과 외부 라이브러리 연동하기 (react-select, react-datePicker, antd, mui 등)

2022. 7. 22. 23:01Library,Framework

728x90

웹이나 앱을 만들 때 훅을 애용하고 폼을 자주 다룰일이 많으셨다면 React Hook Form 라이브러리를 한 번쯤 사용해 보셨을 겁니다.  필자는 formik이나 redux-form도 가끔 사용하긴 하지만 간결한 코드 작성이 가능한 훅을 더 선호하기 때문에 해당 라이브러리를 즐겨 사용합니다. 커스텀 인풋 창을 만들어 사용할 경우 register후 바로 사용해주면 되는 구조이지만,  react-select이나 react-datepicker 등 외부 라이브러리를 사용해야 할 경우 하나의 스텝이 더 추가되기 때문에 이번 포스트에서는 해당 내용을 주제로 글을 작성해보겠습니다.

 

글 작성일 기준으로 사용한 버전은 아래와 같습니다.

    "react-hook-form": "^7.33.1",
    "react-select": "^5.4.0",

일단 React Hook Form으로 input을 등록해서 사용하는 방법은 아래와 같습니다.

 

const Home = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();


  const handleFormSubmit = async (formValue) => {
    console.log(formValue);
  };

  return (
    <form onSubmit={handleSubmit(handleFormSubmit)}>
      <fieldset>
        <legend>사용자 선호도 조사 폼</legend>
        <div>
          <label htmlFor="name">이름</label>
          <input id="name" {...register("name")} />
        </div>
        <button type="submit">Submit</button>
      </fieldset>
    </form>
  );
};

export default Home;

 

useForm이라는 훅에서 반환되는 register 메소드에 사용할 필드명을 전달해주면 됩니다.

위 예제에서는 아래와 같이 스프레드 연산자를 사용해 prop을 넘겨줬는데

 

<input id="name" {...register("name")} />

 

이는 input의 값을 제어할수 있도록 아래와 같은 prop들을 전달해주었다고 보시면 됩니다.

const { name, ref, onChange, onBlur } = register("name"); 
        
<input 
  name={name}
  ref={ref}
  onChange={onChange}
  onBlur={onBlur}
/>

인풋에 이름을 작성하고 폼을 전송해주면 아래와 같이 console.log가 정상적으로 찍히는 걸 확인하실 수 있습니다

{name: '코딩마차'}

 


 

외부라이브러리 컴포넌트 폼 제어하기

 

그럼 외부 라이브러리인 react-select는 어떨까요? 

위와 동일하게 작성을 해보겠습니다.

<label htmlFor="flavor">좋아하는 맛</label>
<Select
inputId="flavor"
options={[
    { value: "chocolate", label: "초콜릿" },
    { value: "strawberry", label: "딸기" },
    { value: "vanilla", label: "바닐라" },
  ]}
{...register("flavor")}
/>

이렇게 작성해서 제대로 동작하면 너무 편하겠지만 아쉽게도 어떤 값을 선택해도 flavor의 필드값은 undefined로 나오게 됩니다.

 

그렇기 때문에 React Hook Form에서는 외부 라이브러리의 필드값을 제어해주기 위해선 Controller 컴포넌트를 별도로 사용해야합니다. 이는 단어 그대로 외부 라이브러리의 값을 제어해주는 컨트롤러의 역할을 하게 되는 컴포넌트로써 아래와 같이 사용 가능합니다.

 

import React from "react";
import { useForm, Controller } from "react-hook-form";
import Select from "react-select";

const Home = () => {
  const {
    control,
    register,
    handleSubmit,
    formState: { errors },
  } = useForm();

  const options = [
    { value: "chocolate", label: "초콜릿" },
    { value: "strawberry", label: "딸기" },
    { value: "vanilla", label: "바닐라" },
  ];

  const handleFormSubmit = async (formValue) => {
    console.log(formValue);
  };

  return (
    <form onSubmit={handleSubmit(handleFormSubmit)}>
      <fieldset>
        <legend>사용자 선호도 조사 폼</legend>
        <div>
          <label htmlFor="name">이름</label>
          <input id="name" {...register("name")} />
        </div>
        <label htmlFor="flavor">좋아하는 맛</label>
     
        <Controller
          control={control}
          name="flavor"
          render={({ field: { onChange, value, ref } }) => (   	          
            <Select
              inputId="flavor"
              options={options}
              // 참조를 전달해줌으로써 hook form이랑 select input이랑 연결 (전달시 에러가 있을시 자동으로 해당 인풋으로 포커스해줌)
              ref={ref}
              // react-select 컴포넌트를 통해 선택된 값이랑 폼 선택값이 동일하도록 설정
              value={options.find((option) => option.value === value)}
              // react-select 컴포넌트를 통해 값을 변경하였을경우 flavor 필드폼값도 변경.
    		  // 폼 입력값은 {label: "레이블", "value": "값"}의 객체가 아닌 value 값만 추출하여 전달  }
              onChange={(option) => onChange(option.value)}
            />
          )}
        />

        <button type="submit">Submit</button>
      </fieldset>
    </form>
  );
};

export default Home;

 

Controller를 사용할 경우 따로 register로 prop을 전달해주지 않고 useForm에서 반환되는 control을 prop으로 전달해주고, 사용할 필드 이름 (name), 그리고 render에 외부 라이브러리에서 불러온 컴포넌트를 전달해주면 됩니다.  위와 같이 render 콜백 함수의 field를 디스트럭처링 하게 되면 onChange, value, ref, onBlur 등 필드 값을 제어할 수 있는 프로퍼티들을 받을수 있습니다. 이를 Select 컴포넌트에 그대로 넘겨주게 되면 제어된 폼을 사용할 수 있게 됩니다. Select의 경우 표준 프로퍼티 이름을 사용하기 때문에 그대로 넘겨줘도 문제가 없지만 간혹 외부 라이브러리를 사용하다 보면 표준 프로퍼티 이름 대신에 onDateChange이나  inputRef 같은 비표준 프로퍼티 이름을 사용하는 경우가 많기 때문에 해당 라이브러리의 api를 잘 숙지하신뒤 알맞게 전달해주시면 됩니다.

 


에러 처리

에러 처리도 매우 간단합니다.

 

일단 외부 라이브러리를 사용하지 않을 경우를 살펴보면 register의 두 번째 인자로 객체 형식의 규칙을 전달해주면 해당 규칙에 어긋 날 경우, formState의 errors 객체에 에러가 발생한 필드 이름과 에러 메시지가 기입되고 폼이 전송되지 않는 방식입니다.

 

Yup 같은 validation schema를 사용하지 않고 직접 validation을 한다고 가정했을 때 아래와 같이 작성할 수 있습니다.

const ERROR_RULES = {
    required: "이름을 입력해주세요",
    maxLength: {
    value: 15,
    message: "이름은 최대 15글짜 까지 입력이 가능합니다",
    },
}
<input id="name" {...register("name", ERROR_RULES)} />
<span>{errors.name && errors.name?.message}</span>

 

외부 라이브러리 컴포넌트를 사용할 경우에는 register대신에 Controller 컴포넌트의 rules prop에 해당 규칙을 전달해주면 동일하게 적용이 가능합니다. 

 

 <Controller
	control={control}
    name="flavor"
    rules={{required: "선호하는 맛은 필수 입력사항입니다"}}
    ...생략
    />
 <span>{errors.flavor && errors.flavor?.message}</span>

 

이렇게 설정해주게 되면 아무 값도 선택되지 않았을 경우 "선호하는 맛은 필수 입력사항입니다"라는 에러 문구와 앞서 ref를 전달해줬기 때문에 해당 Select 컴포넌트에 자동으로 포커스가 이동되는 걸 확인할 수가 있습니다.

 


 

 

해당 포스트에서는 react hook form에서 react-select 라이브러리를 제어해서 사용하는 방법에 대해 알아보았지만 react hook form에서 제공하는 Controller 컴포넌트를 사용하면 비슷한 방식으로 react-datepicker, antd, mui input등 거의 모든 외부라이브러리 컴포넌트와 연결해서 사용할수가 있습니다. 만약 특정 컴포넌트와 연결하는데 어려움이 있으시다면 댓글을 남겨주시면 해당 예제를 추가해보도록 하겠습니다.