- 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 덕분에 리렌더링에 대한 비용이 적다는 주장 입니다. 실제로 관련 실험 영상이 있습니다.
영상에서도 나왔듯, 실제로 비제어 컴포넌트는 성능상의 이점이 있습니다. 하지만, 영상에서도 나왔듯,
useRef
와 관련된 보일러플레이트가 크다는 것이 비제어 컴포넌트의 큰 단점 중 하나 입니다. useRef
와 ref 객체 관리, form 데이터 관리가 state 로 하는 것이 훨씬 편하다는 주장입니다.- 그렇다면 제어 컴포넌트와 비제어 컴포넌트는 각각 언제 어떻게 사용하는 것이 좋을까요?
비제어 컴포넌트는 간단하게 사용자가 직접 입력하는 input 이나 select 에 사용하기 좋습니다.
제어 컴포넌트는 훨씬 더 복잡한 커스텀 컴포넌트가 필요한 경우나, 어떤 값이 일반적인 사용자 입력이 아닌 경우에 사용하기 좋습니다. input 이더라도, 팝업이 뜬 후 선택된 값을 입력하게 되는 경우라던가, chip 리스트 등등이 있겠습니다.
2. react-hook-form 의 역할
- form 관련 보일러플레이트를 줄여주는 역할
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
내부에서 관리해주기 때문이죠.- form 관련 추가적 관심사 분리
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
의 return
값들을 중 짚고 넘어가야할 것들을 간단하게 살펴보겠습니다.register
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 이 변경될 때마다 그 값이 잘 변경되게 됩니다.watch
watch
도 함수입니다. form 내의 어떤 필드 또는 form 전체 필드 값들을 볼 수 있는 함수인데요. 이 함수는 리액트의 리렌더링을 유발합니다. 즉, 이 함수를 통해 form 값을 state 처럼 사용할 수 있습니다. 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
라는 필드를 기준으로 리렌더링을 진행합니다.getValues
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> ) }
control
control
은 객체입니다. useForm
을 통해 등록한 form 관련 정보들을 다른 컴포넌트나 함수에 전달할 때 사용합니다.handleSubmit
handleSubmit
은 form 을 제출할 때 react-hook-form
에서 전처리를 위해 submit
의 기본적인 이벤트를 막고, validation 을 진행합니다. 그 후, 4. useController
API
useController
API 는 useForm
으로 form 을 관리하면서, 제어 컴포넌트와 같이 사용하기 좋은 API 입니다. 실제로 useController
는 useForm
과 엮어 사용합니다. useForm
의 control
객체가 필요합니다.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 /> ); }
useForm
의 control
객체를 넘겨 받아 사용하던 useForm
의 form 관련 정보에 필드를 등록하여 사용할 수 있습니다. Controller
라는 컴포넌트의 내부 훅이지만, Controller
컴포넌트는 제약이 너무 심해서 이 훅을 사용하는 것이 편합니다.연관 API 를 추가적으로 살펴보겠습니다.
useWatch
useWatch
는 watch
의 리액트 성능 개선 버전입니다. useController
와 같이 소개하는 이유는, 사용하는 느낌이 비슷하기 때문입니다. (실제 구현도 useController
내부에서 useWatch
를 사용하기도 하구요.) 복잡한 form 구현 시 사용하기 좋으며, 하나의 form 필드만 리렌더가 되길 원할 때 사용합니다.Controller
useController
의 Wrapper 컴포넌트 입니다. 개인적으로는 MUI
등 외부 디자인 시스템을 그대로 사용하지 않는 이상 이 컴포넌트를 사용하는 것 보다는 useController
를 통해 구체적으로 정의하는 것이 좋다고 생각합니다.5. 그 외 API
useFormContext
useForm
의 form 관련 정보를 control
객체로 넘기다보니, prop drilling 이 생기는 경우가 많아서 지원되는 Context
API 입니다. 실제 코드를 봐도 control
객체를 Context
API 로 감싸서 제공합니다.useFieldArray
form 의 필드가 동적으로 추가, 삭제 될 수 있는 경우에 사용합니다. 추가적인 기능이지만,
7. 결론
react-hook-form
은 리액트에서 form 을 다룰 때 사용하기 좋은 라이브러리 입니다. 리액트에서 사용하는 비제어 컴포넌트에 대한 보일러플레이트를 관리하거나, form validation 에 대한 많은 기능들을 지원합니다. 그리고, 제어 컴포넌트 또한 같이 관리 할 수 있어서 편리합니다. 좀 더 깊게 파면, 리렌더에 관련되어 성능을 깊게 관리 할 수 있게도 지원이 되어있으므로, 간단하게 사용하기도 좋고, 성능 개선을 위해 사용하기에도 좋습니다.