최근에 프로젝트를 하면서 사용해본 라이브러리이다. form의 validation을 하는 로직이 중복되어 줄일 방법이 없나 고민하던 찰나, 스터디에서 권유를 받아 써봤고, 결과적으로 거의 모든 form 관리에 이 라이브러리를 사용하게 됐다.
기본적으로 react에서 입력값에 대한 validation(유효성 검사)는 useState을 통한 상태관리로 이루어진다. validation 룰을 직접 작성하거나, joi나 zod 같은 data schema를 이용해 정규식 등으로 input값의 타입이나 유형을 지정하고, 이를 원하는 이벤트에 실행하여 form 데이터에 대한 검증을 실행하고 error 또한 마찬가지로 상태로 관리한다.
기존의 밸리데이션 방식을 한 번 보자.
import {useState} from "react";
const Login= () =>{
const [formValues, setFormValues] = useState({email:"", password:""});
const [formErrors, setFormErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const onChange = (e)=>{
const { name, value } = e.target;
setFormValues({ ...formValues, [name]: value });
}
const handleSubmit = (e) => {
e.preventDefault();
validate(formValues);
setIsSubmitting(true);
};
const validate = (values) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i;
if (!values.email) {
setFormErrors({email:"Cannot be blank"})
} else if (!regex.test(values.email)) {
setFormErrors({email:"Invalid email format"});
}
if (!values.password) {
setFormErrors({password:"Cannot be blank"});
} else if (values.password.length < 4) {
setFormErrors({password:"Password must be more than 4 characters"});
}
};
useEffect(() => {
if (Object.keys(formErrors).length === 0 && isSubmitting) {
submitForm();
}
}, [formErrors]);
const submitForm = ()=>{
axios.post(`/api/user`, {
email, password
}, {
withCredentials: true // CORS 처리 옵션
}
).then(() => {
setFormValues({email:"", password:""});
}).catch((error) => {
console.dir(error);
});
}
return(
<form onSubmit={handleSubmit}>
<input value={formValues.email} onChange={onChange}/>
{formErrors.email && (
<span className="error">{formErrors.email}</span>
)}
<input value={formValues.passWord} onChange={onChange}/>
{formErrors.password && (
<span className="error">{formErrors.password}</span>
)}
</form>);
참고: https://www.smashingmagazine.com/2020/10/react-validation-formik-yup/
리액트로만 작성한 validation 예시이다.
로직은 단순하지만, 여러 단점들이 있다. 에러부터 입력값까지 모든 것을 state로 활용해야 하며, 심지어 form state도 관리해야 한다. state이 바뀔 때마다 리렌더링이 되니, 무거워질 수 밖에 없고 리렌더링을 줄이기 위해 많은 부분을 최적화해야한다.
useForm을 활용했을 때는 이런 리렌더링 일어나지 않으면서도 input의 상태가 관리된다. 무엇보다 validation부터 error 처리 및 formdata의 관리까지 많은 것들이 간단해진다.
import {useForm} from 'react-hook-form';
const Login=()=>{
const {register, watch, handleSubmit, reset, formState }= useForm({ defaultValues: {text:""});
useEffect= (()=>{
if(formState.isSubmitSuccessful){
reset({text:""});
}, [formState, reset]);
const onSubmit = data=>{
const {email, password} = data;
axios.post(`/api/user`, {
email, password
}, {
withCredentials: true // CORS 처리 옵션
}
).catch((error) => {
console.dir(error);
});
}
const{errors} = formState;
return(
<form onSbumit={handleSubmit(onSubmit)}>
<input {...register("email", {
required:"필수 응답 항목입니다.",
pattern:{
value:/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i,
message:"이메일 형식이 아닙니다."}})}/>
{errors.email & errors.message}
<input {...register("password", {
required:"필수 응답 항목입니다.",
minLength:{
value:8
message:"비밀번호는 8자 이상이여야 합니다,"}})} />
{errors.password & errors.password.message}
</form>);
}
한눈에 봐도 코드가 많이 줄었다.
우선 register함수를 기존에 상태를 관리하던 input에 언팩하면 기존과 동일하게 상태를 관리한다. watch 함수를 이용하면 onChange이벤트와 똑같이 상태변화를 추적할 수 있다. register한 input의 value를 관리하면서도 리렌더링은 일어나지 않는다.
form의 onSubmit 이벤트는 useForm 함수 객체의 handleSubmit이라는 메서드로 핸들링하며, handleSubmit은 매개변수로 formData를 가진다. 즉, onSubmit={handleSubmit(data=> console.log(data)} 와 같은 방식으로 원하는 처리 동작을 handleSubmit함수의 콜백함수로 처리한다.
입력값은 defaultValues라는 옵션으로 초기값을 지정할 수 있으며, 이 옵션은 초기에 useForm을 호출할 때 useForm({defaultValues:{email:""}})와 같이 매개변수로 넣어주면 된다.
setValue 입력값을 초기화시키거나,reset으로 입력값을 defaultvalues로 초기화할 수 있다. 이 때, formState에 subscribe 되어있다면(formState.errors 등의 상태를 컴포넌트가 follow한다면) 특정값으로 변화시키고 싶을 때는 handleSubmit 함수에서 바로 실행시키는 것이 아니라, useEffect에서 현재 form data의 submit 상태를 나타내는 formState을 dependency로 하여 트리거 해야 한다는 점이다. 이는 handleSubmit이 자체적으로 비동기이기 때문이며, 마치 useReducer에서 비동기 로직과 dispatch를 따로 분리시켜야 하는 것과 비슷하다고 생각하면 된다.
useForm을 호출할 때 사용할 수 있는 옵션은 다음과 같다.
useForm({
mode: 'onSubmit',
reValidateMode: 'onChange',
defaultValues: {},
resolver: undefined,
context: undefined,
criteriaMode: "firstError",
shouldFocusError: true,
shouldUnregister: false,
shouldUseNativeValidation: false,
delayError: undefined
})
보시는 바와 같이 다양한 옵션이 있으며, mode를 활용하면 validation을 트리거하는 이벤트를 다양하게 설정할 수 있다. 포커스가 없어졌을 때, input을 입력했을 때, submit 했을 때 등 사용자 이벤트를 다양하게 활용할 수 있다.
기타 옵션에 대한 자세한 설명은 공식문서를 참조하면 된다.
useForm API 공식문서
react-hook-form은 타입스크립트를 지원한다. 타입스크립트를 활용하면 강력한 타입검증은 덤이다.
import { useForm, SubmitHandler } from "react-hook-form";
type Formvalues = {
email: string,
password: string
}
const Login:VFC = ()=>{
const { register, handleSubmit, formState, reset } = useForm<Formvalues>({
defaultValues: {
email: "",
password: ""
}
});
const onSubmit:SubmitHandler<Formvalues> = (data)=>{
const {email, password} = data;
axios.post(`/api/user`, {
email, password
}, {
withCredentials: true // CORS 처리 옵션
}
).catch((error) => {
console.dir(error);
});
}
const{errors} = formState;
return(
<form onSbumit={handleSubmit(onSubmit)}>
<input {...register("email", {
required:"필수 응답 항목입니다.",
pattern:{
value:/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i,
message:"이메일 형식이 아닙니다."}})}/>
{errors.email & errors.email.message}
<input {...register("password", {
required:"필수 응답 항목입니다.",
minLength:{
value:8
message:"비밀번호는 8자 이상이여야 합니다,"}})} />
{errors.password & errors.password.message}
</form>);
useForm 함수를 호출하면서 타입을 주었고, handleSubmit의 매개변수가 될 함수에도 타입을 줬다. 보다 강력한 타입 검증을 할 수 있다.
useForm는 타입스크립트와 완전히 호환된다. 거의 모든 메서드를 타이핑할 수 있으므로 단순히 사용자 입력값만이 아니라 formData을 원하는 시점에 검증할 수 있다.
다시 한 번 공식문서 링크를 남긴다. 공식문서가 참 잘 되어있다.
error 발생 시 다음과 toastify 라이브러리에서 제공하는 UI를 사용하여 깔끔하게 UI를 표시할 수 있다. ZeroCho님 덕분에 알게됐다.
https://github.com/fkhadra/react-toastify#readme
첫번째 매개변수로 표시할 메시지, 두번째로 position 등의 옵션을 줄 수 있다. 참 깔끔한 것 같다.
감사합니다~ 잘 참고했습니다! onSbumit 오타가 있는거 같네요!