기존 폼에서는 section 태그와 라벨 구조가 반복되어서, 이를 공통으로 사용하기 위해 FormSection 컴포넌트를 구현하여 사용했습니다.
/**
* 폼 섹션 컴포넌트
* @param required 필수 입력 여부
* @param title 섹션 제목
*/
function FormSection({ required, title, children }: FormSectionProps) {
return (
<section>
<Label
required={required}
className="font-extrabold"
>
{title}
</Label>
{children}
</section>
);
}
inModal 등), 컴포넌트가 비대해지는 문제가 있었습니다.HelpText를 붙여야 하는 변경이 생길 경우에도 유연하게 대응하기 어려운 구조였습니다.이를 개선하기 위해 FormField를 도입하고, Label, Input, HelpText, Error를 컴포지트 컴포넌트 패턴으로 분리했습니다.
// Context를 통한 상태 공유
interface FormFieldContextValue {
id: string;
error?: string;
required?: boolean;
}
const FormFieldContext = createContext<FormFieldContextValue | null>(null);
// Root 컴포넌트
function FormFieldRoot({ children, error, required, className }: FormFieldRootProps) {
const id = useId();
const classNames = cn('flex flex-col', className);
return (
<FormFieldContext.Provider value={{ id, error, required }}>
<section className={classNames}>{children}</section>
</FormFieldContext.Provider>
);
}
// Label 컴포넌트 (Context에서 id, required 자동 연결)
function FormFieldLabel({ children, className, size }: FormFieldLabelProps) {
const { id, required } = useFormFieldContext();
return (
<Label htmlFor={id} required={required} size={size} className={className}>
{children}
</Label>
);
}
// Input 컴포넌트 (Context에서 id, error 자동 연결 + ARIA 속성)
function FormFieldInput(props: FormFieldInputProps) {
const { id, error } = useFormFieldContext();
return (
<Input
{...props}
id={id}
aria-invalid={error ? 'true' : 'false'}
aria-describedby={error ? `${id}-error` : undefined}
/>
);
}
// HelpText 컴포넌트
function FormFieldHelpText({ children, className }: FormFieldHelpTextProps) {
const { id } = useFormFieldContext();
return (
<HelpText id={`${id}-help`} className={className}>
{children}
</HelpText>
);
}
// Error 컴포넌트 (role="alert" 자동 적용)
function FormFieldError({ children, className = '' }: FormFieldErrorProps) {
const { id, error } = useFormFieldContext();
if (!error && !children) return null;
return (
<p id={`${id}-error`} role="alert" className={`text-error text-sm ${className}`}>
{children || error}
</p>
);
}
// Object.assign으로 컴포지트 패턴 구현
export const FormField = Object.assign(FormFieldRoot, {
Label: FormFieldLabel,
Input: FormFieldInput,
HelpText: FormFieldHelpText,
Error: FormFieldError,
});