useReducer의 장점 알아보기 (vs useState)
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