Logo

React Hooks: useReducer 사용법

React에서 컴포넌트의 상태 관리를 위해 기본적으로 가장 많이 쓰이는 hook은 setState() 함수인데요. 좀 더 복잡한 상태 관리가 필요한 React 컴포넌트에서는 setReducer() hook 함수를 사용할 수 있습니다.

React Hooks 중 하나인 setState() 함수에 대한 설명은 관련 포스팅를 참고 바랍니다.

Redux 패턴

기본적으로 useReducer() hook 함수는 다음과 같은 형태로 사용을 합니다.

const [<상태 객체>, <dispatch 함수>] = useReducer(<reducer 함수>, <초기 상태>, <초기 함수>)

reducer 함수는 현재 상태(state) 객체와 행동(action) 객체를 인자로 받아서 새로운 상태(state) 객체를 반환하는 함수입니다. 그리고 dispatch 함수는 컴포넌트 내에서 상태 변경을 일으키기 위해서 사용되는데 인자로 reducer 함수에 넘길 행동(action) 객체를 받습니다. 행동(action) 객체는 관행적으로 어떤 부류의 행동인지를 나타내는 type 속성과 해당 행동과 괸련된 데이터를 담고 있습니다. 다시 말해, 컴포넌트에서 dispatch 함수에 행동(action)을 던지면, reducer 함수가 이 행동(action)에 따라서 상태(state)를 변경해줍니다.

Redux 패턴에 익숙하시거나 Redux 라이브러리를 써보신 분이라면, useReducer() hook에서 reducer 함수와 dispatch 함수가 어떻게 작동하는지 바로 감이 오실 텐데요. 그러지 않으신 분들은 이 게 도대체 무슨 말인지 바로 설명만 들어서는 잘 이해가 되지 않으실 것 같습니다. 예제를 통해 좀 더 자세히 살펴보도록 하겠습니다.

카운터 컴포넌트

useReducer() hook 함수를 이용해서 현재 카운트 값과 2개 버튼을 보여주는 간단한 카운터 컴포넌트를 작성해보겠습니다.

import React, { useReducer } from "react";

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <h2>{state.count}</h2>
      <button onClick={() => dispatch({ type: "INCREMENT", step: 1 })}>
        증가
      </button>
      <button onClick={() => dispatch({ type: "DECREMENT", step: 1 })}>
        감소
      </button>
    </>
  );
}

현재 카운트 값은 상태(state) 객체로 부터 읽어오고, 카운트 값 변경을 위해서는 각 버튼이 클릭되었을 때 dispatch 함수를 호출하도록 설정해주고 있습니다. dispatch 함수의 인자로 type 속성에는 어떤 변경인지에 따라 INCREMENT 또는 DECREMENT가 넘어가고, step 속성에는 변경할 값의 크기를 넘기고 있습니다.

reducer 함수

useReducer() hook 함수는 첫번째 인자로 넘어오는 reducer 함수를 통해 컴포넌트의 상태(state)가 행동(action)에 따라 어떻게 변해야하는지를 정의합니다. 위에서 작성한 카운터 컴포넌트에서 사용할 reducer 함수는 switch 분기문을 이용하면 이해하기 쉽게 작성할 수 있습니다.

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return { count: state.count + action.step };
    case "DECREMENT":
      return { count: state.count - action.step };
    default:
      throw new Error("Unsupported action type:", action.type);
  }
}

INCREMENT 타입의 행동에 대해서는 현재 카운트 값을 step 만큼 증가하여 새로운 상태를 반환하고, DECREMENT 타입의 행동에 대해서는 현재 카운트 값을 step 만큼 감소하여 새로운 상태를 반환합니다. 정의하지 않은 행동 타입이 넘어왔을 때는 예외를 발생시키는 것이 좋습니다.

복잡한 상태 관리

사실 이 정도의 간단한 상태 관리를 위해서라면 그냥 간단하게 useState() hook 함수를 쓰는 편이 나을 수도 있습니다. 좀 더 복잡한 상태 관리를 시뮬레이트하기 위해서 카운트의 하한 값과 상한 값을 제한하고, 카운트의 값을 무작위로 바꾸는 버튼과 초기화 시키는 버튼을 추가해보겠습니다.

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT":
      return state.count < action.max
        ? { count: state.count + action.step }
        : state;
    case "DECREMENT":
      return state.count > action.min
        ? { count: state.count - action.step }
        : state;
    case "RESET":
      return initialState;
    case "RANDOM":
      return {
        count:
          Math.floor(Math.random() * (action.max - action.min)) + action.min,
      };
    default:
      throw new Error("Unsupported action type:", action.type);
  }
}

행동의 종류가 늘어나더라도 그에 따라 카운트 값이 어떻게 변하는지를 reducer 함수 안에 일목요연하게 정리할 수 있습니다.

import React, { useReducer } from "react";

function Counter({ step = 1, min = 0, max = 10 }) {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <>
      <p>
        단계: {step}, 최소: {min}, 최대: {max}
      </p>
      <h2>{state.count}</h2>
      <button onClick={() => dispatch({ type: "INCREMENT", step, max })}>
        증가
      </button>
      <button onClick={() => dispatch({ type: "DECREMENT", step, min })}>
        감소
      </button>
      <button onClick={() => dispatch({ type: "RANDOM", min, max })}>
        무작위
      </button>
      <button onClick={() => dispatch({ type: "RESET" })}>초기화</button>
    </>
  );
}

상태 관리 로직이 복잡해지더라도, 카운터 컴포넌트 코드는 크게 복잡해지지 않습니다. 단순히 새롭게 추가된 버튼이 호출되었을 때 로운 행동 타입으로 dispatch 함수가 호출되도록 설정해줄 뿐입니다.

전체 코드

본 포스팅에서 작성한 전체 코드는 아래에서 확인하고 직접 실행해볼 수 있습니다.