useReducer의 등장

372025년 09월 21일4

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 호출이 여섯 번이나 반복되었다. 이것만으로도 코드는 길어졌지만, 진짜 문제는 여러 상태가 서로 얽혀서 업데이트되어야 할 때 발생했다.
예를 들어, ‘제출’ 버튼을 누르면 isLoadingtrue가 되고, errornull로 초기화되어야 했다. 여러 개의 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로 체계적으로 관리할 수 있게 된 것이다.

이제 훅의 기본 무기고에는 두 종류의 강력한 무기가 더 추가되었다.
상태 관리를 위한 useStateuseReducer,
부수 효과를 위한 useEffect.

하지만 아직 풀지 못한 오래된 숙제가 하나 남아있었다. 수많은 컴포넌트를 거쳐 데이터를 전달해야 하는 고통, 바로 ‘Prop Drilling’의 문제였다. 팀의 다음 목표는 이 지루한 배관 작업을 해결할 훅을 만드는 것이었다.