React

useReducer의 장점 알아보기 (vs useState)

citron031 2023. 7. 15. 10:40

useReducer의 존재는 알고있었고, useState를 대체한다는 점 역시 알고있었다.

redux의 dispatch나 reducer 개념이 useReducer로부터 비롯되었다는 것도 알았지만-

실제로 무슨 장점이 있기에 useState대신 useReducer를 쓰는가에 대해서 진지하게 생각해본적이 없었다는 것을 알게 되었다.

따라서, 이번 기회에 useReducer의 장점에 대해서 기록해보고자 하였다.

 

🐥 닉네임과 보유한 캐시를 상태로 가지는 컴포넌트를 가정하고, useState와 useReducer를 비교해보는 예제를 만들었다.

 

일단, useState를 사용한 예제이다.

import { useState } from "react";

export default function App() {
  const [nickname, setNickname] = useState<string>("");
  const [balance, setBalance] = useState<number>(0);
  const changeNickname = (name: string) => {
    setNickname(name);
  };
  const changeBalance = (money: number) => {
    setBalance(money);
  };
  return (
    <div>
      <h2>
        {nickname}님의 보유 캐시는 {balance}입니다.
      </h2>
      <button onClick={() => changeNickname("Hahaha")}>닉네임 변경!</button>
      <button onClick={() => changeBalance(5000)}>잔액 충전!</button>
    </div>
  );
}

익숙한 코드이고, 딱히 문제점은 보이지 않는다.

각 버튼에는 닉네임과 잔액을 변경할 수 있는 함수가 연결되어있다.

함수에 닉네임과 금액을 전달하여 상태를 변경할 수 있다.

 

그렇다면, 이번엔 useReducer를 통해서 동일한 기능을 구현한 컴포넌트를 작성해보자. 😎

import { useReducer } from "react";

interface ReducerState {
  nickname: string;
  balance: number;
}

type ReducerType = "SET_NICKNAME" | "SET_BALANCE";

interface ReducerAction {
  type: ReducerType;
  payload: number | string;
}

const initialState = {
  nickname: "",
  balance: 0
};

const reducer = (state: ReducerState, action: ReducerAction): ReducerState => {
  switch (action.type) {
    case "SET_NICKNAME": {
      if (typeof action.payload === "string") {
        return { ...state, nickname: action.payload };
      } else {
        return state;
      }
    }
    case "SET_BALANCE": {
      if (typeof action.payload === "number") {
        return { ...state, balance: action.payload };
      } else {
        return state;
      }
    }
    default:
      return state;
  }
};

export default function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  const handleNickname = () => {
    dispatch({ type: "SET_NICKNAME", payload: "Hihihi" });
  };
  const handleBalance = () => {
    dispatch({ type: "SET_BALANCE", payload: 14700 });
  };
  return (
    <div>
      <h2>
        {state.nickname}님의 보유 캐시는 {state.balance}입니다.
      </h2>
      <button onClick={handleNickname}>닉네임 변경!</button>
      <button onClick={handleBalance}>잔액 충전!</button>
    </div>
  );
}

일단 코드가 길어졌다. (redux 코드와 거의 유사하다)

그렇다면, 이 길어진 코드에 무슨 장점이 있는걸까?

 

🍉 역시 가장 눈에띄는 것은 reducer다.

  • reducer 함수 내부에 action으로 미리 어떻게 상태가 변화될지 정해놓을 수 있다.
  • 따라서 상태 관리 로직을 명확하게 분리하고, 액션 타입을 미리 정의하여 상태 업데이트를  처리할 수 있다.
  • 기존의 useState의 change함수나 handler함수는 모두 각각 정의되는 반면, useReducer를 사용하면 dispatch에 적절한 action 객체를 전달함으로써, 여러 상태의 변화를 하나의 reducer 함수로 관리할 수 있다.
  • 만약, 다수의 하위 컴포넌트에 상태를 전달해야하는 상위 컴포넌트에 useReducer를 사용하면, 좀 더 중앙 집중화된 상태관리 로직을 작성할 수 있을 것이다.
  • 반면, useState를 사용하면 상태 업데이트 로직은 각각 컴포넌트에 위치하여 상태의 구조와 변화를 파악하는 일을 방해하게 될것이다.

 

 

const [userData, setUserData] = useState({nickname: "", balance: 0});

const handleUserData = (newState) => {
  setUserData(prev => {
    ...prev,
    ...newState
  });
}

...

return (
    <button onClick={() => handleUserData({balance: 2000})}>잔액 충전!</button>
)

위와 같이 상태를 객체로 만들어 관리하면, 좀 더 중앙 집중된 상태를 사용할 수 있지만, reducer 함수에 미리 상태 변화의 가능성들을 명시해두는 편이 에러를 발견하고 예외를 처리하는데 있어서 용이하다.

action 객체의 type으로 상태 변화의 케이스들이 관리되기 때문에, 유지보수하는 입장에서도 문제가 생긴 부분을 빠르게 파악할 수 있는 장점도 있다.

따라서, 위와 같이 객체로 상태를 관리할 경우에는 useReducer를 사용하는 편이 좀 더 관리적인 측면에서 좋은 것 같다. (초기 작성 코드는 좀 늘어날지도 모르겠지만...)

 

또한, map을 사용하는데 있어서 이점도 존재한다.

기존의 useState에서의 값 변경 함수는 각 상태마다 별개로 존재했지만, useReducer에서는 일관되게 dispatch를 통해서 값을 변경하기 때문에, map을 사용할 때와 같이 일관된 코딩이 필요한 시점에 더 유용하게 사용될 수 있다.

 

👏 또한, useReducer를 사용하면 상태 관련 로직(reducer)이 컴포넌트와 분리되어 외부에서 이를 관리할 수 있고, useState보다 더 세밀한 상태 관리가 가능하기에 불필요한 렌더링을 방지할 수 있다.

 

⛈ useReducer 공식문서

https://react.dev/reference/react/useReducer

 

useReducer – React

The library for web and native user interfaces

react.dev