
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를 전달하는게 포인트이다.
분명 위의 codesandbox의 예제에서는 제대로 작동됐는데, 내가 진행중인 프로젝트에선 field의 값을 확인해보면 undefined로 출력됐다. 위의 코드에서 달라진 부분이 없고, 도대체 뭐가 문제인지 몰라서 끙끙 헤맸는데 이 문제는 버전문제였다.
진행중인 프로젝트에서는 "react-hook-form": "^6.13.1", 버전을 쓰고 있었는데, 위의 field와 render를 사용하는 기능은 최신 버전인 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메서드를 사용하는 방법을 찾아봤는데, 아래와 같은 방법을 사용하면 됐다.
<Controller
control={control}
name="date"
format={dateFormat}
defaultValue={date}
render={({ onChange }) => (
<DatePicker onChange={(value, dateString) => onChange(dateString)} />
)}
/>
as를 사용해서 onChange메서드를 Controller에 직접 지정하거나, render를 사용한 뒤 onChange를 직접 비구조화 할당으로 가져와 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로 요소를 가져오면, onChange와 onBlur와 같은 다양한 메서드를 하나씩 지정해줘야 하는 반면 render를 사용하면 field나 위의 예시와 같이 조금 더 쉽게 지정할 수 있다.