useReducer 훅 소개
리액트에서 여러 상태 전이를 한곳에서 관리할 때 사용하는 훅인 useReducer를 다룹니다.
useReducer는 useState와 유사하게 상태를 관리하지만, 특히 복잡한 상태 로직을 다루거나 다음 상태가 이전 상태에 의존적인 경우, 그리고 여러 하위 값으로 구성된 상태 객체를 다룰 때 더 효과적입니다.
또한 Context API와 함께 사용해 전역 상태 관리에 활용되기도 합니다.
지역 상태와 useReducer 비교
useReducer 훅 소개에서 렌더링 흐름, 상태 경계, 사용자 반응을 정리한 것입니다.
useState는 상태의 값을 직접 설정하는 반면, useReducer는 상태를 업데이트하는 로직을 reducer 함수라는 곳으로 분리하여 관리합니다. 이는 Redux와 같은 상태 관리 라이브러리의 핵심 개념과도 유사합니다.
| 특징 | useState | useReducer |
|---|---|---|
| 사용법 | [state, setState] | [state, dispatch] |
| 업데이트 방식 | setState(newValue) 또는 setState(prev => ...) | dispatch({ type: 'ACTION_TYPE', payload: ... }) |
| 상태 로직 | 컴포넌트 내부에 직접 작성 | reducer 함수로 분리하여 관리 |
| 적합한 경우 | - 단순한 값 (숫자, 문자열, 불리언) - 단일 상태 - 상태 업데이트 로직이 간단할 때 | - 복잡한 상태 로직 - 여러 하위 값으로 구성된 상태 - 다음 상태가 이전 상태에 의존적일 때 - 상태 업데이트 로직을 재사용하고 싶을 때 - Context API와 함께 전역 상태 관리 |
| 장점 | 간단하고 직관적 | - 상태 로직의 분리로 가독성 및 유지보수성 향상 - 테스트 용이 - 상태 변경의 예측 가능성 (Redux와 유사) |
useReducer의 기본 사용법
useReducer 훅은 두 개의 인자를 받고, 두 개의 값을 반환합니다.
const [state, dispatch] = useReducer(reducer, initialState, init);-
reducer(함수)- 현재
state와action객체를 인자로 받아 새로운 상태(new state)를 반환하는 순수 함수입니다. reducer(state, action)형태를 가집니다.action은 일반적으로{ type: '액션명', payload: '데이터' }형태의 객체입니다.reducer함수 내부에서는 직접state를 변경(mutate)하지 않고, 항상 새로운state객체를 반환해야 합니다.
- 현재
-
initialState(값)state의 초기값입니다. 어떤 타입이든 될 수 있습니다.
-
init(함수, 선택 사항)- 초기 상태를 지연(lazy)하여 생성할 때 사용하는 함수입니다.
init(initialArg)형태를 가집니다. init함수가 제공되면initialState는init함수의 인자로 사용되고,init함수의 반환값이 초기 상태가 됩니다.- 초기 상태 계산 비용이 높을 때 유용합니다.
- 초기 상태를 지연(lazy)하여 생성할 때 사용하는 함수입니다.
-
state(값)- 현재 상태 값입니다.
useState의state와 동일합니다.
- 현재 상태 값입니다.
-
dispatch(함수)- 상태 업데이트를 요청하는 함수입니다.
action객체를 인자로 받습니다. dispatch(action)을 호출하면 React는reducer함수를 실행하여 새로운 상태를 계산하고 컴포넌트를 재렌더링합니다.
- 상태 업데이트를 요청하는 함수입니다.
카운터 예제를 통한 이해
가장 간단한 카운터 예제를 통해 useReducer의 작동 방식을 알아봅시다.
import React, { useReducer } from 'react';
// 1. reducer 함수 정의
// 이 함수는 현재 상태(state)와 발생한 액션(action)을 받아서 새로운 상태를 반환합니다.
function counterReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
case 'RESET':
return { count: 0 }; // action.payload를 사용한다면: return { count: action.payload };
case 'ADD_BY_VALUE':
return { count: state.count + action.payload }; // payload 사용 예시
default:
// 알 수 없는 액션 타입이 들어오면 현재 상태를 그대로 반환하거나 에러를 던질 수 있습니다.
throw new Error(`Unsupported action type: ${action.type}`);
}
}
function CounterWithReducer() {
// 2. useReducer 훅 사용
// useReducer(reducer 함수, 초기 상태 값)
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
return (
<div style={{ textAlign: 'center', padding: '20px', border: '1px solid #ddd', borderRadius: '8px', maxWidth: '400px', margin: '20px auto', backgroundColor: '#f9f9f9' }}>
<h2 style={{ color: '#2c3e50' }}>Reducer 카운터</h2>
<p style={{ fontSize: '2em', margin: '20px 0', color: '#3498db' }}>현재 카운트: {state.count}</p>
<div style={{ display: 'flex', justifyContent: 'center', gap: '10px' }}>
<button
onClick={() => dispatch({ type: 'INCREMENT' })} // 액션 객체를 dispatch 함수에 전달
style={{ padding: '10px 20px', fontSize: '1em', backgroundColor: '#2ecc71', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
>
증가 (+)
</button>
<button
onClick={() => dispatch({ type: 'DECREMENT' })}
style={{ padding: '10px 20px', fontSize: '1em', backgroundColor: '#e74c3c', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
>
감소 (-)
</button>
<button
onClick={() => dispatch({ type: 'RESET' })}
style={{ padding: '10px 20px', fontSize: '1em', backgroundColor: '#95a5a6', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
>
초기화
</button>
</div>
<div style={{ marginTop: '20px' }}>
<button
onClick={() => dispatch({ type: 'ADD_BY_VALUE', payload: 5 })}
style={{ padding: '10px 20px', fontSize: '1em', backgroundColor: '#f39c12', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer' }}
>
5 추가
</button>
</div>
</div>
);
}
export default CounterWithReducer;App.js에 추가
import React from 'react';
import CounterWithReducer from './components/CounterWithReducer'; // CounterWithReducer 임포트
function App() {
return (
<div className="App">
{/* 다른 라우팅 코드 등 */}
<CounterWithReducer /> {/* 컴포넌트 추가 */}
</div>
);
}실행 및 확인:
CounterWithReducer 컴포넌트를 App.js에 추가하고 실행하면, 버튼을 클릭할 때마다 dispatch 함수가 호출되고, reducer 함수가 새로운 상태를 계산하여 UI를 업데이트하는 것을 볼 수 있습니다.
카운터 예제는 작지만 reducer 사고방식을 익히기에 좋습니다. 버튼은 상태를 직접 바꾸지 않고 action을 보내며, reducer는 이전 상태와 action만 보고 다음 상태를 계산합니다.
useReducer의 장점
복잡한 상태 로직 관리: 여러 개의 하위 상태를 가진 객체나, 다음 상태가 이전 상태에 따라 결정되는 복잡한 상태 전환 로직을 깔끔하게 분리하여 관리할 수 있습니다. useState의 경우 setState 내부에 복잡한 로직이 들어가기 쉽습니다.
재사용성: reducer 함수는 순수 함수이므로, 여러 컴포넌트에서 동일한 상태 로직을 재사용할 수 있습니다. (예: 여러 카운터 컴포넌트가 동일한 counterReducer를 사용할 수 있음)
테스트 용이성: reducer 함수는 독립적인 순수 함수이므로, UI와 분리하여 단위 테스트하기 매우 용이합니다.
성능 최적화: dispatch 함수는 한 번 생성되면 변경되지 않습니다. 따라서 dispatch 함수를 자식 컴포넌트에 props로 전달해도 불필요한 재렌더링을 유발하지 않습니다. (이 점이 useState의 setState 함수와 동일합니다.)
협업 효율성: 상태 로직이 reducer로 추상화되어 있어, 다른 개발자가 코드를 이해하고 수정하기 더 용이합니다.
useReducer를 선택할 때는 상태 개수보다 상태 전환 규칙의 복잡도를 먼저 봐야 합니다. 아래 다이어그램은 useState로 충분한 경우와 reducer로 분리하면 좋은 경우를 기준별로 정리합니다.
useReducer와 Context API의 결합
useReducer는 컴포넌트 내부의 복잡한 상태 로직을 관리하는 데 뛰어나지만, 이 상태를 여러 컴포넌트에 걸쳐 공유하려면 여전히 프롭스 드릴링 문제가 발생할 수 있습니다. 이때 Context API와 useReducer를 함께 사용하면 전역적인 복잡한 상태를 효율적으로 관리할 수 있습니다.
Context API를 사용하여 state와 dispatch 함수를 제공합니다.
useReducer 훅을 사용하여 state와 dispatch 함수를 생성합니다.
Context.Provider의 value로 이 state와 dispatch를 전달합니다.
하위 컴포넌트에서는 useContext 훅을 사용하여 state와 dispatch를 직접 가져와 사용합니다.
이 조합은 Redux와 같은 전역 상태 관리 라이브러리의 경량 버전처럼 작동할 수 있으며, 다음 절에서 더 자세히 다루겠습니다.
이때 중요한 점은 Context와 reducer의 책임을 섞지 않는 것입니다. Context는 상태와 dispatch를 전달하는 통로이고, reducer는 action을 해석해 다음 상태를 계산하는 규칙이므로, 아래처럼 역할을 나누어 보면 구조가 훨씬 선명해집니다.
"useReducer 훅 소개"는 여기까지입니다. 이 장에서는 useReducer 훅의 개념, useState와의 차이점, reducer 함수, dispatch 함수, 그리고 initialState의 역할을 카운터 예제를 통해 상세하게 배웠습니다. 또한 useReducer가 복잡한 상태 로직 관리에 왜 효과적인지 그 장점들을 알아보았습니다.
useReducer를 사용하면 리액트에서 복잡한 상태 전이를 더 명시적으로 관리할 수 있습니다. 다음 절에서는 useReducer와 Context API를 함께 사용해 전역 상태 관리 구조를 만들어 봅니다.
useReducer는 상태 변경 이유를 action으로 드러내므로 복잡한 전이를 한곳에서 검토하기 좋습니다.
지역 상태와 useReducer 비교와 useReducer의 기본 사용법 중심으로 정리한 보조 다이어그램입니다.