icon
7장 : 상태 관리 입문

useReducer 훅 소개


리액트에서 제공하는 또 다른 강력한 훅인 useReducer 대해 알아보겠습니다.

useReduceruseState와 유사하게 상태를 관리하지만, 특히 복잡한 상태 로직을 다루거나 다음 상태가 이전 상태에 의존적인 경우, 그리고 여러 개의 하위 값으로 구성된 상태 객체를 다룰 때 더욱 효과적입니다. 또한, Context API와 함께 사용하여 전역 상태 관리에 활용되기도 합니다.


useState vs useReducer

useState는 상태의 값을 직접 설정하는 반면, useReducer는 상태를 업데이트하는 로직을 reducer 함수라는 곳으로 분리하여 관리합니다. 이는 Redux와 같은 상태 관리 라이브러리의 핵심 개념과도 유사합니다.

특징useStateuseReducer
사용법[state, setState][state, dispatch]
업데이트 방식setState(newValue) 또는 setState(prev => ...)dispatch({ type: 'ACTION_TYPE', payload: ... })
상태 로직컴포넌트 내부에 직접 작성reducer 함수로 분리하여 관리
적합한 경우- 단순한 값 (숫자, 문자열, 불리언)
- 단일 상태
- 상태 업데이트 로직이 간단할 때
- 복잡한 상태 로직
- 여러 하위 값으로 구성된 상태
- 다음 상태가 이전 상태에 의존적일 때
- 상태 업데이트 로직을 재사용하고 싶을 때
- Context API와 함께 전역 상태 관리
장점간단하고 직관적- 상태 로직의 분리로 가독성 및 유지보수성 향상
- 테스트 용이
- 상태 변경의 예측 가능성 (Redux와 유사)

useReducer의 기본 사용법

useReducer 훅은 두 개의 인자를 받고, 두 개의 값을 반환합니다.

const [state, dispatch] = useReducer(reducer, initialState, init);
  • reducer (함수)

    • 현재 stateaction 객체를 인자로 받아 새로운 상태(new state) 를 반환하는 순수 함수입니다.
    • reducer(state, action) 형태를 가집니다.
    • action은 일반적으로 { type: '액션명', payload: '데이터' } 형태의 객체입니다.
    • reducer 함수 내부에서는 직접 state를 변경(mutate)하지 않고, 항상 새로운 state 객체를 반환해야 합니다.
  • initialState (값)

    • state초기값입니다. 어떤 타입이든 될 수 있습니다.
  • init (함수, 선택 사항)

    • 초기 상태를 지연(lazy)하여 생성할 때 사용하는 함수입니다. init(initialArg) 형태를 가집니다.
    • init 함수가 제공되면 initialStateinit 함수의 인자로 사용되고, init 함수의 반환값이 초기 상태가 됩니다.
    • 초기 상태 계산 비용이 높을 때 유용합니다.
  • state (값)

    • 현재 상태 값입니다. useStatestate와 동일합니다.
  • dispatch (함수)

    • 상태 업데이트를 "요청"하는 함수입니다. action 객체를 인자로 받습니다.
    • dispatch(action)을 호출하면 React는 reducer 함수를 실행하여 새로운 상태를 계산하고 컴포넌트를 재렌더링합니다.

카운터 예제를 통한 이해

가장 간단한 카운터 예제를 통해 useReducer의 작동 방식을 알아봅시다.

src/components/CounterWithReducer.js
import React, { useReducer } from 'react';

// 1. reducer 함수 정의
// 이 함수는 현재 상태(state)와 발생한 액션(action)을 받아서 새로운 상태를 반환합니다.
function counterReducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    case 'RESET':
      return { count: 0 }; // action.payload를 사용한다면: return { count: action.payload };
    case 'ADD_BY_VALUE':
        return { count: state.count + action.payload }; // payload 사용 예시
    default:
      // 알 수 없는 액션 타입이 들어오면 현재 상태를 그대로 반환하거나 에러를 던질 수 있습니다.
      throw new Error(`Unsupported action type: ${action.type}`);
  }
}

function CounterWithReducer() {
  // 2. useReducer 훅 사용
  // useReducer(reducer 함수, 초기 상태 값)
  const [state, dispatch] = useReducer(counterReducer, { count: 0 });

  return (
    <div style={{ textAlign: 'center', padding: '20px', border: '1px solid #ddd', borderRadius: '8px', maxWidth: '400px', margin: '20px auto', backgroundColor: '#f9f9f9' }}>
      <h2 style={{ color: '#2c3e50' }}>Reducer 카운터</h2>
      <p style={{ fontSize: '2em', margin: '20px 0', color: '#3498db' }}>현재 카운트: {state.count}</p>
      <div style={{ display: 'flex', justifyContent: 'center', gap: '10px' }}>
        <button
          onClick={() => dispatch({ type: 'INCREMENT' })} // 액션 객체를 dispatch 함수에 전달
          style={{ padding: '10px 20px', fontSize: '1em', backgroundColor: '#2ecc71', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
        >
          증가 (+)
        </button>
        <button
          onClick={() => dispatch({ type: 'DECREMENT' })}
          style={{ padding: '10px 20px', fontSize: '1em', backgroundColor: '#e74c3c', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
        >
          감소 (-)
        </button>
        <button
          onClick={() => dispatch({ type: 'RESET' })}
          style={{ padding: '10px 20px', fontSize: '1em', backgroundColor: '#95a5a6', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
        >
          초기화
        </button>
      </div>
      <div style={{ marginTop: '20px' }}>
         <button
            onClick={() => dispatch({ type: 'ADD_BY_VALUE', payload: 5 })}
            style={{ padding: '10px 20px', fontSize: '1em', backgroundColor: '#f39c12', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
         >
            5 추가
         </button>
      </div>
    </div>
  );
}

export default CounterWithReducer;

App.js에 추가

src/App.js (일부)
import React from 'react';
import CounterWithReducer from './components/CounterWithReducer'; // CounterWithReducer 임포트

function App() {
  return (
    <div className="App">
      {/* 다른 라우팅 코드 등 */}
      <CounterWithReducer /> {/* 컴포넌트 추가 */}
    </div>
  );
}

실행 및 확인: CounterWithReducer 컴포넌트를 App.js에 추가하고 실행하면, 버튼을 클릭할 때마다 dispatch 함수가 호출되고, reducer 함수가 새로운 상태를 계산하여 UI를 업데이트하는 것을 볼 수 있습니다.


useReducer의 장점

복잡한 상태 로직 관리: 여러 개의 하위 상태를 가진 객체나, 다음 상태가 이전 상태에 따라 결정되는 복잡한 상태 전환 로직을 깔끔하게 분리하여 관리할 수 있습니다. useState의 경우 setState 내부에 복잡한 로직이 들어가기 쉽습니다.

재사용성: reducer 함수는 순수 함수이므로, 여러 컴포넌트에서 동일한 상태 로직을 재사용할 수 있습니다. (예: 여러 카운터 컴포넌트가 동일한 counterReducer를 사용할 수 있음)

테스트 용이성: reducer 함수는 독립적인 순수 함수이므로, UI와 분리하여 단위 테스트하기 매우 용이합니다.

성능 최적화: dispatch 함수는 한 번 생성되면 변경되지 않습니다. 따라서 dispatch 함수를 자식 컴포넌트에 props로 전달해도 불필요한 재렌더링을 유발하지 않습니다. (이 점이 useStatesetState 함수와 동일합니다.)

협업 효율성: 상태 로직이 reducer로 추상화되어 있어, 다른 개발자가 코드를 이해하고 수정하기 더 용이합니다.


useReducer와 Context API의 결합

useReducer는 컴포넌트 내부의 복잡한 상태 로직을 관리하는 데 뛰어나지만, 이 상태를 여러 컴포넌트에 걸쳐 공유하려면 여전히 프롭스 드릴링 문제가 발생할 수 있습니다. 이때 Context APIuseReducer를 함께 사용하면 전역적인 복잡한 상태를 효율적으로 관리할 수 있습니다.

아이디어

Context API를 사용하여 statedispatch 함수를 제공합니다.

useReducer 훅을 사용하여 statedispatch 함수를 생성합니다.

Context.Providervalue로 이 statedispatch를 전달합니다.

하위 컴포넌트에서는 useContext 훅을 사용하여 statedispatch를 직접 가져와 사용합니다.

이 조합은 Redux와 같은 전역 상태 관리 라이브러리의 경량 버전처럼 작동할 수 있으며, 다음 장에서 더 자세히 다루겠습니다.


"useReducer 훅 소개"는 여기까지입니다. 이 장에서는 useReducer 훅의 개념, useState와의 차이점, reducer 함수, dispatch 함수, 그리고 initialState의 역할을 카운터 예제를 통해 상세하게 배웠습니다. 또한 useReducer가 복잡한 상태 로직 관리에 왜 효과적인지 그 장점들을 알아보았습니다.

이제 여러분은 리액트에서 복잡한 상태를 더 체계적으로 관리할 수 있는 도구를 하나 더 얻게 되었습니다. 다음 장에서는 오늘 배운 useReducer와 지난 장에서 배운 Context API를 함께 사용하여 진정한 의미의 전역 상태 관리 시스템을 구축하는 방법을 알아보겠습니다.