ReactJS에서 사용자 입력을 관리하기 위해 React Hook Form을 사용한다.
- 사용자 입력을 관리하기 위한 다양한 도구를 제공
- getter와 setter의 분리로 코드의 가독성 향상
- 유효성 검사를 JS로 관리하여 HTML 속성관련 보안 문제를 해결
아쉽게도 사용자의 입력을 받는 Input 컴포넌트가 많아질수록 React Hook Form 역시 한계를 보여준다.
이는 validation 코드와 Input 컴포넌트 및 ErrorMessage를 렌더링하는 컴포넌트가 한 컴포넌트 내에 반복적으로 작성되어 가독성이 낮아지고 유지보수의 난이도가 올라갈 수 있다.
다음은 팀원님이 회원가입 기능을 구현하시고 보내주신 PR이다.
// BE와 비동기적으로 통신하는 코드는 삭제
function Signup() {
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<SignUpForm>({
mode: 'onChange',
});
const onSubmit = async (data: SignUpForm) => {
const { email, password } = data;
//mutate
};
return (
<Wrapper>
<Container>
<Form onSubmit={handleSubmit(onSubmit)}>
<h1>SignUp</h1>
<div>
<Label>email</Label>
<Input
type="email"
placeholder="Enter your email address"
{...register('email', {
required: 'Please enter your email!',
validate: {
isAlphabet: (value) => {
const isAlphabet = value.match(/[a-zA-Z]/g);
return isAlphabet ? true : 'must be include Alphabet';
},
isEmail: (value) => {
const isEmail = value.match(
/^[\w-\\.]+@([\w-]+\.)+[\w-]{2,4}$/g
);
return isEmail ? true : 'not email format';
},
},
})}
/>
{errors.email && <Errorspan>{errors.email.message}</Errorspan>}
</div>
<div>
<Label>password</Label>
<Input
type="password"
placeholder="Enter your password"
{...register('password', {
required: 'Please enter your password!',
minLength: {
value: 8,
message: 'Requires longer than 8',
},
pattern: {
value:
/^(?!((?:[A-Za-z]+)|(?:[~!@#$%^&*()_+=]+)|(?:[0-9]+))$)[A-Za-z\d~!@#$%^&*()_+=]{8,}$/,
message: 'must be include Alphabet & number',
},
})}
/>
{errors.password && (
<Errorspan>{errors.password.message}</Errorspan>
)}
</div>
<div>
<Label>passwordConfirm</Label>
<Input
type="password"
placeholder="Enter your password"
{...register('passwordConfirm', {
required: 'Please enter your password!',
validate: {
matchesPrevios: (value) => {
const pwd = watch('password');
return value === pwd || 'Password not match';
},
},
})}
/>
{errors.passwordConfirm && (
<Errorspan>{errors.passwordConfirm.message}</Errorspan>
)}
</div>
<Submit type="submit" disabled={isLoading}>
Signup
</Submit>
</Form>
</Container>
</Wrapper>
);
}
export default Signup;
위 컴포넌트는 React Hook Form을 이용하여 구현한 회원가입이다.
유저에게 Input 받을 컴포넌트가 많아짐에 따라 발생하는 React Hook Form의 한계점을 엿볼 수 있다.
리팩터링 과정에서 분리한 Input 컴포넌트들을 가져와 InputType으로 분기처리하여 선택적으로 렌더링해주는 디자인 패턴을 사용할 수 있다.
개선 가능한 부분은 다음과 같다.
- 반복되는 단위로 모듈화 (Email, Password, PasswordConfirm)
- 휴먼 에러 방지를 위한 소프트 코딩
- 관심사에 따른 분리를 통한 확장성과 유지보수 용이성 및 가독성 확보
우선, Input 단위의 컴포넌트를 분리하자. Email과 Password를 입력하는 Input 컴포넌트는 로그인과 회원가입에서 반복적으로 사용되기 때문에, 모듈화하여 import하여 사용하는 것이 좋다.
Validation은 아래와 같이 상수화된 객체로 관리하며 소프트코딩을 적용한다.
각 Input에는 ErrorMessage와 해당 컴포넌트의 Label이 렌더링 되어야한다.
관심사에 따라 다시 나누어준다.
재사용을 위해 분리한 Input 컴포넌트들을 가져와 InputType으로 분기처리하여 optional하게 렌더링해준다.
물론, 휴먼에러가 발생할 수 있기 때문에 반드시 TypeScript를 이용해, Factory로 들어오는 type값을 체크해야 한다.
props로 받게될 ReactHookInput을 Type으로 직접 작성해줘도 좋지만, 조금 더 유지보수에 용이하고 휴먼에러로부터 안전하기 위해, 소프트코딩을 할 때 사용했던 상수를 그대로 이용하여 Type을 선언하도록 한다.
이를 import해 Type을 생성하자.
InputType에 대한 Type을 선언하였다면, 이제 Factory(ReactHookInput) 컴포넌트로 들어오는 props에 대한 타입을 선언하자.
이제 상수로 관리되는 객체에 필드명만 추가한다면 자동으로 Type이 등록되어 휴먼에러로부터 안전한 컴포넌트가 생성된다.
export function Signup() {
const { pathname } = useLocation();
const { errorHandler } = useErrorHandler({ route: pathname });
const navigate = useNavigate();
const {
register,
handleSubmit,
formState: { errors },
} = useForm<RegisterField>({
mode: 'onChange',
});
const { mutate } = useMutation(signUp, {
...QUERY.DEFAULT_CONFIG,
onSuccess: () => {
sendToast.success(TOASTIFY.SUCCESS.SIGN_UP);
navigate(ROUTE.LOGIN);
},
onError: errorHandler;
});
const onValid = async (data: RegisterField) => mutate(data);
return (
<Wrapper>
<Container>
<TopWrapper>
<Title>Sign up</Title>
<Form onSubmit={handleSubmit(onValid)}>
<ReactHookInput
type={INPUT_TYPE.EMAIL}
register={register}
errorMessage={errors.email?.message}
/>
<ReactHookInput
type={INPUT_TYPE.PASSWORD}
register={register}
errorMessage={errors.password?.message}
/>
<ReactHookInput
type={INPUT_TYPE.PASSWORD_CONFIRM}
register={register}
errorMessage={errors.passwordConfirm?.message}
/>
<Submit isValid={!Object.keys(errors)[0]}>SignUp</Submit>
<Hr />
<Text>If you already have account?</Text>
<Link to={ROUTE.LOGIN}>
<Login>Login</Login>
</Link>
</Form>
</TopWrapper>
</Container>
</Wrapper>
);
}
가독성과 유지보수성이 확보된 컴포넌트로 개선되었다. :)
해당 코드를 작성하는 시점에 관심사에 따라 코드를 분리했을 뿐, 사실 해당 디자인패턴을 알지 못했다. 최근 디자인 패턴에 대해 공부하다가 해당 패턴의 존재를 인지하게 되어 글을 쓰게 되었다.
inputType을 매개변수로 받아 렌더링할 컴포넌트를 결정하는 로직까지 분리하면 조금 더 나은 코드가 될 것 같아 분리를 시도했지만, props로 type와 register, errorMessage등의 객체를 전달하는 과정에서 과도하게 컴포넌트를 분리한다 느낌이 들었다.
따라서, inputType에 따른 컴포넌트를 return하는 factory 로직을 해당 컴포넌트 내에서 유지하기로 판단했다. 물론, 코드의 규모가 점점 커진다면 분리하는 것이 나을 것이다.
객체는 정말 아름다운 것 같다.