icon

메티의 블로그

react-hook-form 간단 가이드
react-hook-form 간단 가이드

react-hook-form 간단 가이드

Tags
React
HTML
날짜
Apr 14, 2024
상태
공개
 
  • Performant, flexible and extensible forms with easy-to-use validation.
 
react-hook-form docs에 있는 라이브러리 소개 글 입니다. 성능 좋고, 확장성 좋고, form validation 을 사용하기 쉽게 쓸 수 있게 해주는 form 라이브러리라고 하네요. 소개에 맞게 react-hook-form 은 react 에서 form 을 사용할 때 많은 기능을 제공하는 라이브러리 입니다.
제가 일 하는 분야가 ERP 다보니, form 을 통해 화면을 개발 하는 것이 유리할 때가 많습니다. 특히나 react-hook-form 은 성능적으로 유리한 점이 많은 라이브러리입니다.(논란의 여지가 있지만요.) 그리고 form validation 이나 react 에서의 비제어 컴포넌트 기반 form 관리에 유리한 라이브러리 입니다.
 

1. 제어 컴포넌트와 비제어 컴포넌트

react-hook-form 의 주된 목표중 하나는 성능입니다. docs 를 살펴보면, 불필요한 리렌더링을 줄여준다는 말이 꽤나 많이 나옵니다. react-hook-form 은 어떻게 불필요한 리렌더링을 줄여준다는 것 일까요?
 
제어 컴포넌트(controlled component)비제어 컴포넌트(uncontrolled component)는 리액트를 조금 사용해보면 자주 나오는 개념입니다. input 이나 select 같은 html 요소들은 리액트가 아니더라도 사용할 수 있습니다. 즉, 브라우저 내부적으로 input , select 등의 값을 어떤 상태로 이미 관리하고 있다는 것이죠.
제어 컴포넌트는 리액트에서 그 값을 제어한다는 뜻으로 많이 사용하며, form 데이터에 들어갈 만한 input, select 등 태그의 value 값을 리액트의 state 로 관리 합니다. 이는, state 가 변경 시 마다 제어 컴포넌트가 리액트에 의해 리렌더링 된다는 뜻입니다.
비제어 컴포넌트는 html 의 상태를 그대로 사용하는 경우로 많이 사용합니다. 이 상태는 브라우저가 관리하고 있기 때문에, React 에서 또한 이를 사용할 수 있습니다. 이 독자적 상태값을 가져갈 때는 useRef API 와 컴포넌트ref 속성을 통해 비제어 컴포넌트의 현재 상태 값을 읽어올 수 있습니다. (컴포넌트의 ref 속성입니다. JSX 문법이 아닌 일반 html 에서는 input 태그에 ref 속성이 없습니다.) 비제어 컴포넌트는 그 내부 상태값이 변경되더라도 리액트에 의해 리렌더링 되지 않습니다.
 
const ControlledInput = () => { const [inputValue, setInputValue] = useState(''); const handleInputValue = e => { setInputValue(e.target.value); } return ( <input type="text" value={inputValue} onChange={handleInputValue} /> ) }
제어 컴포넌트의 예시 코드
const UncontrolledInput = () => { const inputRef = useRef(null); return { <input ref={inputRef} type="text" /> } } const UncontrolledForm = () => { const handleSubmit = (event) => { event.preventDefault(); console.log(`Input Value: ${inputRef.current.value}`); }; return ( <form onSubmit={handleSubmit}> <UncontrolledInput /> <button>Submit</button> </form> ) };
비제어 컴포넌트의 예시 코드
 
react-hook-form 은 불필요한 리렌더링을 줄여준다고 했습니다. 이는 리액트의 불필요한 리렌더링을 줄여준다는 뜻으로, <ControlledInput /> 컴포넌트의 내부에 inputValue 라는 state 가 값을 가지고 있기 때문에 <ControlledInput />inputValue 값이 변경될 때 마다 새로 렌더링이 됩니다. 하지만, <UncontrolledInput /> 컴포넌트는 input 의 값이 변경 되더라도 리렌더링이 일어나지 않아 성능상 이점이 있습니다.
 
이것은 논란의 여지가 있습니다. 아무리 새로 렌더링이 된다고 하더라도, 리액트의 가상DOM 덕분에 리렌더링에 대한 비용이 적다는 주장 입니다. 실제로 관련 실험 영상이 있습니다.
Video preview
영상 요약: 제어 컴포넌트는 아주 약간의 렌더링 속도 저하가 있지만, 코드 유지 보수 측면에서 보면 비제어 컴포넌트를 사용하는 것이 문제가 많다고 주장하는 영상입니다. 비제어 컴포넌트로 얻는 성능상의 이점이 너무나도 미미하다는 것이죠.
영상에서도 나왔듯, 실제로 비제어 컴포넌트는 성능상의 이점이 있습니다. 하지만, 영상에서도 나왔듯, useRef 와 관련된 보일러플레이트가 크다는 것이 비제어 컴포넌트의 큰 단점 중 하나 입니다. useRef 와 ref 객체 관리, form 데이터 관리가 state 로 하는 것이 훨씬 편하다는 주장입니다.
 
  • 그렇다면 제어 컴포넌트와 비제어 컴포넌트는 각각 언제 어떻게 사용하는 것이 좋을까요?
    • 비제어 컴포넌트는 간단하게 사용자가 직접 입력하는 input 이나 select 에 사용하기 좋습니다.
      제어 컴포넌트는 훨씬 더 복잡한 커스텀 컴포넌트가 필요한 경우나, 어떤 값이 일반적인 사용자 입력이 아닌 경우에 사용하기 좋습니다. input 이더라도, 팝업이 뜬 후 선택된 값을 입력하게 되는 경우라던가, chip 리스트 등등이 있겠습니다.

2. react-hook-form 의 역할

  1. form 관련 보일러플레이트를 줄여주는 역할
    1. react-hook-form 은 비제어 컴포넌트의 보일러 플레이트 코드를 줄여주는 역할도 합니다. 가장 많이 사용되는 react-hook-form 의 API 는 useForm 입니다. 이 훅을 통해서 useRef 관련 여러 보일러 플레이트를 줄여줍니다.
      import { useForm } from "react-hook-form" export default function App() { const { register, handleSubmit } = useForm({ shouldUseNativeValidation: true, }) const onSubmit = async (data) => { console.log(data) } return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register("firstName", { required: "Please enter your first name.", })} // custom message /> <input type="submit" /> </form> ) }
      기본적으로 register 함수가 비제어 form 에 필요한 ref 객체나, validation 체크 관련 props 를 자동으로 제공해줍니다. 복잡한 커스텀 input 컴포넌트가 없는 form 을 만들기에는 useForm 이 아주 좋습니다. form 관련 input 컴포넌트들이 많아지면 ref 객체 관리가 어려워지기 시작하는데, 이를 useForm 내부에서 관리해주기 때문이죠.
 
  1. form 관련 추가적 관심사 분리
    1. form 을 사용하는 여러가지 이유 중 하나는 validation 입니다. 어떤 필드는 필수 값이고, 어떤 필드는 숫자만 들어가야하고, 어떤 필드는 이메일 형식이어야 하고 하는 것들을 react-hook-form 으로 조금 더 간단하게 관리 할 수 있도록 지원합니다. (위의 예시 코드를 보시면, register 함수 인자로 required 가 있는 것을 볼 수 있습니다.) 또한, 특정 필드가 변경되었는지 감지하거나, 특정 필드에 블러가 일어났는지 감지하는 등의 기능도 제공합니다.
 

3. useForm API

useForm API 는 form 관련 모든 것을 하나로 관리하도록 만들어진 훅 입니다. useForm 은 form 관련 많은 기능을 제공합니다. 제출해야할 큰 form 하나를 useForm 하나의 훅으로 전부 관리 할 수 있다고 보면 좋습니다. useForm 은 비제어 컴포넌트에 사용하기 좋은 API 라고 설명하긴 했지만, useForm 은 언제 어디에서나 반드시 사용해야 하는 API 이기는 합니다.
useForm 은 비제어 컴포넌트들을 한번에, 그리고 쉽게 관리 할 수 있습니다. useForm 은 form 관련 값들을 useRef 를 통해 렌더링에 영향을 받지 않는 값으로 가지고 있습니다.
useForm 은 다음과 같이 _formControl 이라는 내부 변수를 useRef 를 통해 리액트의 렌더링에 영향을 받지 않도록 form 값을 관리하고 있습니다. 이후 만들어진 control 변수를 통해 내부적으로 렌더링에 영향 받지 않게 form 값을 변경합니다.
useForm 은 다음과 같이 _formControl 이라는 내부 변수를 useRef 를 통해 리액트의 렌더링에 영향을 받지 않도록 form 값을 관리하고 있습니다. 이후 만들어진 control 변수를 통해 내부적으로 렌더링에 영향 받지 않게 form 값을 변경합니다.
createFormControl 이라는 함수에서 이러한 모든 변수들을 let 변수로 가지고 있습니다.
createFormControl 이라는 함수에서 이러한 모든 변수들을 let 변수로 가지고 있습니다.
 
useFormreturn 값들을 중 짚고 넘어가야할 것들을 간단하게 살펴보겠습니다.
  1. register
    1. register 는 함수입니다. 이 함수는 비제어 컴포넌트를 form 필드로 등록할 때 사용하기 좋은 함수입니다. useForm 을 통해 form 의 필드를 등록할 때 사용합니다. 비제어 컴포넌트에 사용될 ref 와 form 중 하나의 속성 이름인 name 그리고, 값이 변경될 때 사용될 onChange , 포커스를 잃을 때 사용될 onBlur 함수 4개를 return 합니다.
      const { onChange, onBlur, name, ref } = register('firstName'); // include type check against field path with the name you have supplied. <input onChange={onChange} // assign onChange event onBlur={onBlur} // assign onBlur event name={name} // assign name prop ref={ref} // assign ref prop /> // same as above <input {...register('firstName')} />
      register 옵션에 아무것도 두지 않는다면, 위 코드의 firstName 필드는 ref 를 받은 input 이 변경될 때마다 그 값이 잘 변경되게 됩니다.
  1. watch
    1. watch 도 함수입니다. form 내의 어떤 필드 또는 form 전체 필드 값들을 볼 수 있는 함수인데요. 이 함수는 리액트의 리렌더링을 유발합니다. 즉, 이 함수를 통해 form 값을 state 처럼 사용할 수 있습니다. form 값이 변경되면 화면 렌더가 변경되어야하는 경우에 이를 쓰기 좋습니다.
      react-hook-form 에서 제공한 예시 코드 캡쳐
      react-hook-form 에서 제공한 예시 코드 캡쳐
      function App() { const { register, watch, formState: { errors }, handleSubmit, } = useForm<IFormInputs>(); const watchShowAge = watch("showAge", false); // you can supply default value as second argument const watchAllFields = watch(); // when pass nothing as argument, you are watching everything const watchFields = watch(["showAge", "name"]); // you can also target specific fields by their names const onSubmit = (data: IFormInputs) => { console.log("TEST", data); // alert(JSON.stringify(data)); }; console.log("watchAllFields", watchAllFields); console.log("watchFields", watchFields); return ( <> <form onSubmit={handleSubmit(onSubmit)}> <label>Name</label> <input type="text" {...register("name", { required: true, maxLength: 50 })} /> {errors.name && ( <p>{"The Name Field is Required and must be > 49 characters"}</p> )} <label>Show Age</label> <input type="checkbox" {...register("showAge")} /> {watchShowAge && ( <> <label>Age</label> <input type="number" {...register("age", { min: 50 })} /> {errors.age && <p>{"The number must be greater then 49"}</p>} </> )} {/* based on yes selection to display Age */} <input type="submit" /> </form> <div> {watchAllFields.name ? ( <> <label>Watched Fields:</label>name: {watchAllFields.name} </> ) : ( "" )} </div> </> ); }
      예시의 경우 watch 를 통해 showAge 라는 필드를 기준으로 리렌더링을 진행합니다.
  1. getValues
    1. watch 와 비슷하지만, 리렌더링을 유발하지 않습니다. 값을 확인하는 용도로 사용합니다.
      export default function App() { const { register, getValues } = useForm<FormInputs>() return ( <form> <input {...register("test")} /> <input {...register("test1")} /> <button type="button" onClick={() => { const values = getValues() // { test: "test-input", test1: "test1-input" } const singleValue = getValues("test") // "test-input" const multipleValues = getValues(["test", "test1"]) // ["test-input", "test1-input"] }} > Get Values </button> </form> ) }
  1. control
    1. control 은 객체입니다. useForm 을 통해 등록한 form 관련 정보들을 다른 컴포넌트나 함수에 전달할 때 사용합니다.
  1. handleSubmit
    1. handleSubmit 은 form 을 제출할 때 react-hook-form 에서 전처리를 위해 submit 의 기본적인 이벤트를 막고, validation 을 진행합니다. 그 후,
       

4. useController API

useController API 는 useForm 으로 form 을 관리하면서, 제어 컴포넌트와 같이 사용하기 좋은 API 입니다. 실제로 useControlleruseForm 과 엮어 사용합니다. useFormcontrol 객체가 필요합니다.
function Input({ control, name }) { const { field, fieldState: { invalid, isTouched, isDirty }, formState: { touchedFields, dirtyFields } } = useController({ name, control, rules: { required: true }, }); return ( <TextField onChange={field.onChange} // send value to hook form onBlur={field.onBlur} // notify when input is touched/blur value={field.value} // input value name={field.name} // send down the input name inputRef={field.ref} // send input ref, so we can focus on input when error appear /> ); }
useFormcontrol 객체를 넘겨 받아 사용하던 useForm 의 form 관련 정보에 필드를 등록하여 사용할 수 있습니다. Controller 라는 컴포넌트의 내부 훅이지만, Controller 컴포넌트는 제약이 너무 심해서 이 훅을 사용하는 것이 편합니다.
 
연관 API 를 추가적으로 살펴보겠습니다.
  1. useWatch
    1. useWatchwatch 의 리액트 성능 개선 버전입니다. useController 와 같이 소개하는 이유는, 사용하는 느낌이 비슷하기 때문입니다. (실제 구현도 useController 내부에서 useWatch 를 사용하기도 하구요.) 복잡한 form 구현 시 사용하기 좋으며, 하나의 form 필드만 리렌더가 되길 원할 때 사용합니다.
  1. Controller
    1. useController 의 Wrapper 컴포넌트 입니다. 개인적으로는 MUI 등 외부 디자인 시스템을 그대로 사용하지 않는 이상 이 컴포넌트를 사용하는 것 보다는 useController 를 통해 구체적으로 정의하는 것이 좋다고 생각합니다.
       

5. 그 외 API

  1. useFormContext
    1. useForm 의 form 관련 정보를 control 객체로 넘기다보니, prop drilling 이 생기는 경우가 많아서 지원되는 Context API 입니다. 실제 코드를 봐도 control 객체를 Context API 로 감싸서 제공합니다.
  1. useFieldArray
    1. form 의 필드가 동적으로 추가, 삭제 될 수 있는 경우에 사용합니다. 추가적인 기능이지만,
       

7. 결론

react-hook-form 은 리액트에서 form 을 다룰 때 사용하기 좋은 라이브러리 입니다. 리액트에서 사용하는 비제어 컴포넌트에 대한 보일러플레이트를 관리하거나, form validation 에 대한 많은 기능들을 지원합니다. 그리고, 제어 컴포넌트 또한 같이 관리 할 수 있어서 편리합니다. 좀 더 깊게 파면, 리렌더에 관련되어 성능을 깊게 관리 할 수 있게도 지원이 되어있으므로, 간단하게 사용하기도 좋고, 성능 개선을 위해 사용하기에도 좋습니다.