[TIL/React-Hook-Form] 2024/07/09

원민관·2024년 7월 9일
0

[TIL]

목록 보기
140/159
post-thumbnail

reference:
1) https://www.react-hook-form.com/api/usecontroller/controller
2) https://legacy.reactjs.org/docs/render-props.html

✅ React-Hook-Form 공식문서 톺아보기

6. Integrating with UI libraries ✍️

import Select from "react-select";
import { useForm, Controller } from "react-hook-form";
import Input from "@material-ui/core/Input";

const App = () => {
  const { control, handleSubmit } = useForm({
    defaultValues: {
      firstName: '',
      select: {}
    }
  });
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="firstName"
        control={control}
        render={({ field }) => <Input {...field} />}
      />
      <Controller
        name="select"
        control={control}
        render={({ field }) => <Select 
          {...field} 
          options={[
            { value: "chocolate", label: "Chocolate" },
            { value: "strawberry", label: "Strawberry" },
            { value: "vanilla", label: "Vanilla" }
          ]} 
        />}
      />
      <input type="submit" />
    </form>
  );
};

외부 UI 라이브러리와 React Hook Form을 어떻게 통합해야 하는지 설명하는 section이다.

If the component doesn't expose input's ref, then you should use the Controller component, which will take care of the registration process.

Controller 컴포넌트를 사용하면, 외부 UI 라이브러리의 요소의 registration process를 잘 수행할 수 있다고 설명하고 있다.

아래의 코드가 본 section의 핵심이다. 하지만 공식 문서의 Get Started 파트에서는 피상적인 사용법만 다루고 있기에, Controller Component의 더 깊은 내용을 확인해야 했다.

<Controller
  name="firstName"
  control={control}
  render={({ field }) => <Input {...field} />}
/>

external controlled component와 쉽게 통합하기 위해 사용하는 'Wrapper Component'임을 명시하고 있다.

render props가 핵심인데, render는 컴포넌트에 event와 value를 연결할 수 있는 기능을 제공한다. 자식 컴포넌트에 onChange, onBlur, name, ref, value를 제공하며, 특정 '입력 상태'를 포함하는 fieldState 객체도 제공한다.

render의 type은 'Function'이다. 그런데 render={({ field }) => <Input {...field} />} 부분이 솔직히 잘 이해가 되지 않았다. 이유를 알아보니, render props가 무엇인지 몰라서 그랬던 것이었다.(React 공식 문서 읽어야 한다는 뜻.)

'render prop'은 함수이고, Controller 컴포넌트에 의해 호출되도록 내부적으로 구현이 되어있다. field 객체를 포함한 다양한 인자를 받을 수 있는 함수인데, 위 코드에서는 field를 구조 분해 할당을 통해 추출한다. 이때, field 객체는 Controller 컴포넌트에 의해 자동으로 제공되는 'form field와 관련된 상태와 메서드'를 포함한다. 이렇게 추출한 field 객체를 하위 컴포넌트(MUI Component 같은)에 전달하여 외부 컴포넌트 요소가 form field의 상태와 상호작용할 수 있게 되는 것이다.

과거 React doc을 살펴보니 커스텀 훅으로 대체되었다고 한다. 예전 공식 문서에서 설명하는 것처럼, render prop은 react 컴포넌트 간에 코드를 공유하기 위한 기법인데, custom hook의 존재 이유와 정확히 일치하기 때문이다. custom hook의 시초 같은 느낌이다.

7. Integrating Controlled Inputs ✍️

import { useForm, Controller } from "react-hook-form";
import { TextField, Checkbox } from "@material-ui/core";

function App() {
  const { handleSubmit, control, reset } = useForm({
    defaultValues: {
      checkbox: false,
    }
  });
  const onSubmit = data => console.log(data);

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Controller
        name="checkbox"
        control={control}
        rules={{ required: true }}
        render={({ field }) => <Checkbox {...field} />}
      />
      <input type="submit" />
    </form>
  );
}

계속 공식 문서를 읽다 보니, Controlled component와 Uncontrolled component가 핵심이라는 생각이 든다.

Controlled component는 React의 상태(state)에 의해 값이 제어됨을 의미한다. 한마디로, 사용자가 입력 form을 조작할 때마다 React 상태가 업데이트되며, 이에 따라 UI가 리렌더링 된다는 뜻이다.

이에 반해, Uncontrolled component는 상태에 의존하지 않고 DOM 요소의 ref를 통해 값에 접근한다. 사용자의 입력에 따라 DOM 요소의 값이 변화할지언정, React의 상태는 이를 추적하지 않기에, 렌더링이 일어나지 않아 렌더링을 최적화할 수 있다는 장점을 갖는다.

각각의 장단점이 분명하겠지만, 사용자로부터 많은 정보를 입력받을 경우에는 불필요한 리렌더링을 최적화하는 것이 좋고, 이것이 React Hook Form의 가장 본질적인 컨셉인 것이다.

import React, { useState } from "react";  // React와 useState 훅을 import합니다.
import ReactDOM from "react-dom";  // ReactDOM을 import합니다.
import { useForm, Controller } from "react-hook-form";  // react-hook-form에서 useForm과 Controller를 import합니다.
import Header from "./Header";  // Header 컴포넌트를 import합니다.
import ReactDatePicker from "react-datepicker";  // react-datepicker 컴포넌트를 import합니다.
import NumberFormat from "react-number-format";  // react-number-format 컴포넌트를 import합니다.
import ReactSelect from "react-select";  // react-select 컴포넌트를 import합니다.
import Mui from "./Mui";  // Mui 컴포넌트를 import합니다.
import ButtonsResult from "./ButtonsResult";  // ButtonsResult 컴포넌트를 import합니다.
import DownShift from "./DownShift";  // DownShift 컴포넌트를 import합니다.
import AntD from "./AntD";  // AntD 컴포넌트를 import합니다.
import DraftExample from "./DraftExample";  // DraftExample 컴포넌트를 import합니다.
import { EditorState } from "draft-js";  // draft-js의 EditorState를 import합니다.
import InputMask from "react-input-mask";  // react-input-mask 컴포넌트를 import합니다.
import Chakra from "./Chakra";  // Chakra 컴포넌트를 import합니다.
import "react-datepicker/dist/react-datepicker.css";  // react-datepicker의 CSS 파일을 import합니다.
import "antd/dist/antd.css";  // antd의 CSS 파일을 import합니다.
import "./styles.css";  // 스타일 CSS 파일을 import합니다.

let renderCount = 0;  // 렌더링 횟수를 저장하는 변수를 초기화합니다.

const defaultValues = {
  Native: "",  // Native 입력 필드의 기본값을 설정합니다.
  TextField: "",  // TextField 입력 필드의 기본값을 설정합니다.
  Select: "",  // Select 입력 필드의 기본값을 설정합니다.
  ReactSelect: { value: "vanilla", label: "Vanilla" },  // ReactSelect 컴포넌트의 기본 선택값을 설정합니다.
  Checkbox: false,  // Checkbox 입력 필드의 기본값을 설정합니다.
  switch: false,  // switch 입력 필드의 기본값을 설정합니다.
  RadioGroup: "",  // RadioGroup 입력 필드의 기본값을 설정합니다.
  numberFormat: 123456789,  // numberFormat 입력 필드의 기본값을 설정합니다.
  AntdInput: "Test",  // AntdInput 입력 필드의 기본값을 설정합니다.
  AntdCheckbox: true,  // AntdCheckbox 입력 필드의 기본값을 설정합니다.
  AntdSwitch: true,  // AntdSwitch 입력 필드의 기본값을 설정합니다.
  AntdSlider: 20,  // AntdSlider 입력 필드의 기본값을 설정합니다.
  AntdRadio: 1,  // AntdRadio 입력 필드의 기본값을 설정합니다.
  downShift: "apple",  // downShift 입력 필드의 기본값을 설정합니다.
  ReactDatepicker: new Date(),  // ReactDatepicker 컴포넌트의 기본 날짜값을 설정합니다.
  AntdSelect: "",  // AntdSelect 입력 필드의 기본값을 설정합니다.
  DraftJS: EditorState.createEmpty(),  // DraftJS 에디터의 기본 상태를 설정합니다.
  MUIPicker: new Date("2020-08-01T00:00:00"),  // MUIPicker 컴포넌트의 기본 날짜값을 설정합니다.
  country: { code: "AF", label: "Afghanistan", phone: "93" },  // country 객체의 기본값을 설정합니다.
  ChakraSwitch: true,  // ChakraSwitch 입력 필드의 기본값을 설정합니다.
  reactMaskInput: ""  // reactMaskInput 입력 필드의 기본값을 설정합니다.
};

function App() {
  const { handleSubmit, reset, setValue, control } = useForm({ defaultValues });  // useForm 훅을 사용하여 폼 상태를 초기화하고 제어할 수 있는 객체들을 가져옵니다.
  const [data, setData] = useState(null);  // data 상태와 setData 함수를 useState 훅을 통해 초기화합니다.
  renderCount++;  // 렌더링 횟수를 증가시킵니다.

  return (
    <form onSubmit={handleSubmit((data) => setData(data))} className="form">  // 폼을 제출할 때 handleSubmit 함수를 호출하여 데이터를 설정하고, CSS 클래스 "form"을 적용합니다.
      <Header renderCount={renderCount} />  // Header 컴포넌트를 렌더링하고, renderCount를 prop으로 전달합니다.

      <Mui control={control} />  // Mui 컴포넌트를 렌더링하고, control prop으로 useForm에서 가져온 컨트롤 객체를 전달합니다.

      <hr />  // 수평선을 추가합니다.

      <AntD control={control} />  // AntD 컴포넌트를 렌더링하고, control prop으로 useForm에서 가져온 컨트롤 객체를 전달합니다.

      <hr />  // 수평선을 추가합니다.

      <div className="container">  // CSS 클래스 "container"를 가진 div를 생성합니다.
        <section>  // 섹션을 시작합니다.
          <label>React Select</label>  // 라벨을 추가합니다.
          <Controller  // Controller를 사용하여 ReactSelect 컴포넌트를 제어합니다.
            name="ReactSelect"  // 폼 데이터의 이름을 설정합니다.
            control={control}  // useForm에서 가져온 컨트롤 객체를 전달합니다.
            render={({ field }) => (  // render prop을 통해 field 객체를 받아 ReactSelect 컴포넌트를 렌더링합니다.
              <ReactSelect
                isClearable
                {...field}
                options={[
                  { value: "chocolate", label: "Chocolate" },
                  { value: "strawberry", label: "Strawberry" },
                  { value: "vanilla", label: "Vanilla" }
                ]}
              />
            )}
          />
        </section>

        <section>  // 섹션을 시작합니다.
          <label>React Datepicker</label>  // 라벨을 추가합니다.
          <Controller  // Controller를 사용하여 ReactDatePicker 컴포넌트를 제어합니다.
            control={control}  // useForm에서 가져온 컨트롤 객체를 전달합니다.
            name="ReactDatepicker"  // 폼 데이터의 이름을 설정합니다.
            render={({ field }) => (  // render prop을 통해 field 객체를 받아 ReactDatePicker 컴포넌트를 렌더링합니다.
              <ReactDatePicker
                className="input"  // CSS 클래스 "input"을 추가합니다.
                placeholderText="Select date"  // 플레이스홀더 텍스트를 설정합니다.
                onChange={(e) => field.onChange(e)}  // onChange 이벤트를 설정하고 field.onChange 함수를 호출합니다.
                selected={field.value}  // 선택된 날짜 값을 설정합니다.
              />
            )}
          />
        </section>

        <section>  // 섹션을 시작합니다.
          <label>NumberFormat</label>  // 라벨을 추가합니다.
          <Controller  // Controller를 사용하여 NumberFormat 컴포넌트를 제어합니다.
            render={({ field }) => (  // render prop을 통해 field 객체를 받아 NumberFormat 컴포넌트를 렌더링합니다.
              <NumberFormat thousandSeparator {...field} />  // NumberFormat 컴포넌트에 필드 속성을 전달합니다.
            )}
            name="numberFormat"  // 폼 데이터의 이름을 설정합니다.
            className="input"  // CSS 클래스 "input"을 추가합니다.
            control={control}  // useForm에서 가져온 컨트롤 객체를 전달합니다.
          />
        </section>

        <section>  // 섹션을 시작합니다.
          <Controller  // Controller를 사용하여 DownShift 컴포넌트를 제어합니다.
            render={({ field: { ref, ...rest } }) => <DownShift {...rest} />}  // render prop을 통해 필드 객체에서 ref를 제외한 나머지 속성을 전달하여 DownShift 컴포넌트를 렌더링합니다.
            control={control}  // useForm에서 가져온 컨트롤 객체를 전달합니다.
            name="downShift"  // 폼 데이터의 이름을 설정합니다.
          />
        </section>

        <section>  // 섹션을 시작합니다.
          <label>DraftJS</label>  // 라벨을 추가합니다.
          <DraftExample control={control} />  // DraftExample 컴포넌트를 렌더링하고, control prop으로 useForm에서 가져온 컨트롤 객체를 전달합니다.
        </section>

        <section>  // 섹션을 시작합니다.
          <label>React Input Mask</label>  // 라벨을 추가합니다.
          <Controller  // Controller를 사용하여 React Input Mask 컴포넌트를 제어합니다.
            name="reactMaskInput"  // 폼 데이터의 이름을 설정합니다.
            control={control}  // useForm에서 가져온 컨트롤 객체를 전달합니다.
            render={({ field: { onChange, value } }) => (  // render prop을 통해 필드 객체에서 onChange와 value를 받아 React Input Mask 컴포넌트를 렌더링합니다.
              <InputMask mask="99/99/9999" value={value} onChange={onChange}>  // InputMask 컴포넌트에 마스크와 값을 설정하고 onChange 이벤트를 처리합니다.
                {(inputProps) => (  // 랜더링 함수를 통해 input 요소에 inputProps를 전달합니다.
                  <input
                    {...inputProps}
                    type="tel"  // 전화번호 타입의 입력 필드로 설정합니다.
                    className="input"  // CSS 클래스 "input"을 추가합니다.
                    disableUnderline  // 밑줄을 비활성화합니다.
                  />
                )}
              </InputMask>
            )}
          />
        </section>
      </div>

      <hr />  // 수평선을 추가합니다.

      <Chakra control={control} />  // Chakra 컴포넌트를 렌더링하고, control prop으로 useForm에서 가져온 컨트롤 객체를 전달합니다.

      <ButtonsResult {...{ data, reset, setValue }} />  // ButtonsResult 컴포넌트를 렌더링하고, data, reset, setValue를 props로 전달합니다.
    </form>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);  // App 컴포넌트를 root 요소에 렌더링합니다.

위 코드에서 renderCount 값은 UI Interaction이 있어도 증가하지 않는다. 다만 상태는 존재하고, submit을 하는 순간 렌더링이 발생하게 된다. react hook form은 각 입력 필드에 대해 값의 변화를 추적하지 않도록 ref를 통해 최적화되어 있기에 위와 같은 현상이 발생하는 것이다.

처음에 해당 section의 제목이 잘 와닿지 않았다. Integrating Controlled Inputs라는 제목은 결국, 입력 폼에서 렌더링을 최적화함과 동시에 폼에서 입력받은 값을 state로 엮기 위해서 어떠한 방식을 따라야 하는지를 설명하는 제목이었던 것이다.

"그런데 내가 생각한 게 진짜 맞아?", "react hook form에서 정말 이런 이유로 본인들의 서비스를 만든 게 맞아?"라는 생각이 들었다.

간판도 안 보고 식당에 들어갔던 거시다~~ 어쨌든, 정확히 이해했으니 다행이다.

✅ 회고

공식 문서가 읽기 어려운 이유는 많은 개념이 '추상화'되어 있기 때문이다. 가장 핵심적인 컨셉, 즉 특정한 기술이 만들어진 이유를 읽어보고, 그 이유에서 '추상화된 개념'(aka. 키워드)을 탑다운 방식으로 파고들다 보면 이해가 쌓이는 것 같다.

개발을 잘 하려면 공식 문서를 많이 읽어야 된다고들 하는데, 가장 중요한 것은 거부감과 부담감을 내려놓고 작은 단서부터 시작하는 태도인 것 같다. 천천히 접근하면 오히려 빨라진다.

profile
Write a little every day, without hope, without despair ✍️

0개의 댓글