프로젝트 내 기존의 입력 폼을 리팩토링하면서 다시 한번 zod, react hook form에 대해 리뷰해보고 싶어졌다.
zod, react hook form 사용 전
이메일 폼 하나에 대해서만 해도 이메일의 값, 유효성 상태, 유효성 검사 처리 등 작성해야 할 코드가 굉장히 많아진다. 또한 폼이 늘어날 수록 관리해야 할 상태도 많아지고 폼 입력 시마다 컴포넌트 전체가 리렌더링 되기 때문에 성능 저하를 가져 올 가능성이 있다.
위의 문제들을 zod와 react hook form을 이용해 리팩토링 해보려고 한다.
// email 폼 상태관리
const [email, setEmail] = useState("");
const [emailIsValid, setEmailIsValid] = useState({
message: "email is required",
isError: true,
});
// 이메일 유효성 검사 처리
const checkEmailVerification = (value: string) => {
if (value == "") {
setEmailIsValid({ isError: true, message: "email is required" });
return;
}
setEmailIsValid({ isError: false, message: "" });
};
const emailOnChange = (value: string) => {
setEmail(value);
checkEmailVerification(value);
};
// 폼 유효성 상태
const formIsValid = !emailIsValid.isError && 그 외 다른 폼 에러 상태 ...
(생략)
<input
name="email"
type="text"
value={email}
onChange={(e) => emailOnChange(e.target.value)}
/>
<button disabled={!formIsValid}>Submit</button>
zod, react hook form 사용하기
1. zod 왜 사용하나?
- 유효성 검사를 쉽게 처리 할 수 있다.
- 타입스크립트만으로는 런타임 타입 검증을 할 수 없다.
2. react hook form 왜 사용하나?
- 폼과 관련된 코드를 심플하게 작성 할 수 있다.
- 제어 컴포넌트와 비제어 컴포넌트의 장점만을 가져와 사용 할 수 있다.
✅ 제어 컴포넌트 vs 비제어 컴포넌트
제어 컴포넌트 : state를 사용하여 데이터를 관리한다, 입력 폼이 많아질 수록 관리해야할 state도 많아지고 폼 입력값이 변할 떄마다 리렌더링 발생한다. 이 과정에서 불필요한 연산이 발생할 수 있다.
- 언제 제어 컴포넌트 사용하면 좋은가? : 폼 입력 값이 변할 때마다 유효성 검사가 필요한 경우
비제어 컴포넌트 : state를 가지지 않으며 사용자 입력에 대한 DOM 이벤트를 직접 처리하여 데이터를 추출한다.
- 언제 비제어 컴포넌트 사용하면 좋은가? : 성능 최적화가 중요한 경우, 외부 라이브러리와 통합 시(ex: shadcn ui)
zod, react hook form 사용해서 리팩토링하기
import { useForm, SubmitHandler } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
const loginSchema = z
.object({
email: z.string().nonempty({ message: "Email is required" }).email(),
password: z.string().nonempty({ message: "Password is required" }),
passwordConfirm: z.string().nonempty({ message: "Check your password" }),
})
.refine((data) => data.password === data.passwordConfirm, {
path: ["passwordConfirm"],
message: "Check your password",
});
type LoginProp = z.infer<typeof loginSchema>;
export default function LoginPage() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginProp>({
resolver: zodResolver(loginSchema),
});
const onSubmit: SubmitHandler<LoginProp> = (data) => 로그인 데이터 처리;
return (
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="email">e-mail</label>
<input
id="email"
{...register("email", { required: true })}
/>
{errors.email?.message && (
<span>
{errors.email?.message}
</span>
)}
<label htmlFor="password">password</label>
<input
id="password"
type="password"
{...register("password")}
/>
{errors.password?.message && (
<span>
{errors.password?.message}
</span>
)}
<label>password confirm</label>
<input
id="passwordConfirm"
type="password"
{...register("passwordConfirm")}
/>
{errors.passwordConfirm?.message && (
<span>
{errors.passwordConfirm?.message}
</span>
)}
<buttontype="submit">Submit</button>
</form>
</div>
);
}
+ input 컴포넌트화 하기
1. register 방식
- 비교적 간단한 폼에서 불필요한 상태 관리 최소화해 최적화를 최대한 활용하고 싶을 때
// Input.tsx
type InputProps = {
id: "email" | "password" | "passwordConfirm";
type?: string;
register: UseFormRegister<LoginProp>;
errorMessage?: string;
};
export const Input = ({
id,
type = "text",
register,
errorMessage,
}: InputProps) => {
return (
<div>
<input
id={id}
type={type}
{...register(id)}
/>
{errorMessage && (
<span>{errorMessage}</span>
)}
</div>
);
};
// LoginPage.tsx
<Input
id="password"
type="password"
register={register}
errorMessage={errors.password?.message}
/>
2. useController 방식
- 커스텀 컴포넌트를 사용하거나 복잡한 폼 상태 관리 할 때
// Input.tsx
import React from "react";
import { useController, Control, FieldValues, Path } from "react-hook-form";
type InputProps<T extends FieldValues> = {
name: Path<T>;
control: Control<T>;
type?: string;
label?: string;
};
export const Input = <T extends FieldValues>({
name,
control,
type = "text",
label,
}: InputProps<T>) => {
const {
field,
fieldState: { error },
} = useController<T>({
name,
control,
rules: { required: `${label || name} is required` },
});
return (
<div>
{label && (
<label htmlFor={name}>{label}</label>
)}
<input
id={name}
type={type}
{...field}
/>
{error && (
<span">{error.message}</span>
)}
</div>
);
};
// LoginPage.tsx
const { control, handleSubmit } = useForm<LoginProp>({
...
});
<Input
name="password"
type="password"
label="Password"
control={control}
/>