본문으로 바로가기

[React] useReducer는 언제 사용할까?

category 1. 웹개발/1_1_5 React JS 2024. 4. 6. 14:10

 

 

useReducer란?

useReducer는 useState의 심화 버전이라고 볼 수 있습니다. useState와 비슷한 형태를 띠지만 좀 더 복잡한 상태값을 미리 정의해 놓은 시나리오에 따라 관리가 가능합니다. useReducer에서 사용되는 용어를 먼저 살펴보겠습니다.

  • 반환값은 useState와 동일하게 길이가 2인 배열입니다.
    • state: 현재 useReducer가 가지고 있는 값을 의미합니다. useState와 마찬가지로 배열을 반환하는데, 동일하게 첫 번째 요소가 이 값입니다.
    • dispatcher: state를 업데이트하는 함수. useReducer가 반환하는 배열의 두 번째 요소입니다. setState는 단순히 값을 넘겨주지만 여기서는 action을 넘겨준다는 점이 다릅니다. 이 action은 stat를 변경할 수 있는 액션을 의미합니다.
  • useState의 인수와 달리 2개에서 3개의 인수를 필요로 합니다.
    • reducer: useReducer의 기본 action을 정의하는 함수입니다. 이 reducer는 useReducer의 첫 번째 인수로 넘겨주어야 합니다.
    • initialState: 두 번째 인수로, useReducer의 초깃값을 의미합니다.
    • init: useState의 인수로 함수를 넘겨줄 떄처럼 초깃값을 지연해서 생성시키고 싶을 때 사용하는 함수입니다. 이 함수는 필수값이 아니며, 만약 여기에 인수로 넘겨주는 함수가 존재한다면 useState와 동일하게 게으른 초기화(Lazy initialization)가 일어나며 initialState를 인수로  init 함수가 실행됩니다.

 

useReducer 사용법

useReducer에 대해 간단히 살펴봤으니 본격적으로 코드를 보면서 사용법을 확인해보겠습니다.

// useReducer가 사용할 state를 정의
  type State = {
    count: number
  }

  // state의 변화를 발생시킬 action의 타입과 넘겨줄 값(payload)을 정의
  // 꼭, type과 payload라는 네이밍을 지킬 필요도 없으며, 굳이 객체일 필요도 없음.
  // 다만 이러한 네이밍이 가장 널리 사용됨.
  type Action = { type: 'up' | 'down' | 'reset'; payload?: State }

  // 무거운 연산이 포함된 게으른 초기화 함수
  function init(count: State): State {
    // count: State를 받아서 초깃값을 어떻게 정의할지 연산하면 됨.
    return count;
  }

  // 초깃값
  const initialState: State = { count: 0 };

  // 앞서 선언한 state와 action을 기반으로 state가 어떻게 변경될지 정의
  function reducer(state: State, action: Action): State {
    switch (action.type) {
      case 'up': return { count: state.count + 1 };
      case 'down': return { count: state.count - 1 > 0 ? state.count -1 : 0 };
      case 'reset': return init(action.payload || { count : 0 })
      default: throw new Error(`Unexpected action type ${action.type}`)
    }
  }

  function App() {
    const [state, dispatcher] = useReducer(reducer, initialState, init);

    function handleUpButtonClick() {
      dispatcher({ type: 'up'});
    }

    function handleDownButtonClick() {
      dispatcher({ type: 'down'});
    }

    function handleResetButtonClick() {
      dispatcher({ type: 'reset', payload: { count: 1 }});
    }
  }

 

useReducer를 사용하는 모습이 언뜻 보면 복잡해 보일 수 있지만 useReducer의 목적은 간단합니다. 복잡한 형태의 state를 사전에 정의된 dispatcher로만 수정할 수 있게 만들어 줌으로써 state 값에 대한 접근은 컴포넌트에서만 가능하고, 이를 업데이트하는 방법에 대한 상세 정의는 컴포넌트 밖에다 둔 다음, state의 업데이트를 미리 정의해 둔 dispatcher로만 제한하는 것입니다. state값을 변경하는 시나리오를 제한적으로 두고 이에 대한 변경을 빠르게 확인할 수 있게끔 하는 것이 useReducer의 목적입니다.

 

일반적으로 단순히 number나 boolean과 같이 간단한 값을 관리하는 것은 useState로 충분하지만 state 하나가 가져야 할 값이 복잡하고 이를 수정하는 경우의 수가 많아진다면 state를 관리하는 것이 어려워집니다. 또 여러 개의 state를 관리하는 것보다 때로는 성격이 비슷한 여러 개의 state를 묶어 useReducer로 관리하는 편이 더 효율적일 수도 있습니다. 이렇게 useReducer를 사용해 state를 관리하면 state를 사용하는 로직과 이를 관리하는 비즈니스 로직을 분리할 수 있어 state를 관리하기가 한결 쉬워집니다.

 

세 번째 인수인 게으른 초기화 함수는 굳이 사용하지 않아도 됩니다. 이 함수가 없다면 두 번째 인수로 넘겨받은 기본값을 사용하게 될 것입니다. 다만 게으른 초기화 함수를 넣어줌으로써 useState에 함수를 넣은 것과 같은 동일한 이점을 누릴 수 있고, 추가로 state에 대한 초기화가 필요할 때 reducer에서 이를 재사용할 수 있다는 장점도 있습니다.

 

useRecuder나 useState 둘 다 세부 작동과 쓰임에만 차이가 있을 뿐, 결국 클로저를 활용해 값을 가둬서 state를 관리한다는 사실에는 변함이 없습니다.

 

 

 

Reference

React Depp Diver