react-hook-form 하위 제어 컴포넌트 값 전송하기 (antd datepicker 값 전송하기)

짱유경·2021년 4월 23일
12
post-thumbnail

📋 react-hook-form & 제어컴포넌트

react-hook-form은 기본적으로 비제어 컴포넌트 방식으로 폼을 접근한다. 하지만 antd나 react-datepicker, react-select 등 기본적으로 제어 컴포넌트 요소의 경우 ref를 전달해주는 방식이 아닌 제어 요소를 다루는 방법을 사용해야 한다.

react-hook-form에서는 그런 제어 컴포넌트들을 다루기 위해 Controller를 제공한다. 하지만 Controller를 사용할 때, 제어 컴포넌트를 바로 사용하지 않고 하위 요소로 분리하면 일반적인(?) 방식으론 값이 제출되지 않고 undefined로 출력됐다.

🎯 첫 시도 & 문제점

처음에는 부모 컴포넌트에서 Controller를 직접 선언했는데, 이렇게 하면 undefined를 반환했다. 따로 컴포넌트를 분리하지 않은 TimePicker의 경우 아래와 같이 바로 데이터를 전송하는 컴포넌트에다가 선언해도 잘 값이 출력됐는데, 아래와 같은 방법으론 값이 출력되지 않았다.

// undefined
<InputOther
  label="label1"
  component={
    <Controller
      as={<InputDatePicker />}
      name="label1"
      type="date"
      control={control}
    />
  }
/> 

✨ 해결 방법

부모 컴포넌트

import InputDatePicker from "./InputDatePicker";
import { useForm } from "react-hook-form";
import "./styles.css";
import "antd/dist/antd.css";

export default function App() {
  const { handleSubmit, control } = useForm();

  const onSubmit = (data) => {
    console.log(data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <InputDatePicker 
        // control을 자식 컴포넌트에 넘겨주기
        control={control} 
      /> 
      <button type="submit">전송</button>
    </form>
  );
}

자식 컴포넌트

import React from "react";
import { DatePicker } from "antd";
import { Controller } from "react-hook-form";

// control을 받아오고,
function InputDatePicker({ control }) {
  const dateFormat = "YYYY-MM-DD";

  return (
    // Controller를 선언한 후 control을 속성으로 넣어주면 된다.
    <Controller
      control={control}
      name="installDate"
      format={dateFormat}
      // render를 사용해서, field값을 복사하거나 꺼내 쓰면 된다.
      // field안에는 value나 onBlur와 같은 함수도 있음
      // render안의 onChange를 조작해, onChange안에 들어갈 값을
      // 선택할 수 있다.
      render={({ field: { onChange } }) => (
        // antd의 datepicker에서 e.target.value는 
        // moment 객체 그대로를 반환하기에,
        // "2021-04-15"와 같은 값을 얻고싶다면, 두번째 파라미터
        // "dateString"을 추가해서 값을 넣어야 한다.
        <DatePicker
          onChange={(value, dateString) => {
            onChange(dateString); 
          }}
          format={dateFormat}
        />
      )}
    />
  );
}

export default InputDatePicker;


잘 출력되는 모습 👏👏 !!
Controller를 부모 컴포넌트에서 사용하지 않고, 자식 컴포넌트에서 사용하고 control를 전달하는게 포인트이다.

⚠️ Controller field의 값이 undefined로 반환될 때

분명 위의 codesandbox의 예제에서는 제대로 작동됐는데, 내가 진행중인 프로젝트에선 field의 값을 확인해보면 undefined로 출력됐다. 위의 코드에서 달라진 부분이 없고, 도대체 뭐가 문제인지 몰라서 끙끙 헤맸는데 이 문제는 버전문제였다.
진행중인 프로젝트에서는 "react-hook-form": "^6.13.1", 버전을 쓰고 있었는데, 위의 fieldrender를 사용하는 기능은 최신 버전인 7.13.0에서 사용이 되고 있었다.
그런데, 7.13.0버전으로 업그레이드 하면 기존에 사용되던
<Input label="label1" name="label1" register={register} />
와 같이 register를 ref로 전달해주는 방식에서 path.split is not a function ~~ 하는 에러가 생긴다.
해당 문제를 해결하려면, 위와 같은 형식으로, register를 전달시키는게 아닌
<Input label="label1" name="label1" {...register("label1")} />
헤당 형식으로 바꿔줘야 했었다.

하지만 field를 사용하자고 모든 폼을 바꾸는건 정말정말정말..수가 많아서 6.13.1버전에서 onChange메서드를 사용하는 방법을 찾아봤는데, 아래와 같은 방법을 사용하면 됐다.

6.13.1 버전에서 render & onChange 메서드 사용하기

<Controller
  control={control}
  name="date"
  format={dateFormat}
  defaultValue={date}
  render={({ onChange }) => (
    <DatePicker onChange={(value, dateString) => onChange(dateString)} />
  )}
/>

as를 사용해서 onChange메서드를 Controller에 직접 지정하거나, render를 사용한 뒤 onChange를 직접 비구조화 할당으로 가져와 render내부에서 사용하면 된다.

잘 전송되는 감격의 모습...

🎁 보너스 - Controller 컴포넌트에서 as와 render의 차이

// 버전 6.13.1
- <Controller as={Input}
-   name="test"
-   onChange={([_, value]) => value}
-   onChangeName="onTextChange"
-   onBlur={([_, value]) => value}
-   onBlurName="onTextChange"
-   valueName="textValue"
- />

// 버전 7.13.0
+ <Controller name="test"
+   render={({ onChange, onBlur, value }) => {
+     return <Input
+       valueName={value}
+       onTextChange={(val) => onChange(value)}
+       onTextBlur={(val) => onBlur(value)}
+     />
+   }}
+ />

as로 요소를 가져오면, onChangeonBlur와 같은 다양한 메서드를 하나씩 지정해줘야 하는 반면 render를 사용하면 field나 위의 예시와 같이 조금 더 쉽게 지정할 수 있다.

0개의 댓글