이전 프로젝트에서는 formik과 yup이라는 유효성관리 라이브러리를 사용했는데
이번에는 react hook form 그리고 zod를 사용해보았다.
React Hook Form이란?
Performant, flexible and extensible forms with easy-to-use validation.
사용하기 쉬운 유효성 검사를 통해 성능이 우수하고 유연하며 확장 가능한 양식을 제공합니다.
React Hook Form을 사용해서 폼 컴포넌트의 개발과 유지 보수를 용이하게 할 수 있다.
React Hook Form을 사용하면 폼 컴포넌트의 상태 및 유효성 검사를 관리하는 데 필요한 코드 양을 대폭 줄일 수 있음
폼 요소의 값이나 상태 변경에 대한 이벤트를 관리하기 위해 명시적인 이벤트 핸들러를 작성할 필요 없이, 단순히 Hook(useForm,useController,useWatch...)을 사용하여 관련 데이터를 추적하고 업데이트할 수 있음.
장점
- 간결한 API : React Hook Form은 사용하기 쉽고 직관적인 API를 제공하여 복잡한 폼 로직을 단순화 한다. 기본적으로 제공하는 hook 함수들과 컴포넌트들을 사용하여 폼을 쉽게 생성하고 관리할 수 있다.
- 높은 성능 : React Hook Form은 성능에 중점을 두어 최적화되어 있다. 입력 필드의 값 변화를 추적하는 상태 대신 각 입력 필드의 참조(reference)를 사용하여 불필요한 리렌더링을 방지하고, 가상 DOM의 업데이트를 최소화한다. 그로 인해 높은 성능을 보여줌.
- 유효성 검사: React Hook Form은 내장된 유효성 검사를 지원하며, yup,Joi등의 외부 유효성 검사 라이브러리와 통합할 수 있다. 입력 필드의 값에 대한 유효성 검사를 수행하고, 에러 메세지를 표시할 수 있음
- 커스텀 훅: 커스텀 훅을 사용하여 개발자가 필요한 로직을 쉽게 작성하고 재사용할 수 있도록 지원한다. 커스텀 훅을 사용하면 폼 상태, 에러 처리, 폼 제출 등의 로직을 캡슐화할 수 있음.
제어 컴포넌트
react에 의해 값이 제어되는 컴포넌트이다. useState hook을 통해 데이터를 관리한다.
비제어 컴포넌트
react에 의해 값이 제어되지 않는 컴포넌트, useRef를 통해 데이터를 관리한다.
리렌더링이 발생하지 않기 때문에 필요에 따라 활용이 가능하다.
React hook form
- 제어컴포넌트로 form을 관리하면, 여러 state와 값을 변경하는 event Handler, 또 값을 검증하는 validation이 존재한다 => 코드양이 많아지고 관리하기 번거로울뿐더러, 불필요한 리렌더링이 발생한다.
- react-hook-form의 useForm hook을 사용하면 form을 쉽게 사용할 수 있다.
- 기본적으로 동작하는 방식은 비제어 방식으로 동작한다. 비제어방식에서는 register함수를 통해 react-hook-form이 input에 대한 값들을 추적하도록 도와준다. => 불필요한 렌더링을 줄이고 성능이 좋아짐
- Controller 방식을 사용하면 MUI와 같은 라이브러리와 함께 사용이 가능하다.
mode, defaultValues,resolver
- mode: 동작모드 설정 및 유효성 검사방법을 지정하는 option
onBlur(기본값): 입력 필드의 유효성 감사가 입력 필드가 포커스를 잃을 때만 수행한다. 사용자가 입력 필드를 편집하고 다른 곳을 클릭하거나 탭할 때 유효성 검사가 실행됨. -> 사용자 경혐 향상, 입력 필드가 자주 변경되는 경우 유효성 검사를 줄이는 데 유용하다.
onChange: 입력 필드의 값이 변경될 때마다 즉시 유효성 검사가 수행된다. 사용자가 텍스트를 입력할 때마다 오류 메세지를 표시하거나 숨기고자 할 때 유용하다. -> 다수의 리렌더링이 발생할 수 있어 성능에 영향을 끼칠 수 있다.
onSubmit: 폼 제출시에만 유효성 검사가 진행된다. - defaultValues : 기본값을 설정하는 역할 -> 처음 렌더링될때만 적용된다. 이후에는 setValue 메서드를 사용해 동적으로 값 변경이 가능하다. * React Hook Form 을 사용할 때 기본값을 제공하지 않는 경우 Input의 초기값은 undefined로 관리된다.
사용 예
//회원가입 폼 상태 관리
const form = useForm({
defaultValues: {
email: '',
code: '',
password: '',
confirmPassword: '',
nickname: '',
phone: '',
},
mode: 'onChange',
});
resolver
- 비동기 유효성 검사를 수행하기 위해 사용한다.
- resolver는 폼 제출 시 실행되는 함수를 정의한다. 이 함수는 입력 데이터를 인자로 받아서 비동기적으로 유효성 검사를 싱행하고 Promise로 유효성 검사 결과를 반환한다.
- Promise가 resolve 되면 실행이 계속 되고 아니면 중단한다.
- React Hook Form과 함께 사용되는 resolver는 yup,zod.. 등이 있다. 클라이언트단에서 zodResolver를 이용해 input값들을 서버를 보내기전 검증하는 과정을 거친다.
- 스키마를 정의해서 정의한 스키마 기반으로 정보를 검증한다.
사용 예
// 스키마 정의
const signUpFormSchema = zod
.object({
// 이메일 형식 지정
email: zod.string().email({ message: '이메일 형식이 아닙니다.' }),
code: zod.string().min(1, { message: '인증코드를 입력해주세요.' }),
password: zod // 비밀번호 형식 지정
.string()
.regex(
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,20}$/,
'문자와 특수문자, 숫자가 혼합된 8~20자리의 비밀번호를 입력해주세요.'
)
.min(8, { message: '비밀번호는 8자 이상이어야 합니다.' })
.max(20, { message: '비밀번호는 20자 이하여야 합니다.' }),
confirmPassword: zod.string(),
nickname: zod.string().min(1, { message: '닉네임은 필수 입력값입니다.' }),
phone: zod.string().regex(/^(\d{3}-\d{3,4}-\d{4})$/, '전화번호 형식이 유효하지 않습니다.'),
})
register
- React Hook Form에 입력요소를 등록하는데 사용한다. resister하는 과정을 통해 해당 input값을 제어하고, 값을 수집, 유효성 검사를 실행한다.(함수형태)
- register 함수의 첫번째 매개변수에는 name을 준다. 해당 필드를 다루게 될 key 값으로 반드시 들어가야하는 값이다.
- 두번째 값으론 options 객체가 들어가는데 해당 객체는 유효성 검사를 위한 프로퍼티들이 들어갈 수 있다.
사용 예
<input
type="text"
{...register("email", {
pattern: {
value:
/^[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*@[0-9a-zA-Z]([-_.]?[0-9a-zA-Z])*.[a-zA-Z]{2,3}$/i,
message: "이메일 형식에 맞지 않습니다.",
},
})}
/>;
* 유효성 검사를 위해서 value만 줄수도 있지만 value,message로 구성된 객체를 줌으로써 해당 에러에 대한 구체적인 메세지를 제공할 수도 있다.
watch
watch: 폼에 입력된 값을 구독하여 실시간으로 체크 할 수 있게 해주는 함수. 매개변수를 주지 않으면 전체값을 관찰가능하다. 매개변수를 주면 해당 name의 값만 관찰 가능하다.
const {id, name, pwd, ...watch} = watch(); //전체 필드를 리턴
const id = watch("id");
해당 값에 따라 리렌더링을 발생시킨다 -> 폼 내에서 값이 변경할 때마다 해당 값이 변한다.
{watch("occupation") === "professor" ? (
<Row>
<Label>phone: </Label>
<ControlInputText<IForm> control={control} name="phone" />
</Row>
) : null
}
*관찰하려는 필드에 defaultValue를 주지않는다면 초기값이 undefined로 관리가 된다.
serValue,getValue
- setValue: setValue(name,value) 형태로 사용한다. Key값에 대한 value를 등록한다.
- getValues: 값을 반환하지만 리렌더링을 발생하지않고 해당 값을 추적하지 않는다.( 그 순간의 값만 가져온다.)
handleSubmit, setError, setFocus
handleSubmit: submit 이벤트가 발생했을 때, form 태그에 onSubmit 이벤트 프로퍼티에 handleSubmit이라는 함수를 넣어주는 형태로 사용한다.
onSubmit에서는 기본이벤트를 막아주는 e.preventDefault()를 할 필요가 없다.
setError, setFocus : onSubmit에서 에러가 발생했다면, setError 함수를 사용해 에러를 발생시킬 수 잇고, 에러가 발생한 필드에 setFocus를 통해 강조할 수 있다.
setError("name", {type: "minLength", message: "1글자 이상 입력해주세요"});
setFocus("name");
그래서 zod는 언제 쓰는디요...
zod를 사용하면! 폼과 유효성 검사를 위한 코드에 관한 내용을 분리하여 작성할 수 있다!
resolver 파트 참고
사용하기
npm install react-hook-form zod @hookform/resolvers // npm
yarn add react-hook-form zod @hookform/resolvers // yarn
zod를 이용하여 Schema 작성
import { z as zod } from 'zod';
const signUpFormSchema = zod
.object({
// 이메일 형식 지정
email: zod.string().email({ message: '이메일 형식이 아닙니다.' }),
code: zod.string().min(1, { message: '인증코드를 입력해주세요.' }),
password: zod // 비밀번호 형식 지정
.string()
.regex(
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,20}$/,
'문자와 특수문자, 숫자가 혼합된 8~20자리의 비밀번호를 입력해주세요.'
)
.min(8, { message: '비밀번호는 8자 이상이어야 합니다.' })
.max(20, { message: '비밀번호는 20자 이하여야 합니다.' }),
confirmPassword: zod.string(),
nickname: zod.string().min(1, { message: '닉네임은 필수 입력값입니다.' }),
phone: zod.string().regex(/^(\d{3}-\d{3,4}-\d{4})$/, '전화번호 형식이 유효하지 않습니다.'),
})
.refine(data => data.password === data.confirmPassword, {
path: ['confirmPassword'],
message: '비밀번호가 일치하지 않습니다.',
});
zod를 Import 할때 원래는 import z from 'zod' 이긴한데..
요약어를 쓰는게 좋아보이지않는다라는 조언을 들어서 zod로 바꿔 사용했다.
useForm
//회원가입 폼 상태 관리
const form = useForm({
resolver: zodResolver(signUpFormSchema),
defaultValues: {
email: '',
code: '',
password: '',
confirmPassword: '',
nickname: '',
phone: '',
},
mode: 'onChange',
});
const {
register,
formState: { errors },
getValues,
} = form;
*resolver: signUpFormSchema를 전달하면 내가 작성한 규칙대로 form에서 유효성 검사를 한다.
formState : 입력 필드의 상태를 return 한다.
React Hook Form 라이브러리의 useForm 훅을 사용하여 mode를 onSubmit으로 설정해줌으로써 비제어 컴포넌트가 되어 불필요한 렌더링을 제거한다. 만약 mode를 onChange로 설정한다면 입력값에 실시간으로 피드백이 가능한 제어 컴포넌트로 동작한다.
나의 프로젝트에서는 onChange를 사용할지말지 고민을 했는데 사용자 입장에서 실컷 입력 다 하고 제출했는데 그때서야 오류가 뜨면 조금 짜증날것같아서 onChange로 설정을 해둔채로 작업을 진행했다.
다 만들고 나서 생각해보니 그냥 onSubmit 하고 인풋 밑에 작게 안내문구를 써놔도 되었겠다 라는 생각이 들긴한다.
유효성 검사 적용
<FormProvider {...form}>
<TextField
{...register('email')}
{errors.email ? errors.email.message : '사용가능한 이메일입니다.'}
/>
</FormProvider>
{errors.email ? errors.email.message : '사용가능한 이메일입니다.'} -> 규칙에 맞지 않는 입력값이라면 스키마에서 작성해둔 메세지가 출력된다.
실제 적용 코드
import AuthInput from '@/components/AuthInput';
import CommonButton from '@/components/CommonButton';
import { zodResolver } from '@hookform/resolvers/zod';
import { FormProvider, useForm } from 'react-hook-form';
import { z as zod } from 'zod';
const SignUp = () => {
const signUpFormSchema = zod
.object({
// 이메일 형식 지정
email: zod.string().email({ message: '이메일 형식이 아닙니다.' }),
code: zod.string().min(1, { message: '인증코드를 입력해주세요.' }),
password: zod // 비밀번호 형식 지정
.string()
.regex(
/^(?=.*[a-zA-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,20}$/,
'문자와 특수문자, 숫자가 혼합된 8~20자리의 비밀번호를 입력해주세요.'
)
.min(8, { message: '비밀번호는 8자 이상이어야 합니다.' })
.max(20, { message: '비밀번호는 20자 이하여야 합니다.' }),
confirmPassword: zod.string(),
nickname: zod.string().min(1, { message: '닉네임은 필수 입력값입니다.' }),
phone: zod.string().regex(/^(\d{3}-\d{3,4}-\d{4})$/, '전화번호 형식이 유효하지 않습니다.'),
})
.refine(data => data.password === data.confirmPassword, {
path: ['confirmPassword'],
message: '비밀번호가 일치하지 않습니다.',
});
//회원가입 폼 상태 관리
const form = useForm({
resolver: zodResolver(signUpFormSchema),
defaultValues: {
email: '',
code: '',
password: '',
confirmPassword: '',
nickname: '',
phone: '',
},
mode: 'onChange',
});
const {
register,
formState: { errors },
getValues,
} = form;
const handleClickSignUp = form.handleSubmit(async data => {
try {
await signUpAPI({
email: data.email,
password1: data.password,
password2: data.confirmPassword,
nickname: data.nickname,
phone: data.phone,
});
// console.log(response, '회원가입성공');
navigate('/sign-in', { replace: true })
} catch (error) {
//에러처리인데 이미 있는 회원이면.. 무슨코드 내려주는지 모르겟서잉...
if ((error as AxiosError).response && (error as AxiosError).response?.status === 409) {
console.error('이미 존재하는 회원입니다:', (error as AxiosError).response?.data);
toast.error('이미 존재하는 회원입니다.');
} else {
console.error('사용자 등록 오류:', error);
}
}
// console.log(data);
});
return (
<FormProvider {...form}>
<div className="w-full mb-40 bg-mainBlack flex justify-center items-center flex-col">
<div className="w-[460px] flex justify-center flex-col mt-40">
<p className="font-didot text-3xl text-center mb-7">Sign Up</p>
<form onSubmit={handleClickSignUp} autoComplete="off">
<div className="w-full flex items-center">
<div className="flex-grow">
<AuthInput {...register('email')} type="email">
Email
</AuthInput>
</div>
<div className="flex-none w-1/4 ml-2">
<CommonButton
className="w-full h-12 rounded-lg bg-mainWhite px-3.5 py-2.5 mt-6 text-base font-base text-mainBlack shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
onClick={handleSendAuthCode}
>
{countdown > 0 ? `재전송` : '인증번호 발송'}
</CommonButton>
</div>
</div>
{errors.email && <p className=" text-sm text-red-500 mt-1">{errors.email.message}</p>}
<div className="flex flex-col">
<div className="w-full flex items-center">
<div className="flex-grow relative">
<AuthInput {...register('code')} type="text" placeholder="인증코드 입력">
Auth Code
</AuthInput>
<p className="absolute bottom-3.5 right-3 text-subGray text-sm">
{`${Math.floor(countdown / 60)}:${countdown % 60 < 10 ? '0' : ''}${countdown % 60}`}
</p>
</div>
<div className="flex-none w-1/4 ml-2">
<CommonButton
className="w-full h-12 rounded-lg bg-mainWhite px-3.5 py-2.5 mt-6 text-base font-base text-mainBlack shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
onClick={handleAuthCode}
type="button"
>
확인
</CommonButton>
</div>
</div>
{authCodeSent && (
<p className="text-subGray text-sm mt-2">인증코드가 발송되었습니다. 이메일을 확인해주세요.</p>
)}
{authCodeError && (
<p className="text-red-500 text-sm mt-2">
인증번호가 일치하지 않습니다. 올바른 인증번호를 입력해주세요.
</p>
)}
{emailVerified && <p className="text-subGray text-sm mt-2">이메일 인증이 완료되었습니다.</p>}
</div>
{errors.code && <p className=" text-sm text-red-500 mt-1">{errors.code.message}</p>}{' '}
<AuthInput {...register('password')} type="password">
Password
</AuthInput>
{errors.password && <p className=" text-sm text-red-500 mt-1">{errors.password.message}</p>}
<AuthInput {...register('confirmPassword')} type="password">
Confirm Password
</AuthInput>
{errors.confirmPassword && <p className=" text-sm text-red-500 mt-1">{errors.confirmPassword.message}</p>}
<AuthInput {...register('nickname')} type="text">
Nickname
</AuthInput>
{errors.nickname && <p className=" text-sm text-red-500 mt-1">{errors.nickname.message}</p>}
<AuthInput {...register('phone')} type="tel" placeholder="010-0000-0000">
Phone
</AuthInput>
{errors.phone && <p className=" text-sm text-red-500 mt-1">{errors.phone.message}</p>}
<CommonButton className=" w-full rounded-lg bg-mainWhite px-3.5 py-2.5 text-lg font-semibold font-didot text-mainBlack shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 mt-6">
Sign In
</CommonButton>
</form>
</div>
<div className="mt-6 ">
회원이신가요?
<button type="button" className="font-semibold ml-2">
Sign in
</button>
</div>
</div>
</FormProvider>
);
};
export default SignUp;
이렇게 사용하게 된다면
React Hook Form을 사용해서 form 상태를 관리하여 불필요한 렌더링을 방지하고, zod를 사용하여 유효성 검사를 함으로써 코드의 가독성을 향상 시킬 수 있다!
나는 테일윈드까지 사용한 코드이기때문에.. 만약 React Hook Form,zod를 사용하지 않았다면 코드가 더 길어졌을것이다.
참고
react-hook-form 을 활용해 효과적으로 폼 관리하기
'React' 카테고리의 다른 글
React websocket을 이용한 1:1 채팅 만들기 (0) | 2024.07.10 |
---|---|
React 파일 이미지 최적화 하기 react-image-file-resizer (0) | 2024.07.10 |
리액트로 OAuth 소셜로그인 구현하기(구글,네이버,카카오) (0) | 2024.06.10 |
React-Query 사용하기 (0) | 2024.06.10 |
리액트 랜딩페이지 제작하기 1일차 (0) | 2024.03.22 |
댓글