//invitationFormType.type.ts
export type InvitationFormType = {
gallery: GalleryType;
type: 'scroll' | 'slide';
moodPreset: MoodPresetType;
mainView: DecorateImageType;
bgColor: ColorType;
stickers: StickerType[];
imgRatio: ImageRatioType;
personalInfo: PersonalInfoType;
greetingMessage: GreetingMessageType;
weddingInfo: WeddingInfoType;
account: AccountInfoType;
navigationDetail: NavigationDetailType;
guestbook: boolean;
attendance: boolean;
dDay: boolean;
mainPhotoInfo: MainPhotoType;
isPrivate: boolean;
renderOrder: OrderItem[];
fontInfo: FontInfoType;
};
모바일 청첩장 제작 프로젝트를 진행하며, 사용자 맞춤형 기능 제공을 위해 다수의 입력 필드를 포함해야 한다는 점이 초기 기획의 중요한 부분이었다. 청첩장 제작에는 이미지, 텍스트, 색상, 계좌 정보 등 다양한 데이터가 필요하며, 이 모든 입력 필드를 하나의 폼으로 효율적으로 관리해야 했다.
특히 입력 필드의 개수가 많아질수록 상태 관리와 성능 최적화가 핵심 과제로 떠올랐다. 개별 입력 필드를 각각 다른 state로 관리하면 코드가 복잡해지고 유지보수가 어려울 뿐만 아니라, 상태 변화로 인한 불필요한 리렌더링 문제가 발생할 가능성이 높았다.
이러한 이유로 프로젝트 초기에 두 가지 기술적 접근법을 검토했다. 하나는 상태 관리 라이브러리인 Zustand를 활용해 폼 상태를 중앙 집중식으로 관리하는 것이었고, 다른 하나는 React Hook Form을 사용해 입력 필드 상태와 유효성 검사를 통합적으로 관리하는 방식이었다.
19개의 입력 필드를 효과적으로 제어하기 위해 각각의 기술이 제공하는 장단점을 분석했다.
Zustand는 경량 상태 관리 라이브러리로, 전역 및 로컬 상태 관리를 유연하게 처리할 수 있는 도구다.
React Hook Form은 폼 상태와 유효성 검사를 통합적으로 관리할 수 있는 도구로, 입력 필드가 많은 폼에서 효율성을 극대화할 수 있다.
두 기술 모두 강력한 장점을 가지고 있었지만, React Hook Form이 프로젝트의 요구 사항에 더 적합하다고 판단했다.
유효성 검사와 상태 관리의 통합
입력 필드가 많아질수록 유효성 검사 로직이 복잡해지기 때문에, 이를 지원하는 React Hook Form은 개발 시간을 줄이고 유지보수를 단순화하는 데 유리했다.
최소화된 리렌더링
Zustand는 해당 상태를 사용하는 컴포넌트가 리렌더링되지만, React Hook Form은 상태 변화가 발생한 입력 필드만 리렌더링하기 때문에, 다수의 입력 필드가 포함된 폼에서도 성능이 뛰어나다.
타입 안정성
TypeScript를 적극적으로 활용하는 프로젝트 특성상, React Hook Form의 타입 기반 관리와 Zod를 활용한 스키마 검증이 안정적이고 일관된 데이터 관리를 가능하게 했다.
간단한 통합
React Hook Form은 선언적인 방식으로 입력 필드를 등록할 수 있어 기존 UI 컴포넌트에 쉽게 적용할 수 있었다.
pnpm install react-hook-form
폼 상태를 여러 컴포넌트에 제공하려면 FormProvider
를 사용해 폼 상태를 전역적으로 감싸주어야 한다. 이렇게 하면 하위 컴포넌트들이 useFormContext
를 사용하여 폼 상태에 접근할 수 있다.
const CreateCardPage = () => {
const methods = useForm<InvitationFormType>({
mode: 'onChange',
defaultValues: INVITATION_DEFAULT_VALUE,
resolver: zodResolver(validationSchema)
});
const onSubmit = (data: InvitationFormType) => {
console.log(data);
};
return (
<FormProvider {...methods}>
<Components/>
</FormProvider>
);
}
하위 컴포넌트에서 useFormContext
를 사용하여 상위에서 제공한 form 상태를 참조하여 값을 가져오거나 수정할 수 있다.
const AccountInput = () => {
const [accountType, setAccountType] = useState<'groom' | 'bride'>('groom');
const { register, control, setValue, watch } = useFormContext();
const { fields: groomFields } = useFieldArray({
control,
name: 'account.groom',
});
const { fields: brideFields } = useFieldArray({
control,
name: 'account.bride',
});
return (
<div className='flex-col-center text-sm gap-4 w-full'>
<div className='flex gap-3 h-[32px] w-full'>
<label className='self-center w-[50px]'>제목</label>
<input
className='px-[8px] w-full rounded-md'
{...register('account.title')}
placeholder='신랑 & 신부에게 마음 전하기'
maxLength={20}
/>
</div>
<div className='flex gap-3 h-[32px] w-full'>
<label className='self-center w-[50px]'>내용</label>
<input
className='px-[8px] w-full rounded-md'
{...register('account.content')}
placeholder='축복의 의미로 축의금을 전달해보세요.'
maxLength={20}
/>
</div>
</div>
);
};
export default AccountInput;
useFormContext
를 사용해도 성능 저하 없이 폼 상태를 관리할 수 있다.useFormContext
를 통해 에러 상태를 중앙에서 관리할 수 있다.zod를 통해 유효성 검사 schema를 만든 뒤 useForm을 선언할 때 resolver로 해당 schema를 넣어주면 유효성 검사를 쉽게 할 수 있다.
import { z } from 'zod';
export const validationSchema = z.object({
bgColor: z.object({
r: z.number(),
g: z.number(),
b: z.number(),
a: z.number(),
name: z.string(),
}),
...
dDay: z.boolean().default(true),
mainView: z.object({
name: z.string(),
type: z.string(),
}),
isPrivate: z.boolean().default(false),
renderOrder: z.any(),
});
export type validationFormType = z.infer<typeof validationSchema>;
useWatch
를 통해서 현재 form에서 관리하는 데이터를 실시간으로 관찰할 수 있다. 청첩장 제작 페이지에서 현재 입력에 따른 미리보기를 제공하고 있기 때문에 실시간으로 값을 추적하여 바로 사용자에게 변경사항을 보여준다.
const AccountPreView = ({ control }: { control: Control<InvitationFormType> }) => {
const [account, fontInfo] = useWatch({
control,
name: ['account', 'fontInfo'],
});
return (
<Account
account={account}
fontInfo={fontInfo}
/>
);
};
export default AccountPreView;
하지만 useWatch
를 사용하기 위해서는 사용하려는 컴포넌트에 useForm
객체의 control
을 props로 넣어주어야 한다.
<AccountPreView control={methods.control} key='accountPreview'/>
React Hook Form을 도입한 후, 폼 관리를 간편하고 효율적으로 할 수 있었다. form field를 리렌더링 최적화하고, 유효성 검사, useFormContext
를 통한 여러 컴포넌트에서의 form 관리 등의 복잡한 로직을 손쉽게 처리할 수 있게 되었다. useForm
과 useFormContext
의 기능 덕분에 form의 복잡도가 높아져도 편리한 컴포넌트 분리와 코드의 일관성 유지 및 성능 저하를 최소화할 수 있게 되었다.
이야... 이게 도전팀이구나...