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

원민관·2024년 7월 8일
0

[TIL]

목록 보기
139/159
post-thumbnail

reference
1) https://www.react-hook-form.com/get-started/#Integratinganexistingform
2) https://react.dev/learn/referencing-values-with-refs
3) https://react.dev/reference/react/useRef
4) https://react.dev/reference/react/forwardRef#forwardref
5) https://developer.mozilla.org/en-US/docs/Web/API/clearInterval

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

1. Overview ✍️

대충 어마어마하게 좋은, 사용하기 쉬운 폼을 제공하는, 리액트 폼 관리 라이브러리이다. 새로운 프로젝트를 시작하기에 앞서, sign up 또는 sign in, 즉 form에 관한 validation 처리에 아주 유용한 툴을 제공하는 라이브러리가 있다는 것을 알았다.(공식문서 읽어야 한다는 뜻)

딱 보니까 Get Started랑 API 탭 부분 읽고, 향후에 TS 적용하면서 TS랑 Advanced 읽어보면 되겠다는 느낌이 든다.

Get Started에 있는 메뉴를 읽고 정처기 좀 하면 하루가 마무리되지 않을까 싶다. Get Started!

2. Quick start ✍️

2-1. Installation 🚀

npm install react-hook-form

ㅋㅋ 자 다음!

2-2. Example 🚀

// react-hook-form 라이브러리에서 useForm 훅을 가져온다.
import { useForm } from "react-hook-form"; 

export default function App() {
  // useForm 훅을 사용하여 이것저것 가져온다.
  const { register, handleSubmit, watch, formState: { errors } } = useForm();
  
  // onSubmit 함수는 폼이 제출되었을 때 호출된다. 
  const onSubmit = data => console.log(data);

  // watch 함수는 지정한 폼 필드의 현재 값을 반환한다.
  console.log(watch("example"));

  return (
    // form 요소에 onSubmit 이벤트 핸들러를 설정한다.
    // handleSubmit 함수는 폼 제출 시 입력값을 검증한 후 onSubmit 함수를 호출한다.
    <form onSubmit={handleSubmit(onSubmit)}>
      
      {/* register 함수를 사용하여 "example" 입력 필드를 등록한다.
          defaultValue 속성을 사용하여 기본값을 "test"로 설정한다. */}
      <input defaultValue="test" {...register("example")} />
      
      {/* "exampleRequired" 입력 필드를 등록한다.
          필수 입력 필드로 설정하여 사용자가 값을 입력하지 않으면 오류가 발생한다. */}
      <input {...register("exampleRequired", { required: true })} />
      
      {/* 입력 필드에 오류가 발생한 경우, 오류 메시지를 표시한다.
          errors 객체에서 "exampleRequired" 필드의 오류를 확인한다. */}
      {errors.exampleRequired && <span>This field is required</span>}
      
      {/* 폼 제출 버튼 */}
      <input type="submit" />
    </form>
  );
}

사실 위 예제 코드를 처음 봤을 때, 대부분은 직관적으로 이해할 수 있었다. 그러나 {...register("어쩌구")} 부분 빼고.

register 함수는 입력 필드에 필요한 속성들을 반환할 것이다. API 섹션에서 살펴볼 예정이기 때문에 본 글에서 자세히 다루지는 않을 것이다. 다만 register 함수가, 우리가 평소 인풋에 입력하고 있었던 다양한 속성들을 반환할 것이고, 각 속성을 하나하나 일일이 지정하는 것이 아니라, 스프레드 연산자의 형태로 간단하게 처리하고 있음을 알 수 있다.

추가적으로 errors.exampleRequired로 에러처리를 하는 것을 보아, register 함수의 첫번째 파라미터에 전달한 string 값이 formState와 연결되어 있음을 예상해볼 수 있겠다.

3. Register fields ✍️

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

export default function App() {
  const { register, handleSubmit } = useForm();
  const onSubmit = data => console.log(data);
   
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("firstName")} />
      <select {...register("gender")}>
        <option value="female">female</option>
        <option value="male">male</option>
        <option value="other">other</option>
      </select>
      <input type="submit" />
    </form>
  );
}

결국 위 코드에서 강조하는 바는, 1)form의 각 입력 필드에 register 함수를 적용할 것, 2)각 필드는 고유한 이름을 가질 것(=필드 식별 및 관리에 사용되기 때문에)이다.

한마디로 폼에 name을 넣어주어야, 해당 name을 key로 활용하여, value에 대한 validation과 submission 등의 'registration process'를 진행할 수 있다는 것이다.

4. Apply validation ✍️

import React from "react";
import ReactDOM from "react-dom";
import { useForm } from "react-hook-form";

import "./styles.css";

function App() {
  const {
    register,
    handleSubmit,
    watch,
    formState: { errors }
  } = useForm();

  const onSubmit = (data) => {
    alert(JSON.stringify(data));
  }; // your form submit function which will invoke after successful validation

  console.log(watch("example")); // you can watch individual input by pass the name of the input

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <label>First Name</label>
      <input
        {...register("firstName", {
          required: true,
          maxLength: 20,
          pattern: /^[A-Za-z]+$/i
        })}
      />
      {errors?.firstName?.type === "required" && <p>This field is required</p>}
      {errors?.firstName?.type === "maxLength" && (
        <p>First name cannot exceed 20 characters</p>
      )}
      {errors?.firstName?.type === "pattern" && (
        <p>Alphabetical characters only</p>
      )}
      <label>Laste Name</label>
      <input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} />
      {errors?.lastName?.type === "pattern" && (
        <p>Alphabetical characters only</p>
      )}
      <label>Age</label>a
      <input {...register("age", { min: 18, max: 99 })} />
      {errors.age && (
        <p>You Must be older then 18 and younger then 99 years old</p>
      )}
      <input type="submit" />
    </form>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

form을 만들었으니, 구체적으로 validation 처리를 어떻게 하는지에 대한 주제를 다룬다.

register 함수에서 이름 다음에 전달하는 객체는 type이다. 따라서, 만약 firstName에서 required에 관한 validation을 처리하고 싶다면 errors?.firstName?.type === "required"와 같은 방식으로 처리를 진행하면 된다.

5. Integrating an existing form ✍️

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

// The following component is an example of your existing Input Component
const Input = ({ label, register, required }) => (
  <>
    <label>{label}</label>
    <input {...register(label, { required })} />
  </>
);

// you can use React.forwardRef to pass the ref too
const Select = React.forwardRef(({ onChange, onBlur, name, label }, ref) => (
  <>
    <label>{label}</label>
    <select name={name} ref={ref} onChange={onChange} onBlur={onBlur}>
      <option value="20">20</option>
      <option value="30">30</option>
    </select>
  </>
));

const App = () => {
  const { register, handleSubmit } = useForm();

  const onSubmit = (data) => {
    alert(JSON.stringify(data));
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Input label="First Name" register={register} required />
      <Select label="Age" {...register("Age")} />
      <input type="submit" />
    </form>
  );
};

제목에 집중을 해야 된다. Integrating의 대상은 react-hook-form이다. react-hook-form을 이미 존재하는 form에 통합하는 과정을 다루겠다는 뜻인데, 여기서 existing form은 Input과 Select이다.

그러니까, 만약 내가 Input과 Select라는 개별 컴포넌트를 이미 구현해놓은 상태라면, "여기서 react-hook-form을 어떻게 사용합니까?" 하는 점에 관한 논의라는 것이다.

문제는 forwardRef를 모른다. forwardRef를 모른다는 것은 useRef를 모른다는 것이다. 결국 ref 자체에 대한 공부가 필요하다는 말이고, 한마디로 골치 아파졌다.

5-1. ref 🚀

컴포넌트가 특정한 정보를 기억하게 하고 싶다 + 렌더링에 영향을 주지는 않았으면 좋겠다 => ref 사용
(=> state와의 차이가 중요하겠다고 예상된다. state는 특정한 정보를 기억하는데 사용하지만 렌더링을 발생시키기 때문이다.)

"0을 기억하고 싶어, 근데 렌더링에는 영향을 안 줬으면 좋겠어"가 바로 위 이미지에 대한 설명이다. current라는 key를 담고 있는 obj를 반환한다.

공식문서의 설명이 참 적절하다. 한마디로 컴포넌트의 secret pocket이다. 가계부에는 반영되지 않는 '비상금 지갑' 같은 느낌이다.

import { useState, useRef } from 'react';

export default function Stopwatch() {
  const [startTime, setStartTime] = useState(null); // 시작 시간 상태
  const [now, setNow] = useState(null); // 현재 시간 상태
  const intervalRef = useRef(null); // interval ID를 저장하는 useRef

  // Start 버튼 클릭 시 실행되는 함수
  function handleStart() {
    setStartTime(Date.now()); // 현재 시간을 시작 시간으로 설정
    setNow(Date.now()); // 현재 시간을 현재 시간 상태로 설정

    clearInterval(intervalRef.current); // 기존에 설정된 interval 제거
    intervalRef.current = setInterval(() => {
      setNow(Date.now()); // 10ms 간격으로 현재 시간을 업데이트하는 interval 설정
    }, 10);
  }

  // Stop 버튼 클릭 시 실행되는 함수
  function handleStop() {
    clearInterval(intervalRef.current); // 현재 설정된 interval 제거
  }

  let secondsPassed = 0;
  if (startTime != null && now != null) {
    secondsPassed = (now - startTime) / 1000; // 경과 시간 계산
  }

  return (
    <>
      <h1>Time passed: {secondsPassed.toFixed(3)}</h1> {/* 경과 시간을 출력 */}
      <button onClick={handleStart}>
        Start
      </button>
      <button onClick={handleStop}>
        Stop
      </button>
    </>
  );
}

인터벌을 종료하려면 intervalID를 전달해야 하는데, 이 값은 컴포넌트가 기억은 해야 하지만 렌더링에는 영향을 미치지 않기 때문에 ref로 처리함이 바람직한 것이다.

5-2. forwardRef() 🚀

https://www.youtube.com/watch?v=LtYzjv2yXHE

짱짱맨 강의, 기똥차다.

5-3. 다시 코드 🚀

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

// The following component is an example of your existing Input Component
const Input = ({ label, register, required }) => (
  <>
    <label>{label}</label>
    <input {...register(label, { required })} />
  </>
);

// you can use React.forwardRef to pass the ref too
const Select = React.forwardRef(({ onChange, onBlur, name, label }, ref) => (
  <>
    <label>{label}</label>
    <select name={name} ref={ref} onChange={onChange} onBlur={onBlur}>
      <option value="20">20</option>
      <option value="30">30</option>
    </select>
  </>
));

const App = () => {
  const { register, handleSubmit } = useForm();

  const onSubmit = (data) => {
    alert(JSON.stringify(data));
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <Input label="First Name" register={register} required />
      <Select label="Age" {...register("Age")} />
      <input type="submit" />
    </form>
  );
};

register 함수가 실행되면 객체를 반환하는데, 그 객체에 ref가 포함되어 있던 것이다. forwardRef 키워드를 제거하면 Select의 내용이 반영되지 않는 것을 직접 확인했다.

  1. handleSubmit이 전달받는 data 객체는 register로 등록된 내용임
  2. register는 개별 form에서의 내용을 반영함
  3. select tag(즉 DOM node)를 ref로 연결함으로써, select tag를 Select(부모 컴포넌트)에 노출할 수 있음
  4. 20 또는 30은 렌더링에 영향을 미치지는 않지만 컴포넌트가 기억해야 하는 값임이 분명하다.

✅ 회고

아이들은 '왜?'를 입에 달고 산다. 천재적인 아이라 사물의 본질을 알고 싶어 한 질문일 수도 있지만, 실제로 아이들이 던지는 '왜?'는 그들의 가장 강력한 '가불기' 중 하나다. '왜?'는 딱 한 글자이지만, 이에 대한 설명은 매우 길어질 수밖에 없기 때문에, 어른들의 '자신을 향한 관심'을 쉽게 얻어낼 수 있기 때문이다.

새로운 기술을 학습할 때, 1) 아이처럼 '왜?'를 남발하고, 2) 어른처럼 쩔쩔매며 설명하는 두 과정을, 나 혼자 동시에 겪어내는 프로세스가 필요하다고 생각한다. 좌우지간, '왜?'는 가성비가 좋아서 기술의 본질을 명확히 이해하는 데 큰 도움을 주기 때문이다.

불가피하게 훌륭한 소프트웨어 개발자의 최고 덕목은 '인내심'일 수밖에 없다. 어른은 그럼에도 불구하고 아이를 사랑해 주어야 할 의무가 있기 때문이다.

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

0개의 댓글