useReducer에 대해 학습 하기 전 reducer에 대해 먼저 살펴보겠습니다.
function reducer(state, action) { switch (action.type) { case 'increment': return { count: state.count + 1 }; case 'decrement': return { count: state.count - 1 }; default: return state; } }
reducer 함수는 현재 상태(state), 액션 객체를 인수로 받고, action.type에 따라 새로운 상태(state)를 반환하는 함수입니다.
useReducer는 useState 처럼 상태를 관리하는 hook입니다. useReducer는 주로 복잡한 상태 로직을 처리해야 할 때 사용됩니다.
function MyComponent() { const [state, dispatch] = useReducer(reducer, { age: 42 });
- reducer (위에서 미리 본 거와 같습니다.)
state가 어떻게 업데이트 되는지 지정하는 리듀서 함수(순수 함수)
state와 action을 인수로 받고, 다음 state 반환
state와 action에 모든 데이터 타입이 할당 가능
- initialArg
초기 state가 계산되는 값
모든 데이터 타입이 할당 가능
- init
초기 state를 반환하는 초기화 함수
이 함수가 인수에 할당되지 않으면 초기 state는 initialArg로 설정
할당되었다면 초기 state는 init(initialArg)를 호출한 결과가 할당
위의 흐름도를 살펴보면 Dispatch에 Action을 담아 Reducer로 보냅니다. 2개의 인수(현재의 state와 Action)를 받은 Reducer는 Action의 내용에 따라 state를 새로운 값으로 변경한 뒤 반환합니다.
위의 예시 코드를 보면 counterReducer 함수의 경우 Component 밖에 정의되어 있습니다. 이렇게 코드를 작성한다면 몇가지 장점이 있습니다.
로직의 재사용성
다른 컴포넌트에서도 동일한 상태 업데이트 로직을 사용할 수 있어 재사용성을 높이고, 중복을 줄여줍니다.
테스트 용이성
로직이 컴포넌트와 분리되어 있으므로 테스트하기 쉽고 효율적입니다.
코드 분리
상태 업데이트 로직이 컴포넌트와 분리되어 있으므로 컴포넌트 코드가 더 간결해집니다.
reducer 안에서의 state는 읽기 전용으로 직접적인 변경을 할 수 없습니다.
잘못된 예시
function reducer(state, action) { switch (action.type) { case 'incremented_age': { // 🚩아래와 같이 객체의 직접적인 변형 X state.age = state.age + 1; return state; }
올바른 예시
function reducer(state, action) { switch (action.type) { case 'incremented_age': { // ✅ 새로운 객체를 반환하는 형식으로 사용 return { ...state, age: state.age + 1 }; }
처음 useReducer을 공부 할 때 쉽게 이해되지 않아(사실 공부 대충한 제 잘못...) 대충 넘긴 기억이 있습니다. 상태 관리 라이브러리 중 하나인 Redux를 배울 때, 전체적인 흐름이 useReducer와 비슷해 다시 공부했었습니다. 지금 이 글을 작성하며 스스로 새로운 것을 배울 때 무엇보다 중요한건 내용의 난이도를 떠나 마음가짐에 달려있다고 생각합니다.