React Hooks 도입 후 Custom Hook으로 폼 로직 분리하기
배경
프로젝트에 React 16.8이 도입된 지 몇 달이 지났다. 기존 클래스 컴포넌트로 작성된 회원가입 폼이 200줄을 넘어가면서 유지보수가 어려워졌다. Hooks를 활용해 리팩토링을 진행했다.
기존 문제점
- 컴포넌트 내부에 상태 관리, 검증, 제출 로직이 뒤섞임
- 비슷한 폼 로직을 다른 컴포넌트에서도 사용하는데 복사-붙여넣기로 해결
- 테스트 작성이 어려움
Custom Hook 구현
function useForm(initialValues, validate) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const handleChange = useCallback((e) => {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
}, []);
const handleSubmit = useCallback(async (onSubmit) => {
const validationErrors = validate(values);
setErrors(validationErrors);
if (Object.keys(validationErrors).length === 0) {
setIsSubmitting(true);
await onSubmit(values);
setIsSubmitting(false);
}
}, [values, validate]);
return { values, errors, isSubmitting, handleChange, handleSubmit };
}
사용 예시
function SignupForm() {
const { values, errors, handleChange, handleSubmit } = useForm(
{ email: '', password: '' },
(values) => {
const errors = {};
if (!values.email.includes('@')) errors.email = '이메일 형식이 아닙니다';
if (values.password.length < 8) errors.password = '8자 이상 입력하세요';
return errors;
}
);
const onSubmit = async (data) => {
await api.post('/signup', data);
};
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(onSubmit); }}>
<input name="email" value={values.email} onChange={handleChange} />
{errors.email && <span>{errors.email}</span>}
{/* ... */}
</form>
);
}
결과
- 컴포넌트 코드가 100줄 이하로 줄어듦
- 같은 Hook을 로그인, 프로필 수정 폼에도 재사용
- 검증 함수만 분리해서 단위 테스트 작성 가능
Hooks의 조합 가능성이 생각보다 강력하다. 앞으로 점진적으로 더 많은 컴포넌트를 마이그레이션할 예정이다.