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의 조합 가능성이 생각보다 강력하다. 앞으로 점진적으로 더 많은 컴포넌트를 마이그레이션할 예정이다.

React Hooks 도입 후 Custom Hook으로 폼 로직 분리하기