React Hooks로 복잡한 폼 상태 관리 리팩토링

배경

올해 초 React Hooks가 정식 출시되고 나서 몇 달간 관망하다가, 이번 분기에 본격적으로 프로젝트에 도입하기 시작했다. 첫 타겟은 회원가입 플로우의 다단계 폼 컴포넌트였다.

기존 코드는 약 400줄짜리 클래스 컴포넌트였고, componentDidMount에서 초기 데이터를 가져오고, setState로 10개 이상의 필드를 관리하고 있었다. 유효성 검증 로직도 뒤섞여 있어서 수정할 때마다 신경 쓸 게 많았다.

리팩토링 과정

1. useState에서 useReducer로

처음엔 단순히 useState로 전환했는데, 필드가 많다 보니 setter 함수가 너무 많아졌다.

const formReducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return { ...state, [action.field]: action.value };
    case 'SET_ERRORS':
      return { ...state, errors: action.errors };
    case 'RESET':
      return initialState;
    default:
      return state;
  }
};

const [formState, dispatch] = useReducer(formReducer, initialState);

2. Custom Hook 분리

폼 로직을 재사용 가능하도록 useForm이라는 custom hook으로 추출했다.

function useForm(initialValues, validate) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  const [isSubmitting, setIsSubmitting] = useState(false);

  const handleChange = (e) => {
    const { name, value } = e.target;
    setValues(prev => ({ ...prev, [name]: value }));
  };

  const handleSubmit = async (onSubmit) => {
    const validationErrors = validate(values);
    setErrors(validationErrors);
    
    if (Object.keys(validationErrors).length === 0) {
      setIsSubmitting(true);
      await onSubmit(values);
      setIsSubmitting(false);
    }
  };

  return { values, errors, isSubmitting, handleChange, handleSubmit };
}

3. useEffect로 사이드 이펙트 정리

데이터 fetching도 훨씬 명확해졌다.

useEffect(() => {
  const fetchUserData = async () => {
    const data = await api.getUserProfile();
    dispatch({ type: 'UPDATE_FIELD', field: 'email', value: data.email });
  };
  
  fetchUserData();
}, []);

결과

  • 컴포넌트 코드가 400줄에서 150줄로 감소
  • 폼 로직을 다른 곳에서도 재사용 가능
  • 단위 테스트 작성이 쉬워짐 (custom hook만 따로 테스트)

다만 팀원들 중 일부는 아직 클래스 컴포넌트에 익숙해서 코드 리뷰 때 설명이 필요했다. 내년엔 팀 전체가 Hooks에 익숙해질 것 같다.