React에서 컴포넌트의 상태 관리를 위해 기본적으로 가장 많이 쓰이는 hook은 setState()
함수인데요. 좀 더 복잡한 상태 관리가 필요한 React 컴포넌트에서는 setReducer()
hook 함수를 사용할 수 있습니다.
기본적으로 useReducer()
hook 함수는 다음과 같은 형태로 사용을 합니다.
const [<상태 객체>, <dispatch 함수>] = useReducer(<reducer 함수>, <초기 상태>, <초기 함수>)
Reducer 함수는 현재 상태(state) 객체와 행동(action) 객체를 인자로 받아서 새로운 상태(state) 객체를 반환하는 함수입니다. 그리고 dispatch 함수는 컴포넌트 내에서 상태 변경을 일으키기 위해서 사용되는데 인자로 reducer 함수에 넘길 행동(action) 객체를 받습니다. 행동(action) 객체는 관행적으로 어떤 부류의 행동인지를 나타내는 type 속성과 해당 행동과 괸련된 데이터를 담고 있습니다. 다시 말해, 컴포넌트에서 dispatch 함수에 행동(action)을 던지면, reducer 함수가 이 행동(action)에 따라서 상태(state)를 변경해줍니다.
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 속성에는 변경할 값의 크기를 넘기고 있습니다.
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 함수가 호출되도록 설정해줄 뿐입니다.
참고할만한 페이지
https://jeonghwan-kim.github.io/dev/2023/03/29/use-reducer.html
https://dev.to/m0nm/usestate-vs-usereducer-what-are-they-and-when-to-use-them-2c5c