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 호출 패턴을 정리해봐야겠다.