useReducer 훅 소개
리액트에서 제공하는 또 다른 강력한 훅인 useReducer
대해 알아보겠습니다.
useReducer
는 useState
와 유사하게 상태를 관리하지만, 특히 복잡한 상태 로직을 다루거나 다음 상태가 이전 상태에 의존적인 경우, 그리고 여러 개의 하위 값으로 구성된 상태 객체를 다룰 때 더욱 효과적입니다. 또한, Context API와 함께 사용하여 전역 상태 관리에 활용되기도 합니다.
useState
vs 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를 업데이트하는 것을 볼 수 있습니다.
useReducer
의 장점
복잡한 상태 로직 관리: 여러 개의 하위 상태를 가진 객체나, 다음 상태가 이전 상태에 따라 결정되는 복잡한 상태 전환 로직을 깔끔하게 분리하여 관리할 수 있습니다. useState
의 경우 setState
내부에 복잡한 로직이 들어가기 쉽습니다.
재사용성: reducer
함수는 순수 함수이므로, 여러 컴포넌트에서 동일한 상태 로직을 재사용할 수 있습니다. (예: 여러 카운터 컴포넌트가 동일한 counterReducer
를 사용할 수 있음)
테스트 용이성: reducer
함수는 독립적인 순수 함수이므로, UI와 분리하여 단위 테스트하기 매우 용이합니다.
성능 최적화: dispatch
함수는 한 번 생성되면 변경되지 않습니다. 따라서 dispatch
함수를 자식 컴포넌트에 props
로 전달해도 불필요한 재렌더링을 유발하지 않습니다. (이 점이 useState
의 setState
함수와 동일합니다.)
협업 효율성: 상태 로직이 reducer로 추상화되어 있어, 다른 개발자가 코드를 이해하고 수정하기 더 용이합니다.
useReducer
와 Context API의 결합
useReducer
는 컴포넌트 내부의 복잡한 상태 로직을 관리하는 데 뛰어나지만, 이 상태를 여러 컴포넌트에 걸쳐 공유하려면 여전히 프롭스 드릴링 문제가 발생할 수 있습니다. 이때 Context API와 useReducer
를 함께 사용하면 전역적인 복잡한 상태를 효율적으로 관리할 수 있습니다.
아이디어
Context API를 사용하여 state
와 dispatch
함수를 제공합니다.
useReducer
훅을 사용하여 state
와 dispatch
함수를 생성합니다.
Context.Provider
의 value
로 이 state
와 dispatch
를 전달합니다.
하위 컴포넌트에서는 useContext
훅을 사용하여 state
와 dispatch
를 직접 가져와 사용합니다.
이 조합은 Redux와 같은 전역 상태 관리 라이브러리의 경량 버전처럼 작동할 수 있으며, 다음 장에서 더 자세히 다루겠습니다.
"useReducer
훅 소개"는 여기까지입니다. 이 장에서는 useReducer
훅의 개념, useState
와의 차이점, reducer
함수, dispatch
함수, 그리고 initialState
의 역할을 카운터 예제를 통해 상세하게 배웠습니다. 또한 useReducer
가 복잡한 상태 로직 관리에 왜 효과적인지 그 장점들을 알아보았습니다.
이제 여러분은 리액트에서 복잡한 상태를 더 체계적으로 관리할 수 있는 도구를 하나 더 얻게 되었습니다. 다음 장에서는 오늘 배운 useReducer
와 지난 장에서 배운 Context API를 함께 사용하여 진정한 의미의 전역 상태 관리 시스템을 구축하는 방법을 알아보겠습니다.