React Hooks로 복잡한 폼 상태 관리하기

문제 상황

회원가입 플로우를 개선하는 작업을 맡았다. 기존 코드는 클래스 컴포넌트로 작성되어 있었고, 5단계에 걸친 폼 상태를 관리하느라 componentDidUpdate가 복잡하게 얽혀있었다.

팀에서 React Hooks 도입을 논의한 지 2개월 정도 지났고, 이번 기회에 실전 적용해보기로 했다.

초기 접근: useState만 사용

처음엔 각 필드마다 useState를 선언했다.

const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [name, setName] = useState('');
const [phone, setPhone] = useState('');
// ... 15개 필드

당연히 관리가 안 됐다. 각 필드의 validation 상태까지 추가하니 state가 30개를 넘어갔다.

useReducer로 리팩토링

복잡한 상태는 useReducer가 낫다는 글을 읽고 적용했다.

const formReducer = (state, action) => {
  switch (action.type) {
    case 'UPDATE_FIELD':
      return {
        ...state,
        [action.field]: action.value,
        errors: { ...state.errors, [action.field]: null }
      };
    case 'SET_ERRORS':
      return { ...state, errors: action.errors };
    case 'NEXT_STEP':
      return { ...state, step: state.step + 1 };
    default:
      return state;
  }
};

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

필드 업데이트, 에러 처리, 단계 이동 로직이 한 곳에 모였다.

Custom Hook으로 분리

여러 폼에서 재사용할 수 있게 useForm 훅을 만들었다.

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

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

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

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

컴포넌트는 훨씬 간결해졌고, 테스트 작성도 쉬워졌다.

결과

  • 코드 라인 수: 450줄 → 320줄
  • 재사용 가능한 로직 분리 완료
  • 클래스 컴포넌트의 this 바인딩 고민 제거

Hooks를 실전에 적용해보니 확실히 장점이 많았다. 다음엔 useEffect를 활용한 API 호출 패턴을 정리해봐야겠다.

React Hooks로 복잡한 폼 상태 관리하기