react-hook-form & Input with forwardRef

joooonis·2023년 8월 12일
2
post-thumbnail

Performant, flexible and extensible forms with easy-to-use validation.

react-hook-form은 React 애플리케이션에서 폼을 쉽게 관리하기 위한 라이브러리 중 하나입니다. 이 라이브러리는 더 나은 성능과 사용자 경험을 제공하며, React 컴포넌트의 상태 및 라이프사이클을 활용하여 폼 상태를 관리합니다.

사용법을 먼저 간단하게 소개합니다.

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

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

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

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("좋아요")} />
      <input {...register("싫어요")} />
      <button type="submit">Submit</button>
    </form>
  );
}

위처럼 inputregister 를 다음과 같이 넘기면 {...register("좋아요")} 해당 key로 등록이 되고 이후 폼 제출시 확인이 가능합니다.

그런데 HTML input 태그를 바로 사용하는 것이 아니라 개발자가 Input 컴포넌트를 따로 만들어서 register를 하려고 하면 어떻게 될까요? label을 함께 보여주는 MyInput 컴포넌트를 만들고 똑같이 register 함수를 사용해봅니다.

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

interface MyInputProps extends React.HTMLProps<HTMLInputElement> {
  label: string;
}

const MyInput = ({ label, ...props }: MyInputProps) => {
  return (
    <label className='flex'>
      <span className='mr-2 text-2xl'> {label}</span>
      <input {...props} />
    </label>
  );
};

export default function Home() {
  const { register, handleSubmit } = useForm();

  function onSubmit(data: any) {
    console.log(data);
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <MyInput label='😀' {...register('좋아요')} />
      <button type="submit">Submit</button>
    </form>
  );
}


랜더링시에 에러가 발생하며 동시에 폼 제출시 값도 잘 전달되지 않는 것을 확인할 수 있습니다. 두 가지 해결방법을 알려드리겠습니다.

props로 register 전달.

커스텀 컴포넌트에서 직접 register 함수를 사용하려고 했더니 잘 동작하지 않았습니다. 그렇다면 props로 넘겨서 사용하면 그만입니다.


interface MyInputProps extends React.HTMLProps<HTMLInputElement> {
  label: string;
  name: string;
  register?: any;
}

const MyInput = ({ label, name, register, ...props }: MyInputProps) => {
  return (
    <label className='flex'>
      <span className='mr-2 text-2xl'> {label}</span>
      <input {...register(name)} {...props} />
    </label>
  );
};

export default function Home() {
  const { register, handleSubmit } = useForm();
  const onSubmit = (data: any) => console.log(data);
  return (
    <main className='flex min-h-screen flex-col items-center justify-between p-24'>
      <form onSubmit={handleSubmit(onSubmit)}>
        <MyInput label='😀' register={register} name='좋아요' />
        <button type='submit'>Submit</button>
      </form>
    </main>
  );
}

랜더링시 에러도 뜨지 않고 폼 제출시 값도 잘 가져옵니다. 하지만 이 방법에는 여러 문제가 있습니다.

먼저 register 함수에 태깅할 nameprops에 반드시 추가되어야 합니다. 그리고 register 함수는 사용시 유효성 검사를 포함한 여러가지 옵션을 추가로 인자로 넘겨줄 수 있습니다. 그렇다면 이를 위해 props를 또 추가해야 할 것입니다.

  <input {...register("firstName", { required: true, maxLength: 20 })} />
  <input {...register("lastName", { pattern: /^[A-Za-z]+$/i })} />
  <input type="number" {...register("age", { min: 18, max: 99 })} />

그리고 registerprops로 넘겨줄 때 interface의 타입 문제입니다. 물론 any를 사용하지 않고 라이브러리에서 제공하는 타입을 지정해줄수 있습니다. 하지만 any를 지우기 위해서 꽤 공을 들여야 하고 결과적으로 기존의 방식대로 register를 사용하지 못하는 큰 단점을 해결할 수 없습니다.

import { useForm, UseFormRegister, FieldValues } from 'react-hook-form';

interface MyInputProps extends React.HTMLProps<HTMLInputElement> {
  label: string;
  name: string;
  register: UseFormRegister<FieldValues>;
}

const MyInput = ({ label, name, register, ...props }: MyInputProps) => {
  return (
    <label className='flex'>
      <span className='mr-2 text-2xl'> {label}</span>
      <input {...register(name)} {...props} />
    </label>
  );
};

export default function Home() {
  const { register, handleSubmit } = useForm();
  const onSubmit = (data: any) => console.log(data);
  return (
    <main className='flex min-h-screen flex-col items-center justify-between p-24'>
      <form onSubmit={handleSubmit(onSubmit)}>
        <MyInput label='😀' register={register} name='좋아요' />
        <button type='submit'>Submit</button>
      </form>
    </main>
  );
}

forwardRef를 사용한 방식.

props 방식으로는 이런식 저런식으로 어떻게든 해결은 할 수 있으나 본 라이브러리를 적절히 사용하는 것으로 보이지는 않습니다. 그렇다면 어떻게 해결할까요?

실은 에러메세지에서 힌트를 찾을 수 있습니다. React.forwardRef()를 사용하려는 건지 물어보고 있습니다.

Exposing a DOM node to the parent component
By default, each component’s DOM nodes are private. However, sometimes it’s useful to expose a DOM node to the parent—for example, to allow focusing it. To opt in, wrap your component definition into forwardRef():

공식문서에서 언제 forwardRef를 사용하는지 잘 설명되어 있습니다. 지금 같은 상황에서 forwardRef는 완벽하게 들어맞습니다.

우리는 register 함수를 <input> DOM 노드에 직접 사용해야 합니다. 이를 위해 forwardRef를 사용 컴포넌트 내부의 <input> DOM 노드를 컴포넌트에서 노출 시켜야 합니다.

import { forwardRef } from 'react';

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

interface MyInputProps extends React.HTMLProps<HTMLInputElement> {
  label: string;
}

const MyInput = forwardRef<HTMLInputElement, MyInputProps>(
  ({ label, ...rest }, ref) => {
    return (
      <label className='flex flex-col'>
        <span>{label}</span>
        <input ref={ref} {...rest} />
      </label>
    );
  }
);

export default function Home() {
  const { register, handleSubmit } = useForm();
  const onSubmit = (data: any) => console.log(data);
  return (
    <main className='flex min-h-screen flex-col items-center justify-between p-24'>
      <form onSubmit={handleSubmit(onSubmit)}>
        <MyInput label='😀' {...register('좋아요')} />
        <button type='submit'>Submit</button>
      </form>
    </main>
  );
}

위와 같이 사용하면 1번 방법에서 겪었던 모든 문제를 해결할 수 있습니다. 이후 유효성검사를 위한 옵션들도 자연스럽게 넘겨줄 수 있습니다. 행복하네요.

profile
FE Developer

1개의 댓글

comment-user-thumbnail
2024년 4월 12일

감사합니다 헤매고 있던걸 해결했습니다

답글 달기