React-Hook-Form 라이브러리를 직접 사용해보고 공통 컴포넌트화 시키면서 알게 된 정보를 정리하려고 한다.
라이브러리의 공식문서에 장점이 많이 나열되어 있다.
1. 유효성 검사 코드를 간편하게 작성할 수 있다.
1. 특히 유효성에 맞지 경우 error를 바로 확인할 수 있다.
2. state를 사용하지 않기 때문에 불필요한 리렌더링을 줄일 수도 있다.
'와~~~ 좋은 라이브러리에요' 가 아닌 이 라이브러리를 사용해서 UI 컴포넌트에 적용시키는 방법을 집중적으로 이야기하려고 한다.
💡typescript와 함께 설명이 되어 있습니다.
react-hook-form 라이브러리는 useForm
훅을 사용하는 것이 가장 쉬운 방식이다. useForm
훅이 리턴해주는 register
메서드를 input 엘리멘트의 prop로 내려주면 된다.
import { useForm } from 'react-hook-form';
function SampleForm() {
const { register } = useForm();
return <input {...register('sample', { required: true })} />;
}
export default SampleForm;
register : (name: string, RegisterOptions?) => ({ onChange, onBlur, name, ref })
name
: 위의 코드에서는 sample
고유한 이름을 등록했지만 name.firstName.0
로 등록하여 {name: { firstName: [ 'value' ] }}
처럼 다수의 입력을 관리할 수도 있다.
RegisterOptions
으로 간단하게 유효성 검사를 할 수 있다.
자세한 내용은 공식문서를 참고하길 바란다.
공식문서에는 React-Select, AntD and MUI로 설명이 되어 있지만 React-bootstrap으로 설명하겠다.
import { Form } from 'react-bootstrap';
import { useForm } from 'react-hook-form';
function SampleForm() {
const { register } = useForm();
return <Form.Control {...register('sample')} />
}
export default SampleForm;
너무 간단하게 사용할 수 있다. register
메서드의 리턴값을 풀어서 props로 넘겨주는 방식이다.
이것만으로는 입력값을 관리할 수는 없다.
import { Form } from 'react-bootstrap';
import { SubmitHandler, useForm } from 'react-hook-form';
type Inputs = {
sample: string;
};
function SampleForm() {
const {
register,
handleSubmit,
} = useForm<Inputs>();
const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);
const onError: SubmitErrorHandler<Inputs> = (err) => console.log(err)
return (
<form onSubmit={handleSubmit(onSubmit, onError)}>
<Form.Control {...register('sample', { required: true })} />
<button type="submit">제출</button>
</form>
);
}
export default SampleForm;
갑자기 코드가 많이 늘었다. 겁먹지 말자.
form 엘리멘트의 onSubmit
어트리뷰트를 보자. useForm
훅이 리턴해주는 handleSubmit
메서드를 실행시켰다.
handleSubmit
메서드의 타입을 보자. (공식문서)
((data: Object, e?: Event) => Promise<void>, (errors: Object, e?: Event) => void) => Promise<void>
위 코드에서는 첫번째 인자로 onSubmit
함수를 넣었다. 두번째 인자는 옵션이지만 onError
함수를 넣었다.
sample
의 값이 유효성을 통과하면 onSubmit
함수를 실행되고 통과하지 못하면 onError
함수가 실행된다.
이제 제출
버튼을 누르면 값에 따라서 아래와 같은 오브젝트를 보여준다.
// onSubmit의 data (유효성을 통과하면)
{ sample: '입력값' }
// onError의 err (유효성을 통과하지 못하면)
{
sample : {
message: "",
ref: input.form-control,
type: "required"
}
}
그럼 에러가 발생했을 때 onError
함수의 err
를 state로 관리해서 사용하면 되는가?
아니다.
코드 중간 +
로 추가한 코드를 표현했다.
import { Form } from 'react-bootstrap';
import { SubmitHandler, useForm } from 'react-hook-form';
type Inputs = {
sample: string;
};
function SampleForm() {
const {
register,
handleSubmit,
+ formState: { errors }
} = useForm<Inputs>();
const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);
const onError: SubmitErrorHandler<Inputs> = (err) => console.log(err)
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Form.Control {...register('sample', { required: true })} />
+ {errors.sample && <span>{errors.sample.message}</span>}
<button type="submit">제출</button>
</form>
);
}
export default SampleForm;
useForm
이 리턴해주는 formState
오브젝트의 값 중 errors
를 사용하여 에러를 표시할 수 있다.
erros
오브젝트는 다음과 같이 발생한다.
{
sample : {
message: "",
ref: input.form-control,
type: "required"
}
}
어디서 많이 본 구조 아닌가? 맞다. onError
함수에서 콘솔에 찍힌 내용과 같다. 우리가 상태로 관리하는 것이 아닌 useForm
훅에서 리턴해주는 formState
오브젝트 안에 모두 담겨있다.
지금이야 입력이 sample
하나만 있지만 다수의 입력이 있다면 통과하지 못한 입력의 name만 나오게 되어 있다.
에러는 언제 뜨게 되는가?
제출 버튼을 누르고 (onSubmit
함수 실행 ) 이후에는 실시간으로 확인할 수 있다. onSubmit
함수 실행 전에는 errors
오브젝트는 빈 오브젝트이다.
크롬의 리액트 개발자 도구를 이용해서 확인해보자. 비교군을 두기 위해 왼쪽이 일반적인 state로 관리하는 폼이고 오른쪽이 라이브러리를 사용한 폼이다. (좀 더 UI 답게 만들어보았다)
일반 폼은 값을 입력할 때마다 렌더링이 발생한다. 하지만 React Hook Form
은 onSubmit
을 할 때만 발생한다.
리액트의 렌더링은 언제 발생하는가? state가 변경이 되면! 으로 쉽게 대답할 수 있다.
리액트에서 렌더링이 발생하지 않고 값만 변경하는 방법은 무엇이 있는가? useRef가 반환해주는 변경 가능한 ref 객체를 사용!
그렇다면 일반 폼이 렌더링 되는 이유는? state가 변경이 되어서!
그렇다면 React Hook Form
은 왜 렌더링이 되지 않는가? useRef가 반환해주는 변경 가능한 ref 객체를 사용해서?
맞다. 왠지 감이 잡히지만 실제 코드를 보면 더 확실해진다.
아래 코드는 실제 코드와 엄청 다르다. 이해를 위해 간단하게 정리한 것이다.
// src/useForm.ts
// TFieldValues은 예시 코드의 Inputs를 제네릭으로 받은 것이다.
export function useForm<TFieldValues>(props) {
const _formControl = useRef<UseFormReturn<TFieldValues> | undefined>();
const [formState, updateFormState] = useState<TFieldValues>({
errors : props.errors || {}
})
_formControl.current.formState = getProxyFormState(formState, control);
return _formControl.current;
}
Custom Hook은 무언가를 return 하게 되어 있다. 마지막에 무엇을 리턴하였는가?
_formControl.current
를 리턴하였다. _formControl
은 useRef가 리턴하는 ref 객체이다.
그리고 errors
오브젝트는 formState
이라는 state 변수의 값이다.
위의 코드에서는 생략되었지만 입력의 값이 변경될 때는 state를 건드리지 않고 _formControl.current
만 변경된다. 폼의 상태가 변경될 때 (updateFormState
함수가 호출될 때를 찾아보면 된다) 리렌더링이 발생하게 된다.
즉, 우리가 state를 통해서 값을 변경하던 것을 React Hook Form
이 알아서 ref를 사용하게 해주는 것이다.
React Hook Form
이 동작하는 방식은 정확하게 이해하지는 못했지만 적어도 왜 그렇게 하는지는 알게 되었다.
다음에는 register
메서드가 아닌 control
메서드와 Controller
컴포넌트를 사용해서 좀 더 세부적으로 값을 컨트롤 하는 방식을 알아보자.