react-hook-form :
React
기반의form
관리 라이브러리로,form 상태
와유효성 검사
를 처리하기 위한 간편한 방법을 제공한다. 이를 통해form component
의 개발과 유지 보수를 용이하게 처리할 수 있다.
$ yarn add react-hook-form
useForm
hook을 사용하여 register
를 이용해 등록한다.
import { useForm } from "react-hook-form";
const { register, handleSubmit } = useForm<FormType>({ defaultValues: { email: '', password: ''}});
// validation 성공
const handleFormSubmit = (form: FormType) => {};
// validation 실패
const handleFormError = (errors: FieldErrors<FormType>) => {};
<form onSubmit={handleSubmit(handleFormSubmit, handleFormError)}>
<input
type="email"
{...register("email", {
pattern: {
value: EMAIL_REGEX,
message: "Email is invalid.",
},
})}
/>
<input
type="password"
{...register("password", { required: "비밀번호는 필수" })}
/>
<button type="submit" />
</form>
간단한 입력 페이지같은경우 위의 하나의 컴포넌트내에서 useForm
으로도 충분하지만, 실제 비즈니스 로직을 작성하는경우 복잡한 form을 다루는 경우가 많기에. useFormContext
와 FormProvider
를 사용해서 관리한다. (하위에서 값이 변경된다 하여도 전체 리렌더링 X)
import { FormProvider, useForm } from "react-hook-form";
const method = useForm<FormType>({
defaultValues: { email: "", password: "" },
});
<FormProvider {...method} >
{children}
</FormProvider>
위와 같이 선언하면, 하위 컴포넌트에서 아래와같이 사용할 수 있다.
// 타입을 명시해야 타입추론 가능
const { register } = useFormContext<FormType>();
기본적으로 react-hook-form
은 state
로 관리하는것이 아닌 ref
로 관리하므로 값이 변경되어도 리렌더링 시키지않는다.
watch
와 useWatch
를 사용하면 해당 값을 관찰하다가 리렌더링
시킬 수 있다.
import { useFormContext, useWatch } from "react-hook-form";
const { register, watch, control } = useFormContext<FormType>({
defaultValues: { email: "", password: "" },
});
const email = watch('email');
// or
const email = useWatch({ control, name: 'email' });
여기서 둘의 차이점은 watch
는 Context
내에서 사용할 경우, Context
전체를 리렌더링 시키고, useWatch
의 경우 해당 컴포넌트만 리렌더링 시킨다.
import { useFormContext } from "react-hook-form";
const { getValues, setValue } = useFormContext<FormType>();
// get
const email = getValues('email')
// set
const setEmail = (value: string) => {
setValue('email', value);
};
getValues
를 사용하면 form
에서 값을 가져올 수 있다. 하지만, 리렌더링이 되지않으므로 선언시점의 값만 가져올 수 있다.
setValue
를 사용하면 외부에서 form
의 값을 변경해줄 수 있다.
이렇게 state
가 아닌 ref
를 통해서 상태를 관리하는 컴포넌트를 비제어 컴포넌트
라고 한다. 하지만, 기존에 만들어져있는 라이브러리를 사용한다던가, 회사의 디자인시스템을 사용할 경우 해당 컴포넌트가 제어 컴포넌트
라면 Controller
를 사용해서 react-hook-form
의 기능을 사용할 수 있다.
import { useFormContext } from "react-hook-form";
const { control } = useFormContext<FormType>();
<Controller
control={control}
name="email"
render={({ field: { value, onChange } }) => (
<CustomInput value={value} onChange={onChange} />
)}
/>
type FormType = {
name: string;
friendList: {
name: string;
phoneNumber: string;
}[]
};
위와 같이 배열을 관리하는 form
을 다뤄야 할 경우 useFieldArray
훅을 사용해서 아래와같이 관리할 수 있다.
import { useFormContext, useFieldArray } from "react-hook-form";
const { control } = useFormContext<FormType>();
const { fields, append, remove } = useFieldArray({ control, name: "friendList" });
// 추가
const handleFriendAdd = () => {
append({ name: '', phoneNumber: '' });
}
// 제거
const handleFriendRemove = (idx: number) => {
remove(idx);
}
return (
<>
{fields.map((item, idx) => (
<div>
<input {...register(`friendList.${idx}.name`)} />
<input {...register(`friendList.${idx}.phoneNumber`)} />
</div>
))}
<button onClick={handleFriendAdd}>친구 추가</button>
<button onClick={handleFriendRemove}>친구 삭제</button>
</>
)
+++ Context
에서 동일한 배열에 대해 서로 다른 컴포넌트에서 useFieldArray
를 사용할 경우, append
remove
와 같은 함수들이 다른 컴포넌트에선 적용이 안된다.
(ex : 입력 컴포넌트와 view 컴포넌트가 분리되어있고, 서로 동일한 useFieldArray를 사용한 경우)
submit이 동작할 경우 미리 정의해둔 옵션에 따라서 validation check를 진행하게 되고, 통과했다면 첫번째 콜백이, 실패했다면 두번째 콜백이 실행된다. 이때 실패했을경우 formState
의 errors
가 업데이트되어 아래와같이 error 처리를 진행할 수 있다.
import { useFormContext } from "react-hook-form";
const { register, handleSubmit, formState: { errors } } = useFormContext<FormType>();
// validation 성공
const handleFormSubmit = (form: FormType) => {};
// validation 실패
const handleFormError = (errors: FieldErrors<FormType>) => {};
<form onSubmit={handleSubmit(handleFormSubmit)}>
<input {...register("name", { required: "이름은 필수입니다." })} />
{errors.name && <p>{errors.name.message}</p>}
<button type="submit" />
</form>
zod :
스키마 선언
및유효성 검사
라이브러리이다.typescript
의 유효성 검증은 컴파일시에만 발생하는데,zod
의 유효성 검증은 컴파일이 끝난 프로그램의 실행 시점에 발생한다.
$ yarn add @hookform/resolvers
$ yarn add zod
zod
를 사용해 아래와 같이 schema
를 정의하고 type
으로 나타낼 수 있다.
import { z } from 'zod';
const ManSchema = z.object({
name: z.string().min(1, { message: '이름을 추가해주세요' }),
email: z.string().email(),
age: z.number(),
phoneNumber: z.string().optional()
});
type ManType = z.infer<typeof ManSchema>
또한 schema
정의시에 validation check 부분의 에러메시지도 작성할 수 있다.
작성한 schema를 react-hook-form 선언부에서 resolver
로 전달하게 되면, submit시에 해당 schema에 정의해둔대로 validation check를 진행하게 된다.
+++ (react-hook-form에서 ...register('email', { required: '...'}) 형식으로 추가하여도 무시된다.)
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const method = useForm<ManType>({
defaultValues,
resolver: zodResolver(ManSchema),
});
react-hook-form의 submit을 하지않고도 해당 schema를 이용해 validation check를 진행해야하는 상황이 있을수도있다 (제출전 특정 케이스에서 필요할때)
try {
ManSchema.parse(form);
// ... 통과 로직 작성 ~
} catch (error) {
// 통과 X
if (error instanceof z.ZodError) {
error.errors.forEach((err) => {
// 에러 로직 작성
setError(key, { message: err.message });
});
}
}
위와같이 정의한 schema
에 parse
함수를 통해 validation check를 진행하게 되고 실패시 에러를 반환한다.
schema
정의시에 다른값과 상호작용을 통해 validation check를 진행하는 케이스에는 refine
혹은 superRefine
을 통해 아래와 같이 처리할 수 있다.
const ManSchema = z
.object({
password: z.string(),
passwordConfirm: z.string(),
})
.refine((value) => value.password === value.passwordConfirm, {
message: '비밀번호가 일치하지 않습니다',
path: ['passwordConfirm'],
});
// or
const ManSchema = z
.object({
password: z.string(),
passwordConfirm: z.string(),
})
.superRefine(({ password, passwordConfirm }, ctx) => {
if (password !== passwordConfirm) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '비밀번호가 일치하지 않습니다',
path: ['passwordConfirm'],
});
}
});
이렇게 react-hook-form
과 zod
를 사용하면 복잡한 form
관리와 그에 대한 validation check
를 좀 더 쉽고 명확하게 작성할 수 있다.