“이럴 때 필요한 것이 바로 useReducer입니다.”
댄은 useState의 한계로 고통받던 개발팀에게, 새로운 해결책을 제시했다. 그는 useReducer가 어떻게 흩어진 상태와 로직을 우아하게 통합하는지 보여주기 시작했다.
그의 첫 번째 단계는, 여러 개의 useState로 흩어져 있던 상태들을 단 하나의 객체로 모으는 것이었다.
const initialState = {
name: '',
email: '',
bio: '',
emailNotifications: true,
pushNotifications: false,
profileVisibility: 'public',
isLoading: false,
error: null,
isSaved: false,
};
두 번째 단계는, 이 거대한 상태 객체를 변경하는 모든 로직을 담을 ‘지휘자’, 즉 리듀서(reducer) 함수를 작성하는 것이었다. 리듀서 함수는 현재 상태(state)와 행동 지침(action)을 받아, 새로운 상태를 반환하는 순수 함수였다.
function settingsReducer(state, action) {
switch (action.type) {
case 'SET_FIELD':
return { ...state, [action.field]: action.value };
case 'RESET':
return initialState;
case 'SAVE_START':
return { ...state, isLoading: true, error: null, isSaved: false };
case 'SAVE_SUCCESS':
return { ...state, isLoading: false, isSaved: true };
case 'SAVE_ERROR':
return { ...state, isLoading: false, error: action.payload };
default:
return state;
}
}
상태를 변경하는 모든 규칙이 이 settingsReducer 함수 안에 명확하게 정의되었다. ‘필드 값 설정’, ‘초기화’, ‘저장 시작’, ‘저장 성공’, ‘저장 실패’ 등 모든 시나리오에 대한 상태 변화가 한 곳에 모여 있었다.
마지막으로, 그는 SettingsForm 컴포넌트 내부를 useReducer를 사용해 극적으로 단순화했다.
function SettingsForm() {
const [state, dispatch] = useReducer(settingsReducer, initialState);
const handleReset = () => {
dispatch({ type: 'RESET' });
};
const handleSave = () => {
dispatch({ type: 'SAVE_START' });
saveSettingsAPI(state)
.then(() => dispatch({ type: 'SAVE_SUCCESS' }))
.catch((err) => dispatch({ type: 'SAVE_ERROR', payload: err }));
};
// ... 렌더링 로직은 state.name, state.email 등을 사용 ...
}
결과는 놀라웠다.
컴포넌트 상단을 가득 메우던 열 개가 넘는 useState 호출이, 단 한 줄의 useReducer로 대체되었다.
handleReset과 handleSave 함수는 더 이상 여러 개의 set 함수를 호출하며 복잡한 로직을 수행하지 않았다. 그저 dispatch를 통해 ‘무슨 일이 일어났는지’를 설명하는 액션 객체를 보낼 뿐이었다.
컴포넌트의 책임이 명확하게 분리되었다.
- 컴포넌트(
SettingsForm): 사용자 인터랙션을 감지하고, 어떤 행동(action)이 일어났는지를dispatch를 통해 알린다. UI를 렌더링한다. - 리듀서(
settingsReducer): 행동(action)에 따라 상태(state)가 ‘어떻게’ 변해야 하는지에 대한 모든 로직을 책임진다.
개발자들은 이제 더 이상 컴포넌트 코드 안에서 복잡한 상태 업데이트 로직을 찾아 헤맬 필요가 없었다. 모든 비즈니스 로직은 settingsReducer 함수 안에 체계적으로 정리되어 있었다. 새로운 상태나 로직을 추가하는 것도 훨씬 쉬워졌다.
useReducer는 복잡한 상태 관리의 세계에 질서를 가져왔다. 그것은 useState의 대체재가 아니라, 더 복잡한 문제를 풀기 위한 상위 호환 도구였다.
댄은 자신이 Redux에서 구현했던 철학이, 이제는 리액트의 내장 기능으로 훨씬 더 가볍고 자연스럽게 녹아든 것에 깊은 만족감을 느꼈다.
훅의 무기고는 점점 더 견고해지고 있었다.
하지만 개발자들의 여정은 아직 끝나지 않았다. 그들은 이제 성능이라는, 또 다른 종류의 미묘하고도 중요한 문제와 마주하기 시작했다. 불필요한 리렌더링이라는 보이지 않는 적이, 그들의 애플리케이션을 서서히 느리게 만들고 있었다.


