react-hook-form을 이용한 커스텀 폼 컴포넌트, RHForm이라고 만들어보자.
유효성 검사에 관한건 hook-form을 이용하지 않고, zod를 사용한 schema기반으로 만든다.
(훅폼의 코어인 useForm에 resolver프로퍼티에 zodResolver를 이용해 연결)
FormProvider를 통해 react-hook-form의 컨텍스트를 제공해 자식 컴포넌트에서 폼 상태에 접근 가능.
import { DevTool } from "@hookform/devtools";
import { zodResolver } from "@hookform/resolvers/zod";
import { PropsWithChildren } from "react";
import {
DefaultValues,
FieldValues,
FormProvider,
SubmitHandler,
useForm,
} from "react-hook-form";
import { ZodSchema } from "zod";
//실행 시점에 타입을 정하는게 좋습니다!
type RHFormProps<T> = {
//2) 몰라 여기서 받아오셈, 근데 이건 어디서 받아옴?
schema: ZodSchema<T>; // 1) T가 뭔데?
defaultValues?: DefaultValues<T>;
};
// 3) 몰라 props로 넘길때 타입 알아서 맞춰서 주겠지, 다 돌려서 쓰셈
function RHForm<T extends FieldValues>({
children,
schema,
defaultValues,
showDevTools = import.meta.env.MODE === 'development',
onSubmit,
onError,
validateOnload = false,
mode = 'all'
}: PropsWithChildren<RHFormProps<T>>) {
const methods = useForm<T>({
defaultValues: defaultValues,
resolver: zodResolver(schema),
mode
});
useEffect(() => {
if(validateOnload){
methods.trigger()
}
},[methods, methods.trigger, validateOnload])
return (
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit, onError)}>{children}</form>
{showDevTools && <DevTool control={methods.control} />}
</FormProvider>
);
}
export default RHForm;
이 커스텀 폼에서 가장 중요한건 타입정의, 즉 제네릭을 어떻게 잘 활용했냐는것이다.
컴포넌트의 실행 시점에 타입을 정하는게 가장 좋은 관습이고 타입스크립트에서 타입 추론능력을 잘 활용할 수 있다.
T라고 명시한 제네릭을 순서대로(1,2,3) 따라가다보면 즉 타입결정권을 추상화하여 타입의 유연성을 강화해 재사용성이 높고, 안정성있는 커스텀 폼으로 사용할 수 있다.
...
return (
<RHForm<TestType> schema={schema}>
...
<Button type='submit'/>
<Button type='reset'/>
</RHForm>
)
엄격하게 타입처리를 하려면 이와 같이 조금 어색해보일수 있는 형태로 컴포넌트를 사용할 수 있다.
물론 타입스크립트의 타입 추론 능력으로 단순히 제네릭 없이도 동일하게 동작할것이지만,
위 방식처럼 제네릭으로 타입을 넘기면 더 완벽한 코드를 만들 수 있다.