๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
React

react-hook-form + yup + forwardRef()๋ฅผ ์ด์šฉํ•œ input ์ปดํฌ๋„ŒํŠธ ์ œ์ž‘ํ•˜๊ธฐ, ์˜ค๋ฅ˜ํ•ด๊ฒฐ

by LasBe 2024. 1. 23.
๋ฐ˜์‘ํ˜•

๐Ÿ“’ react-hook-form๋ฅผ ์ด์šฉํ•œ Input ์ปดํฌ๋„ŒํŠธ


npm i react-hook-form @hookform/resolvers yup

react-hook-form์€ ์†์‰ฝ๊ฒŒ form์„ ์กฐ์ž‘ํ•˜๊ณ  ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ํŒจํ‚ค์ง€์ž…๋‹ˆ๋‹ค.

 

๊ฑฐ๊ธฐ์— @hookform/resolvers์™€ yup์„ ๋”ํ•˜๋ฉด ์†์‰ฝ๊ฒŒ validation ์ฒ˜๋ฆฌ์™€ ์—๋Ÿฌ๋ฉ”์„ธ์ง€๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

ํšŒ์‚ฌ ๋™๋ฃŒ๋“ค์ด ์ด ์กฐํ•ฉ์œผ๋กœ ์•ผ๋ฌด์ง€๊ฒŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๋ณด๊ณ ์„  ์ €๋„ ์‚ฌ์šฉํ•  ๋•Œ๋งŒ ๊ธฐ๋‹ค๋ฆฌ๋‹ค, ๋งˆ์นจ ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•˜๋Š” ๊น€์— ๋ฐ”๋กœ ๋„์ž…ํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๊ธฐํš์„ ๋Œ€์ถฉ ๋ณด๋‹ˆ input์— ์ค‘๋ณต๋˜๋Š” ์—ฌ๋Ÿฌ ๊ธฐ๋Šฅ์ด ์žˆ์–ด์„œ ๊ทธ๋ƒฅ ํ•˜๋‚˜๋กœ ๊ธฐ๋Šฅ๋“ค์„ ๋ฌถ์œผ๋ฉด ์ข‹๊ฒ ๋‹ค ์‹ถ์–ด ๋‹ค์Œ๊ณผ ๋น„์Šทํ•œ ํ˜•ํƒœ๋กœ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ดค์Šต๋‹ˆ๋‹ค.

 

import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useForm } from "react-hook-form";

export default function App() {
  const {
    register,
    handleSubmit,
    formState: { errors }
  } = useForm({
    resolver: yupResolver(
      yup.object().shape({ id: yup.string().required("์•„์ด๋””๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”") })
    )
  });

  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <InputComponent
        placeholder="์•„์ด๋””๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
        error={errors.id?.message}
        {...register("id")}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

type InputType = {
  error?: string;
} & React.InputHTMLAttributes<HTMLInputElement>;

const InputComponent = ({ error, ...rest }: InputType) => {
  return (
    <div style={{ color: "red" }}>
      <div>
        <input {...rest} />
      </div>
      {error && error}
    </div>
  );
};

 

์œ„๋Š” ์ œ๊ฐ€ ๊ธฐ๋Šฅ์ ์œผ๋กœ๋งŒ ๋น„์Šทํ•˜๊ฒŒ ๋งŒ๋“ค์–ด๋ณธ TextInput ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.

 

react-hook-form์˜ register๋ฅผ ์ด์šฉํ•ด {...register("id")} ์™€ ๊ฐ™์ด ์Šคํ”„๋ ˆ๋“œ ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์ž์ฒด์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ์†์„ฑ์„ ์ „๋ถ€ ๋„˜๊ฒจ์ค„ ์ˆ˜ ์žˆ์–ด ์ž˜ ์ž‘๋™ํ•˜๊ฒ ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

 

๊ทธ๋Ÿฐ๋ฐ ์•„๋ฌด๋ฆฌ input์— ๊ฐ’์„ ๋„ฃ๊ณ  submit์„ ํ•ด๋ด๋„ ๊ฐ’์ด ์—†๋‹ค๊ณ  validation์„ ํ•ด๋Œ€์„œ console ์ฐฝ์„ ๋ณด๋‹ˆ ๋‹ค์Œ๊ณผ ๊ฐ™์€ warning์ด ์ˆ˜๋‘๋ฃฉํ•˜๊ฒŒ ์ฐํ˜€์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?


๐Ÿ“Œ ๋น„์ œ์–ด ์ปดํฌ๋„ŒํŠธ

react-hook-form์— ๋Œ€ํ•œ ๊ฐœ๋…์€ ์ด๋ฏธ ์•Œ๊ณ  ์žˆ์—ˆ์ง€๋งŒ ์ฒ˜์Œ ์‚ฌ์šฉํ•ด ๋ด์„œ ๋ฐœ์ƒํ•œ ์—๋Ÿฌ์— ์–ผํƒ€๊ณ  ์žˆ๋˜ ์ค‘ ๋น„์ œ์–ด ์ปดํฌ๋„ŒํŠธ๋ผ๊ณ  ํ–ˆ๋˜ ๊ฒƒ์ด ๋– ์˜ฌ๋ž์Šต๋‹ˆ๋‹ค.

 

์šฐ๋ฆฌ๊ฐ€ ํ‰์†Œ์— ์ƒํƒœ์™€ onChange๋ฅผ ์ด์šฉํ•ด ์ž…๋ ฅ๋งˆ๋‹ค re-render๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒŒ ์ œ์–ด ์ปดํฌ๋„ŒํŠธ,

ref๋ฅผ ์ด์šฉํ•ด DOM์— ์ง์ ‘ ์ ‘๊ทผํ•œ ํ›„ ๊ฐ’์„ ์กฐ์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— re-render๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š” ๊ฒƒ์ด ๋น„์ œ์–ด ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.

 

react-hook-form์€ ์ž์ฒด์ ์œผ๋กœ ref๋ฅผ ์ด์šฉํ•œ ๋น„์ œ์–ด ์ปดํฌ๋„ŒํŠธ๊ธฐ ๋•Œ๋ฌธ์— ์ž์ฒด์ ์œผ๋กœ input์„ ์ œ์–ดํ•˜๋Š” register("id") ๊ฐ์ฒด ๋‚ด๋ถ€์— ref๊ฐ€ ์žˆ์„ ํ™•๋ฅ ์ด 100%๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ํƒ€์ž…์„ ๋”ฐ๋ผ๊ฐ€ ๋ณด๋‹ˆ ์—ญ์‹œ๋‚˜ input ์†์„ฑ๊ณผ ํ•จ๊ป˜ ref๊ฐ€ ์กด์žฌํ–ˆ์Šต๋‹ˆ๋‹ค.


๐Ÿ“Œ forwardRef()

ํ•จ์ˆ˜ํ˜• ์ปดํฌ๋„ŒํŠธ์—์„œ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ref๋ฅผ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์—๊ฒŒ props๋กœ ๋„˜๊ฒจ์ค„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ forwardRef ํ•จ์ˆ˜๋ฅผ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์— ๊ฐ์‹ธ์ฃผ๋ฉด ref๋ฅผ ์ „๋‹ฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์ž์‹ ์ปดํฌ๋„ŒํŠธ์˜ DOM ์š”์†Œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

export const App = () => {
  const ref = useRef<HTMLInputElement>(null);
  return (
    <>
      <Children ref={ref} />
      <button onClick={() => console.log(ref)}>Click</button>
    </>
  );
};

const Children = forwardRef<HTMLInputElement>((_, ref) => {
  return <input ref={ref} />;
});

์ž์‹ ์ปดํฌ๋„ŒํŠธ์— forwardRef ๊ฐ์•„์ฃผ๊ณ  (props, ref) ์ˆœ์„œ๋กœ ๋“ค์–ด์˜ค๋Š” ref๋ฅผ ์›ํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์— ๋„ฃ์–ด์ฃผ๋ฉด ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋งˆ์Œ๊ป ์ œ์–ด๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.


๐Ÿ“Œ react-hook-form + forwardRef ํ•ฉ์ฒด

๊ทธ๋Ÿผ TextInput์—์„œ forwardRef๋ฅผ ๊ฐ์•„์ฃผ๋ฉด react-hook-form์ด ref๋ฅผ ์ด์šฉํ•ด ๋งˆ์Œ๊ป ์กฐ์ž‘ํ•˜๊ฒ ์ฃ ?

 

๊ทผ๋ฐ ๋‚˜๋Š” ref๋„ ๋„ฃ์–ด์ฃผ๊ณ  ์‹ถ๊ณ , HTMLInputElement๋„ ๋„ฃ์–ด์ฃผ๊ณ  ์‹ถ์€๋ฐ ์ด๊ฑธ ์Šคํ”„๋ ˆ๋“œ ์—ฐ์‚ฐ์ž๋กœ ํ•œ ๋ฒˆ์— ๋ฟŒ๋ฆฌ๊ธฐ๋Š” ์• ๋งคํ•ด register ๊ฐ์ฒด๋ฅผ ๊ทธ๋ƒฅ props๋กœ ๋ฐ›์•„ ๋”ฐ๋กœ๋”ฐ๋กœ ๋ฟŒ๋ ค์ฃผ๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

export default function App() {
  // ...
  return (
    <form onSubmit={handleSubmit((data) => console.log(data))}>
      <InputComponent
        placeholder="์•„์ด๋””๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
        error={errors.id?.message}
        register={register("id")}
      />
      <button type="submit">Submit</button>
    </form>
  );
}

type InputType = {
  error?: string;
  register?: UseFormRegisterReturn;
} & React.InputHTMLAttributes<HTMLInputElement>;

const InputComponent = ({ error, register, ...rest }: InputType) => {
  return (
    <div style={{ color: "red" }}>
      <div>
        <input {...rest} {...register} />
      </div>
      {error && error}
    </div>
  );
};

์•„๊นŒ ์œ„์—์„œ ํ™•์ธํ–ˆ๋˜ UseFormRegisterReturn ํƒ€์ž…์˜ ๊ฐ์ฒด๋ฅผ prop์œผ๋กœ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค.

 

prop์ด ์ค‘๋ณต๋  ๋•Œ ๋‚˜์ค‘์— ๋ฟŒ๋ฆฐ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ฎ์–ด์“ฐ๋‹ˆ๊นŒ …rest๋ฅผ ๋จผ์ €, ์ค‘์š”ํ•œ ...register๋ฅผ ๋‚˜์ค‘์— ๋ฟŒ๋ ค์ค๋‹ˆ๋‹ค.

 

์ง์ ‘ ํ™•์ธํ•ด ๋ณด๋ฉด ref๊ฐ€ ์ œ๋Œ€๋กœ ์ ์šฉ๋˜์–ด ์ž‘๋™ํ•˜๋Š” ๋ชจ์Šต์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ˜์‘ํ˜•

๋Œ“๊ธ€


์˜คํ”ˆ ์ฑ„ํŒ