useState
는 단순한 상태를 다루는 데에는 완벽했다. 숫자, 문자열, 혹은 참/거짓 값을 관리하기에는 더할 나위 없이 편리했다.
하지만 애플리케이션이 복잡해지면서, 상태 역시 복잡한 구조를 띠기 시작했다.
한 개발자가 여러 개의 입력 필드를 가진 회원가입 폼을 만들고 있었다. 이름, 이메일, 비밀번호, 비밀번호 확인… 각각의 필드는 자신만의 상태를 필요로 했다.
function SignUpForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [passwordConfirm, setPasswordConfirm] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// ... 수많은 핸들러 함수들 ...
}
useState
호출이 여섯 번이나 반복되었다. 이것만으로도 코드는 길어졌지만, 진짜 문제는 여러 상태가 서로 얽혀서 업데이트되어야 할 때 발생했다.
예를 들어, ‘제출’ 버튼을 누르면 isLoading
은 true
가 되고, error
는 null
로 초기화되어야 했다. 여러 개의 set
함수를 연달아 호출하는 것은 번거로웠고, 로직이 흩어져 있어 관리하기 어려웠다.
이 모습을 지켜보던 댄의 머릿속에는 너무나도 익숙한 패턴이 떠올랐다.
그가 수년 전, Redux를 만들 때 사용했던 바로 그 패턴.
‘리듀서(Reducer)’.
리듀서는 (previousState, action) => newState
형태를 가진 순수 함수다. 현재 상태와 ‘액션’이라는 이름의 행동 지침을 받아, 다음 상태를 계산하여 반환하는 역할이었다. 모든 상태 변경 로직이 이 리듀서 함수 하나에 중앙 집중화되는 것이 핵심이었다.
“만약… useState
와 비슷한데, 리듀서 함수를 사용해서 상태를 관리하는 훅이 있다면 어떨까요?”
댄의 제안에 팀은 즉시 동의했다. 그것은 복잡한 상태 관리를 위한 자연스러운 다음 단계처럼 느껴졌다.
그렇게 useReducer
가 탄생했다.
useReducer
는 두 개의 인자를 받았다. 첫 번째는 리듀서 함수, 두 번째는 상태의 초기값.
그리고 useState
처럼 두 개의 값을 배열로 반환했다. 첫 번째는 현재 상태 값, 두 번째는 상태 변경을 촉발하는 dispatch
함수였다.
회원가입 폼은 이제 useReducer
를 사용하여 다음과 같이 리팩토링될 수 있었다.
const initialState = {
name: '',
email: '',
password: '',
// ...
isLoading: false,
error: null,
};
function reducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return { ...state, [action.field]: action.value };
case 'SUBMIT':
return { ...state, isLoading: true, error: null };
// ... 다른 케이스들 ...
default:
throw new Error();
}
}
function SignUpForm() {
const [state, dispatch] = useReducer(reducer, initialState);
const handleSubmit = () => {
dispatch({ type: 'SUBMIT' });
// ... API 호출 로직 ...
};
// ...
}
변화는 극적이었다.
여러 줄에 걸쳐 흩어져 있던 useState
호출이 단 한 줄의 useReducer
로 통합되었다.
상태를 변경하는 모든 복잡한 로직은 reducer
라는 순수 함수 안에 깔끔하게 정리되었다. 컴포넌트는 이제 상태가 ‘어떻게’ 변하는지는 신경 쓰지 않고, 그저 dispatch({ type: '...' })
를 통해 ‘무엇을 할지’만 지시하면 되었다.
이것은 댄에게 특별한 의미가 있는 작업이었다.
그는 자신이 과거에 만들었던 Redux의 핵심 철학—상태는 예측 가능하게 관리되어야 한다는—을, 훅이라는 새로운 패러다임 안에 훨씬 더 가볍고 단순한 형태로 이식하는 데 성공한 것이다.
개발자들은 이제 선택권을 갖게 되었다.
단순한 상태는 useState
로 간편하게,
여러 상태가 얽힌 복잡한 로직은 useReducer
로 체계적으로 관리할 수 있게 된 것이다.
이제 훅의 기본 무기고에는 두 종류의 강력한 무기가 더 추가되었다.
상태 관리를 위한 useState
와 useReducer
,
부수 효과를 위한 useEffect
.
하지만 아직 풀지 못한 오래된 숙제가 하나 남아있었다. 수많은 컴포넌트를 거쳐 데이터를 전달해야 하는 고통, 바로 ‘Prop Drilling’의 문제였다. 팀의 다음 목표는 이 지루한 배관 작업을 해결할 훅을 만드는 것이었다.