react-hook-form을 알아보자

React-hook-form에 대해서는 input 입력 시 렌더링을 최소화하는 라이브러리 정도로만 알고 있었다. 현 회사에 입사해서 계속 리액트 훅 폼을 사용하고 있는데, 기계적으로 붙여넣기(..)만 하다가 제대로 공부해야겠다는 생각이 들어 작성하는 리액트 훅 폼 정리 글이다.
1. 제어 컴포넌트 vs 비제어 컴포넌트
제어/비제어 컴포넌트는 리액트가 실시간으로 state를 제어할 수 있는지 없는지에 따라 나눠진다. 제어 컴포넌트를 사용하게 되면 과도한 리렌더링이 발생하게 되는데 이러한 특성을 보완하기 위해 비제어 컴포넌트인 리액트 훅 폼 라이브러리를 사용하기 시작했다.
🔗 제어 컴포넌트
- 기존 리액트에서 input창에 입력을 하면 onChange함수에 state 변경 함수를 연결하여 실시간으로 상태 변화가 일어나도록 했다. 이렇게 리액트가 해당 상태를 바로 인식하는 즉, 리액트에 의해 값이 제어되는 입력 폼 엘리먼트를 제어 컴포넌트(controlled componen)라고 한다.
HTML에서 <input>, <textarea>, <select>와 같은 폼 엘리먼트는 일반적으로 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트합니다. React에서는 변경할 수 있는 state가 일반적으로 컴포넌트의 state 속성에 유지되며 setState()에 의해 업데이트됩니다.
우리는 React state를 “신뢰 가능한 단일 출처 (single source of truth)“로 만들어 두 요소를 결합할 수 있습니다. 그러면 폼을 렌더링하는 React 컴포넌트는 폼에 발생하는 사용자 입력값을 제어합니다. 이러한 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 “제어 컴포넌트 (controlled component)“라고 합니다.
- 제어 컴포넌트는 input에 값을 입력할 때마다 setState가 발생하고 상태 변경으로 인한 리렌더링이 발생한다. 이런 제어 컴포넌트는 실시간 유효성검사나 조건부 버튼 활성화 등에 유용하다.
type Info = {
name: string;
age: number;
};
const Form = () => {
// 1. 리액트에서 state를 만들어 input 값에 연결한다
const [formData, setFormData] = useState<Info>({
name: "",
age: 0,
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// 2. input 값이 변경될 때마다 상태변경함수가 작동하여 리액트가 state를 알 수 있다
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
console.log(`Name: ${formData.name}, Age: ${formData.age}`);
};
return (
<div>
<h3>리액트 - 제어컴포넌트</h3>
<form onSubmit={handleSubmit}>
이름 :
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
required
/>
나이 :
<input
type="number"
name="age"
value={formData.age}
onChange={handleChange}
required
/>
<p>
Name: {formData.name}, Age: {formData.age}
</p>
<button type="submit">제출</button>
</form>
</div>
);
};
⛓️💥 비제어 컴포넌트
비제어 컴포넌트(uncontrolled component)는 리액트에 의해 값이 제어되지 않는 컴포넌트를 말한다. state로 값을 관리하는 것이 아니라
ref
를 사용하여 DOM 노드에서 값을 관리하고 가져온다.리액트 훅 폼은 이러한 비제어 컴포넌트를 활용한다.
register
를 사용해 input과 연결하면 입력필드에 ref가 연결되고, 입력 값이 변경되더라도 DOM 상태만 업데이트되므로 불필요한 렌더링을 방지한다. 아래처럼 input 값이 변경되더라도 렌더링되지 않는다.
type Info = {
name: string;
age: number;
};
const ReactHookForm = () => {
// register로 input과 연결한다. 따로 state를 만들지 않아도 된다.
const { register, handleSubmit } = useForm<Info>()
const onSubmit= (v: Info) => {
console.log(v);
};
return (
<div style={{ backgroundColor: "ButtonFace" }}>
<h3>리액트훅폼 - 비제어컴포넌트</h3>
<form onSubmit={handleSubmit(onSubmit)}>
이름 : <input {...register("name", { required: true })} />
나이 : <input {...register("age", { required: true })} />
<button type="submit">
제출
</button>
</form>
</div>
);
};
2. useForm vs Controller
리액트 훅폼은 useForm
을 사용해서 비제어 컴포넌트를 쉽게 구현하고, <Controller>
컴포넌트를 사용하여 커스텀 컴포넌트를 연결하여 사용할 수 있다.
⛓️💥 useForm
useForm의 주요 옵션 및 메서드
register
: 입력 필드를 폼 상태에 등록하는 함수handleSubmit
: 폼 제출 시 데이터를 처리하는 함수formState
: 폼 상태 제공(에러, 유효성, 제출 여부 등)pattern
,minLength
등 유효성 검사defaultValues
: 기본 값 설정reset
: 폼 데이터 초기화setValue
,getValue
: 데이터 설정 및 가져오기
- getValue는 watch처럼 폼의 값을 읽어오지만 리렌더링을 일으키지 않고, 값의 변화를 알지 못한다watch
: 입력 값 실시간 감지
type Info = {
id: string;
pw: string;
email: string;
};
const ReactHookForm = () => {
const { register, handleSubmit, watch, formState: {error} } = useForm<Info>({
defaultValues: { id: "", pw: "", email: "" },
});
const onSubmit = (data: FormValues) => {
// 제출 시 실행할 작업
// onSubmit에서 받는 매개변수는 폼 데이터 객체이다
};
return (
<div style={{ backgroundColor: "ButtonFace" }}>
<h3>리액트훅폼 - 비제어컴포넌트</h3>
<form onSubmit={handleSubmit(onSubmit)}>
아이디 : <input {...register("id", { required: true })} /> // required: true이면 필수항목
{errors.id&& <p>{errors.id.message}</p>}
비밀번호 : <input
{...register("pw", {
required: '비밀번호는 필수항목 입니다', // require의 텍스트는 errors.pw.message가 된다
minLength: {
value: 8,
message: '비밀번호는 최소 8자 이상이어야 합니다.'
}
})}
/>
{errors.pw&& <p>{errors.pw.message}</p>}
이메일 : <input
{...register('email', {
required: '이메일은 필수 항목입니다.',
pattern: {
value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '올바른 이메일 형식을 입력해주세요.'
}
})}
/>
{errors.email&& <p>{errors.email.message}</p>}
<button
style={{
backgroundColor:
watch("name") !== "" && watch("age") !== "" ? "green" : "gray",
}}
type="submit"
>
제출
</button>
</form>
</div>
);
};
- watch 또는 useWatch를 사용하여 실시간으로 값을 감지한다는 것은 값의 변경에 의한 리렌더링이 일어난다는 의미이다. watch를 사용하여 제어컴포넌트의 기능을 수행할 수 있다. 사실 이렇게 리렌더링이 계속 일어나면 리액트 훅 폼을 사용하는 큰 의미가 없기 때문에 적절한 상황에 알맞게 사용해야한다.
그리고 watch에 대한 새로운 발견!
watch("name") !== "" && watch("age") !== "" ? "green" : "gray"
이렇게 조건부 렌더링에 watch를 넣어주면 첫번째 watch의 값이 변경되어야만 렌더링 된다. 그러니까 나이 input 창의 값이 먼저 변경될 때는 리렌더링 되지 않는다!
🔗 Controller
<Controller>는 커스텀 컴포넌트를 연결할 수 있어서 MUI, react-native 등 외부 라이브러리와 함께 사용할 때 유용하다. 그런데 이 컨트롤러는 제어 컴포넌트이기때문에 입력할 때마다 input 컴포넌트가 렌더링된다.
Controller의 주요 속성
name
: 필드의 이름. useForm의 defaultValues의 값과 일치하게 전달된다.control
: useForm에서 반환하는 control을 전달하는데 Controller와 리액트 훅 폼을 연결한다.rules
: 필드의 유효성 검사render
: 입력 필드를 렌더링하는 함수defaultValues
: 필드의 기본 값
import { useForm, Controller } from "react-hook-form";
const ControllerCompo = () => {
const { control, handleSubmit } = useForm({
defaultValues: { name: "", age: "" },
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<h3>CONTROLLER 사용</h3>
<Controller
name="name"
control={control}
render={({ field }) => <input {...field} placeholder="이름 입력" />}
/>
<Controller
name="age"
control={control}
render={({ field }) => <input {...field} placeholder="나이 입력" />}
/>
<button type="submit">제출</button>
</form>
);
};
export default ControllerCompo;
Subscribe to my newsletter
Read articles from 화영 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
