22.3.7 (월) +43days
react-hook-form 은 복잡한 폼을 다룰 때 도움을 주는 라이브러리 입니다.
특징
- 비제어 컴포넌트(Uncontrolled component) 를 사용해 불필요한 렌더링을 줄여 퍼포먼스를 향상시킵니다. (Uncontrolled Component vs Controlled Component)
- 타입스크립트를 지원합니다.
- React hook API 를 지원합니다.
Uncontrolled Component vs Controlled Component
react-hook-form 을 다루다보면 제어 컴포넌트(Controlled Component) 와 비제어 컴포넌트(Uncontrolled Component)에 관한 내용이 빈번하게 소개됩니다.
react 공식 문서에서도 이를 다루고 있으나 좀 더 정확하게 이 둘의 차이를 이해하는 것이 react-hook-form 의 이해도를 높일 수 있다고 생각이 들어서 따로 소개하려고 합니다.
제어 컴포넌트 (Controlled Component)
제어된 입력은 현재값을 prop, state 로 받고 해당 값을 변경하기 위한 콜백을 받습니다.
이는 좀 더 리액트스러운 방식이라고 말할 수 있습니다.
<input value={someValue} onChange={handleChange} />
일반적으로 제어 컴포넌트는 입력을 렌더링하는 컴포넌트에서 해당 상태를 저장합니다.
물론 컴포넌트의 상태일 수 도 있고 Redux 와 같은 별도의 상태 저장소에 있을 수도 있습니다.
아래의 예시에서 새 문자를 입력할 때마다 handleNameChange 가 호출되어 입력의 새 값을 상태로 저장합니다.
때문에 입력값을 변경할 때마다 리렌더링이 일어나게 됩니다.
이러한 특징 때문에 값을 명시적으로 요청할 필요가 없이 입력 상태값이 항상 최신의 값을 갖게 됩니다.
즉, 데이터(상태)와 UI (입력) 가 항상 동기화 됩니다.
import React, { FormEvent, useState } from "react";
function App() {
const [value, setValue] = useState<string>('');
const [result, setResult] = useState<string>('');
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
}
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setResult(value);
}
return (
<form onSubmit={handleSubmit}>
<input type="text" value={value} onChange={handleNameChange} />
<input type="submit" />
<p>{result}</p>
</form>
);
}
export default App;
비제어 컴포넌트 (Uncontrolled Component)
비제어된 입력은 기존 HTML 폼 인풋과 동일합니다.
const Form = () => {
return (
<div>
<input type="text" />
</div>
)
}
사용자가 입력한 것을 기억하고, 아래의 예시와 같이 ref 를 사용하여 값을 얻을 수 있습니다.
import React, { FormEvent, useRef, useState } from "react";
function App() {
const inputRef = useRef<HTMLInputElement>(null);
const [result, setResult] = useState('');
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
inputRef.current && setResult(inputRef.current.value);
}
return (
<form onSubmit={handleSubmit}>
<input type="text" ref={inputRef} />
<input type="submit" />
<p>{result}</p>
</form>
);
}
export default App;
즉, 필요할 때 필드에서 값을 가져와야 합니다.
일반적으로 리액트에서는 **제어 컴포넌트 (Controlled Component)**로 입력을 제어하는 것을 권하지만 react-hook-form 은 비제어 컴포넌트(Uncontrolled Component)가 불필요한 렌더링을 하지 않기 때문에 성능상 우위를 점한다는 점에서 이 방법을 사용합니다.
1. 필드 등록
react-hook-form 의 핵심 개념 중 하나는 비제어 컴포넌트 (Uncontrolled Component) 를 React hook API 에 등록하는 것 입니다.
필드를 등록하면 유효성 검사와 제출(submit) 모두에 해당 필드의 값을 사용할 수 있습니다.
useForm()
: useForm() 훅은 폼을 쉽게 관리하기 위한 커스텀 훅 입니다.
- mode: ( onChange | onBlur | onSubmit | onTouched | all = 'onSubmit'
- 이 옵션을 사용하면 어떠한 이벤트가 발생할 때 유효성 검사를 할 것인지 설정합니다.
- reValidateMode : (onChange | onBlur | onSubmit = 'onChange')
- 기본적으로 유효성 검사는 입력 중에만 발생하는데, 이 옵션을 사용하면 제출(submit) 후 에러가 있는 입력이 다시 검증되는 시기를 설정할 수 있습니다.
- defaultValues: 최초 렌더 시, 기본 값을 설정 할 수 있습니다.
- resolver : 이 옵션은 Yup, Zod, Joi, Superstruct, Vest 등 외부 유효성 검사 라이브러리와의 통합을 지원합니다.
- criteriaMode( firstError | all )
- firstError(기본값)로 설정하면 각 필드의 첫 번째 오류만 수집됩니다.
- all로 설정하면 각 필드의 모든 오류가 수집됩니다.
- shouldFocusError : 사용자가 유효성 검사에 실패한 폼을 제출하면 에러가 있는 첫 번째 필드에 포커스가 설정될 지의 여부를 설정합니다.
- shouldUnregister: 기본적으로 입력 값은 입력이 제거될 때 유지됩니다. 언마운트 될 때 입력을 등록 해제(unregister) 하려면 shouldUnregister 를 true 로 설정하면 됩니다.
- delayError: 사용자에게 표시되는 에러 상태를 지연시킵니다. (ms)
type FormInputs = {
firstName: string;
lastName: string;
};
const { register } = useForm<FormInputs>({
mode: 'onSubmit',
reValidateMode: 'onChange',
defaultValues: {},
resolver: undefined,
context: undefined,
criteriaMode: "firstError",
shouldFocusError: true,
shouldUnregister: false,
delayError: undefined
})
import "./styles.css";
import React, { useState } from "react";
import { useForm } from "react-hook-form";
interface Form {
name: string;
password: string;
}
function App() {
const { register, handleSubmit } = useForm();
const [result, setResult] = useState("");
const onSubmit = (data: Form) => setResult(JSON.stringify(data));
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="text" {...register("name")} placeholder="User Name" />
<input type="password" {...register("password")} placeholder="Password" />
<input type="submit" />
<p>{result}</p>
</form>
);
}
export default App;
watch()
watch() 함수는 특정 입력을 감시하고 입력된 값을 반환합니다. 무엇을 렌더링 할지 결정하는데 사용됩니다.
첫번째 인자로 감시할 필드명, 두번째 인자로 defaultValue 를 전달합니다.
import React from "react";
import "./styles.css";
import { useForm } from "react-hook-form";
function App() {
const { register, watch } = useForm();
const watchNickname = watch("nickname", "Easton");
return (
<form>
<input type="text" placeholder="User Name" {...register("nickname")} />
<input type="password" placeholder="Password" {...register("password")} />
<p style={{ color: "#fff" }}>{watchNickname}</p>
</form>
);
}
export default App;
2. 유효성 검사
react-hook-form 은 HTML standard for form validation 에 맞춰 유효성 검사를 쉽게 만들어줍니다.
지원되는 유효성 검증 목록
- required (필수값)
- min (최소값)
- max (최대값)
- minLength (최소 길이)
- maxLength (최대 길이)
- pattern (패턴)
- validate (검증하기)
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import "./styles.css";
interface Form {
age: number;
}
function App() {
const { register, handleSubmit } = useForm();
const [result, setResult] = useState<string>();
const onSubmit = (data: Form) => {
setResult(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input
type="number"
placeholder="Age"
{...register("age", { required: true, min: 17 })}
/>
<input type="submit" />
<p style={{ color: "#fff" }}>{result}</p>
</form>
);
}
export default App;
3. 에러 핸들링
react-hook-form 은 폼의 에러를 표시하기 위해 errors 객체를 제공합니다. 에러 유형은 주어진 유효성 검사 조건을 반환합니다.
2. 유효성 검사 코드를 그대로 가져와서 에러 핸들링하는 부분을 추가해보도록 하겠습니다.
import "./styles.css";
import React, { useState } from "react";
import { useForm } from "react-hook-form";
interface Form {
age: number;
}
function App() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm();
const [result, setResult] = useState<string>();
const onSubmit = (data: Form) => {
setResult(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="container">
<input
type="number"
placeholder="Age"
{...register("age", { required: true, min: 17, max: 40 })}
/>
{errors.age?.type === "required" && (
<span className="error">Age is required</span>
)}
{errors.age?.type === "min" && (
<span className="error">Minimum Age is 17</span>
)}
{errors.age?.type === "max" && (
<span className="error">Maximum Age is 40</span>
)}
</div>
<input type="submit" />
<p style={{ color: "#fff" }}>{result}</p>
</form>
);
}
export default App;
4. 중첩된 컴포넌트 구조에서 FormContext 사용하기
FormContext 는 주로 깊게 중첩된 폼 컴포넌트 구조에서 컨텍스트를 prop 으로 전달하기 어려울 때 사용합니다.
FormProvider
FormProvider 는 Provider 로 컨텍스트를 구독하는 컴포넌트들에게 FormContext 의 변화를 알리는 역할을 합니다.
FormContext 를 사용하기 위해서는 최상위 컴포넌트를 FormProvider 로 감싸주어야 합니다.
useFormContext()
useFormContext 는 FormContext 에 접근하기 위하여 사용합니다. 깊게 중첩된 컴포넌트 구조에서 컨텍스트를 구독하기 위하여 사용됩니다.
useFormContext 가 제대로 동작하기 위해선 상위 컴포넌트를 FormProvider 로 래핑해야 합니다.
5. 제어 컴포넌트에서 react-hook-form 사용하기
react-hook-form 은 기본적으로 비제어 컴포넌트(Uncontrolled Component) 와 HTML Input 의 입력을 수용하지만 외부 라이브러리의 제어 컴포넌트(Controlled Component) 와 함께 작업하는 것을 피할 수 없는 경우가 생기기 마련입니다.
이를 간단히 처리하기 위하여 react-hook-form 에서는 Controller 라는 래퍼 컴포넌트를 제공합니다.
import React, { useState } from "react";
import { useForm, Controller } from "react-hook-form";
import { TextField, Input } from '@material-ui/core';
interface Form {
age: number;
}
function App() {
const { handleSubmit, control, formState: { errors } } = useForm();
const [result, setResult] = useState<string>();
const onSubmit = (data: Form) => {
setResult(JSON.stringify(data));
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="container">
<Controller
name="age"
control={control}
defaultValue={33}
rules={{ required: true, min: 17, max: 40 }}
render={({ field }) => <Input style={{ width: 280 }} {...field} />}
/>
{errors.age?.type === "required" && (
<span className="error">Age is required</span>
)}
{errors.age?.type === "min" && (
<span className="error">Minimum Age is 17</span>
)}
{errors.age?.type === "max" && (
<span className="error">Maximum Age is 40</span>
)}
</div>
<input type="submit" />
<p>{result}</p>
</form>
);
}
export default App;
본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성되었습니다
댓글