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에 익숙해질 것 같다.