본문으로 건너뛰기

안동민 개발노트

본문 시작

useReducer 훅 소개

useReducer의 state·action·dispatch 흐름으로 여러 상태 전이를 한곳에 모으고 Context와 결합하는 방식을 익힙니다.

리액트에서 여러 상태 전이를 한곳에서 관리할 때 사용하는 훅인 useReducer를 다룹니다.

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

또한 Context API와 함께 사용해 전역 상태 관리에 활용되기도 합니다.

useReducer는 사건을 다음 상태로 번역한다

컴포넌트는 “무슨 일이 일어났는지” dispatch하고, reducer 한곳이 “그 사건 뒤 상태가 무엇인지” 결정한다.

  1. 의도 발생

    UI EVENT 의도 발생 사용자가 증가 버튼을 누른다. onClick

  2. 사건 전달

    DISPATCH 사건 전달 다음 값을 직접 정하지 않고 사건을 보낸다. { type: 'increment' }

  3. 순수 계산

    REDUCER 순수 계산 현재 상태와 action만으로 다음 상태를 반환한다. 1 + increment → 2

  4. 일관된 결과

    NEXT RENDER 일관된 결과 React가 반환값을 다음 렌더의 state로 사용한다. count = 2

  5. Reducer 안

    불변 갱신 · 전이 규칙 · 같은 입력에 같은 출력

  6. Reducer 밖

    API 호출 · 타이머 · 저장소 접근은 이벤트 핸들러나 Effect가 담당


지역 상태와 useReducer 비교

useReducer와 Context는 상태 계산과 공급 책임을 나눠 맡는다

reducer는 action으로 다음 state를 계산하고 Provider는 state와 dispatch를 하위 컴포넌트에 공급한다.

  1. 1
    핵심 1

    useReducer와 Context는 상태 계산과 공급 책임을 나눠 맡는다

  2. 2
    핵심 2

    useReducer와 Context는 상태 계산과 공급 책임을 나눠 맡는다

  3. 3
    핵심 3

    reducer는 action으로 다음 state를 계산하고 Provider는 state와 dispatch를 하위 컴포넌트에 공급한다.

  4. 4
    핵심 4

    Reducer state + action -> next state Provider state, dispatch 공급 Consumer state 읽기…

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' })}
          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를 업데이트하는 것을 볼 수 있습니다.

카운터 예제는 작지만 reducer 사고방식을 익히기에 좋습니다.

버튼은 상태를 직접 바꾸지 않고 action을 보내며, reducer는 이전 상태와 action만 보고 다음 상태를 계산합니다.

카운터 action은 이벤트 의도를 reducer 입력으로 바꾼다

버튼은 숫자를 직접 바꾸지 않고 increment, decrement, reset 같은 action을 dispatch한다.

  1. 사용자 의도

    Click 사용자 의도 +, -, reset

  2. type/payload

    Action type/payload 무슨 일이 났는지 기록

  3. next count 계산

    Reducer next count 계산 switch 또는 map

  4. 새 count 표시

    Render 새 count 표시 state 반영


useReducer의 장점

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

useState의 경우 setState 내부에 복잡한 로직이 들어가기 쉽습니다.

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

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

성능 최적화: dispatch 함수는 한 번 생성되면 변경되지 않습니다.

따라서 dispatch 함수를 자식 컴포넌트에 props로 전달해도 불필요한 재렌더링을 유발하지 않습니다. (이 점이 useStatesetState 함수와 동일합니다.)

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

useReducer를 선택할 때는 상태 개수보다 상태 전환 규칙의 복잡도를 먼저 봐야 합니다.

아래 다이어그램은 useState로 충분한 경우와 reducer로 분리하면 좋은 경우를 기준별로 정리합니다.

useReducer로 옮길 상태 로직 선별하기

값 하나를 바꾸는 일은 `useState`가 빠르지만, 액션에 따라 여러 필드가 함께 바뀌면 reducer로 규칙을 한곳에 모으는 편이 읽기 쉽습니다.

  1. useState
    단순 토글과 숫자 변경

    열림 여부, 카운트처럼 변경 규칙이 짧으면 setter가 더 직관적입니다.

  2. reducer
    액션별 분기가 늘어난다

    증가, 감소, 초기화, 값 더하기처럼 같은 상태에 여러 명령이 붙으면 `switch` 구조가 유리합니다.

  3. 이전 상태
    이전 상태 의존성

    여러 필드를 동시에 계산해야 하면 순수 함수 안에서 이전 상태를 기준으로 새 객체를 반환합니다.

  4. 테스트
    UI 없이 규칙 검증

    reducer는 입력 상태와 action만으로 결과를 확인할 수 있어 단위 테스트에 잘 맞습니다.


useReducer와 Context API의 결합

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

이때 Context APIuseReducer를 함께 사용하면 전역적인 복잡한 상태를 효율적으로 관리할 수 있습니다.

아이디어

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

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

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

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

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

이때 중요한 점은 Context와 reducer의 책임을 섞지 않는 것입니다.

Context는 상태와 dispatch를 전달하는 통로이고, reducer는 action을 해석해 다음 상태를 계산하는 규칙이므로, 아래처럼 역할을 나누어 보면 구조가 훨씬 선명해집니다.

Context는 전달 범위, reducer는 변경 규칙을 맡는다

Context와 reducer를 같은 것으로 보면 구조가 흐려진다. 하나는 어디까지 공급할지, 다른 하나는 어떻게 바꿀지를 정한다.

  1. 1
    Context

    reducer를 같은 것으로 보면 구조가 흐려진다.

  2. 2
    하나

    어디까지 공급할지, 다른 하나는 어떻게 바꿀지를 정한다.


"useReducer 훅 소개"는 여기까지입니다.

이 장에서는 useReducer 훅의 개념, useState와의 차이점, reducer 함수, dispatch 함수, 그리고 initialState의 역할을 카운터 예제를 통해 상세하게 배웠습니다.

또한 useReducer가 복잡한 상태 로직 관리에 왜 효과적인지 그 장점들을 알아보았습니다.

useReducer를 사용하면 리액트에서 복잡한 상태 전이를 더 명시적으로 관리할 수 있습니다.

다음 절에서는 useReducer와 Context API를 함께 사용해 전역 상태 관리 구조를 만들어 봅니다.


useReducer는 상태 변경 이유를 action으로 드러내므로 복잡한 전이를 한곳에서 검토하기 좋습니다.

useReducer action 상태 전이

상태 필드가 서로 연결되고 업데이트 규칙이 많아지면 setState 여러 개보다 reducer와 dispatch가 흐름을 읽기 쉽습니다.

  1. 1
    Redux 전환 기준

    init / dispatch / immutability

  2. 2
    state shape count, text

    items처럼 서로 영향을 주는 값을 하나의 상태 객체로 관리합니다.

  3. 3
    action type 사용자 의도

    ADD, REMOVE, RESET 같은 action으로 표현합니다.

  4. 4
    pure reducer

    같은 state와 action에 대해 같은 다음 state만 반환합니다.

  5. 5
    init 초기 상태 구조

    화면 요구와 맞는지 확인합니다.

  6. 6
    dispatch 이벤트 핸들러

    계산보다 action 전달에 집중합니다.

  7. 7
    immutability 배열

    객체는 복사해 새 상태를 반환합니다.

  8. 8
    context 여러 컴포넌트

    dispatch하면 Context와 결합합니다.

지역 상태와 useReducer 비교와 useReducer의 기본 사용법 중심으로 정리한 보조 다이어그램입니다.

useReducer 상태 전이 계약

이벤트 핸들러는 무엇이 일어났는지를 dispatch하고, reducer는 현재 상태와 최소 action 정보만으로 다음 상태를 계산합니다.

  1. 1
    사용자 상호작용

    클릭 · 입력 · 응답 도착

  2. 2
    action 전달

    type + 필요한 최소 payload

  3. 3
    순수 계산

    reducer(prev, action)

  4. 4
    새 상태 반환

    읽기 전용 state를 직접 변형하지 않음

  5. 5
    UI 갱신

    현재 핸들러의 state는 여전히 이전 snapshot