처음 form 부분을 맡았을 당시에는 그닥 어렵지 않은 것이라 생각했다.
하지만 나의 케이스는
한 Form 안에서 다루는 input이 7개가 넘어간다. 심지어 input이 아닌것들도 form 안에 포함되어야 한다(이 부분은 나중에...) 렌더링 문제와, input의 추가에도 대응할 수 있어야 한다
validation 또한 달라질 여지가 굉장히 많았다.
최대한 프론트단에서 validation 처리를 통해 서버 요청을 최소화 해야하고, 유저의 선호도에 따라 언제든지 변경에 대응할 수 있어야 하는 부분이다
submit 시에 서버에 보내야 하는 데이터 구조와 현재 다루고 있는 데이터의 구조가 달라서 변환이 필요하다
다행히도 RHF에서 비제어 방식으로 input을 관리해주고 있고, 이 밖에도 다양한 처리를 통해 생각보다 과도한 렌더링 문제는 걱정하지 않아도 되었다.
그래서 내가 중점적으로 생각한 것은 코드의 가독성이었다.
props로 넘겨주지 않고도 상태를 공유할 수 있는 가장 간단한 방법은 Context API인데, RHF에서 useFormContext 라는 API를 통해 제공하고 있다
그래서 위의 예시처럼 useForm에서 리턴된 register 라는 메서드를 input에 바로 부착하지 않아도
//자식 컴포넌트
const {register} = useFormContext()
return <input {...register}/>
이렇게 사용이 가능하다.
const ProjectInputBox = ({
name,
label,
footer,
children,
width,
}: ProjectInputBoxProps) => {
const {
register,
formState: { errors },
} = useFormContext<ProjectFormValues>()
return (
<Flex
flexDir="column"
gap="5px"
width={width}>
<label htmlFor={name}>
<Text
{label}
</Text>
</label>
<ErrorMessage
name={name}
errors={errors}
render={({ message }) => <ErrorText message={message} />}
/>
{isValidElement(children) &&
cloneElement(children as ReactElement<InputElementProps>, {
id: name,
...register(name, projectInputRegister[name]),
})}
{footer && (
<Text
fontSize="sm"
color="grey">
{footer}
</Text>
)}
</Flex>
<ProjectInputBox
name="endDate"
label="종료일"
width="30rem">
<Input type="date" />
</ProjectInputBox>
간단하게 설명하면 name 이라는 props만 넘겨주면, 알아서 ...register를 children으로 오는 컴포넌트의 props에 등록해준다
<children value={value} ref={ref} onChange={onChange}
onBlur={onBlur}.../>
name 으로 올 수 있는 값들과 유효성과 관련된 조건들은 타입과 상수화를 해주었다
좋은 점
name props만 넘겨주면 RHF와 관련된 것들은 useFormContext 를 통해 알아서 처리된다name, projectInputRegister[name] 을 통해 휴먼 에러를 방지할 수 있다<input register('안녕',projectInputRegister['아녕'])/>
외부에서 이렇게 정의할 오류를 줄일 수 있다
단점
앞서서 말했듯이 내 Form에서는 input 말고도 다른 컴포넌트들 또한 관리하고 있어서, register 를 등록해줄 경우 (value,ref,onChange,onBlur...) 아예 오류가 발생한다
InputBox 라는 합성 컴포넌트를 사용하였고, Content 부분을 children을 통해 외부에서 주입하도록 하였다const ProjectInputBox = ({
name,
label,
footer,
children,
}: ProjectInputBoxProps) => {
return (
<InputBox
id={name}>
<InputBox.Header name={name}>
<Flex gap="5px">
<Text
fontSize="md"
as="b">
{label}
</Text>
</Flex>
</InputBox.Header>
<InputBox.Content>{children}</InputBox.Content>
{footer && <InputBox.Footer text={footer} />}
</InputBox>
)
}
이제 해당 컴포넌트 내부에서 RHF 관련 코드가 제거되었고
<ProjectInputBox
label="제목"
name="name">
<input {...register('name')} />
</ProjectInputBox>
이런 식으로 children을 통해 input + RHF register를 외부에서 주입할 수 있도록 하였다
RHF에서는 에러(유효성 검사 통과 X) 발생 시에 출력할 메시지를 따로 등록할 수 있다
...register('name',{required : '이름은 필수입니다'})
이 메시지를 받기 위해서는 errors라는 객체의 message에 접근해야 하는데
errors[name]?.message
어떻게 보면 좀 지저분? 해지는 것 같아서
RHF에서 만든 @hookform/error-message 라는 패키지에서 나온 ErrorMessage라는 컴포넌트를 사용하고 있었다
<ErrorMessage
errors={errors}
name="singleErrorInput"
render={({ message }) => <p>{message}</p>}
/>
errors 객체를 props로 전달하면, 해당 name에 해당하는 에러 발생 시 등록된 message를 렌더링해준다굉장히 편하고, 선언적으로 에러에 대한 메시지를 보여줄 수 있다는 생각에 사용하고 있었는데
이제는 Context 를 제거했기 때문에, 해당 컴포넌트를 렌더링하기 위해서는 errors 라는 객체를 props 로 받아야 했는데 뭔가 이 점이 좀 별로였다..
생각해보면 ProjectInputBox 라는 컴포넌트에서 사실 에러 객체에 대한 정보를 알 필요는 없다.
단순히 에러에 대한 메시지를 렌더링 하는 것만으로 역할은 충분히 하지 않나.. 라는 생각이 들었다
errors? : string | FieldErrors
{typeof errors === "string" ? (
<ErrorText message={errors} />
) : (
<ErrorMessage
name={name}
errors={errors}
render={({ message }) => (
<ErrorText message={message} />
)}></ErrorMessage>
)}
일단은 이런 식으로 유니온 타입을 통해 객체/단순 문자열 두개가 올 수 있도록 허용해 놓고, 좀 더 생각해 본 뒤 하나로 통일하는 방향으로 가야겠다.