일단 개념은 검색을 해보면 알 다음과 같습니다.
복잡한 상태 전이를 단순화하고, 여러 상태값을 일관성 있게 관리할 때 유용합니다. useReducer는 리듀서 패턴을 사용하여 상태 전이 로직을 관리
합니다.
여기서 주요 특징을 살펴보자면 다음과 같이 설명할 수 있는데요.
- 복잡한 상태 전이를 단순화
- 여러 상태값을 일관성 있게 관리
- 리듀서 패턴을 사용하여 상태 전이 로직을 관리.
우선 앞선 특징들을 하나씩 살펴보겠습니다.
우선 useReducer를 생성하는 방법은 함수 정의
, 초기 상태 정의
, useReudcer 할당 순서
로 이루어지는데요. 대강 생성 방법은 다음과 같습니다.
아래의 코드 예시에서 설명 드려보겠습니다.
import React, { useReducer, useRef } from 'react'; // ★ reducer 함수 정의 const reducer = (state, action) => { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + action.payload }; case 'DECREMENT': return { ...state, count: state.count - action.payload }; case 'SET_INPUT_VALUE': return { ...state, inputValue: action.payload }; default: return state; } }; function Counter() { // ★ 초기 상태 정의 const initialState = { count: 0, inputValue: '', }; // ★ useReducer 할당 const [state, dispatch] = useReducer(reducer, initialState); const { count, inputValue } = state; // useRef를 사용하여 input 요소를 참조 const inputRef = useRef(null); // 버튼 클릭 이벤트 핸들러 const onClickButton = (val) => { dispatch({ type: val > 0 ? 'INCREMENT' : 'DECREMENT', payload: Math.abs(val) }); }; // 엔터 키 이벤트 핸들러 const onEnterPress = (e) => { if (e.key === 'Enter') { dispatch({ type: 'SET_INPUT_VALUE', payload: inputRef.current.value }); } }; return ( <div> <p>Count: {count}</p> <button onClick={() => onClickButton(1)}>Increment</button> <button onClick={() => onClickButton(-1)}>Decrement</button> <input ref={inputRef} type="text" placeholder="Type something" onKeyDown={onEnterPress} /> <p>Input value: {inputValue}</p> </div> ); } export default Counter;
우선 위의 코드에서 함수를 정의하는 부분은 다음과 같습니다.
// ★ reducer 함수 정의 const reducer = (state, action) => { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + action.payload }; case 'DECREMENT': return { ...state, count: state.count - action.payload }; case 'SET_INPUT_VALUE': return { ...state, inputValue: action.payload }; default: return state; } };
함수 정의
부분에서는 switch-case 문을 사용하여 action의 타입 값에 따라 state의 이전값(...state)과 변화시킬 특정 값과 연산하여 할당해주는 코드가 들어갑니다.
정리하자면 기존의 상태 관리(useState)의 각 메서드들이 하던 처리를 해당 reducer 함수에 모아놓고, case에 따라 처리한다고 보시면 됩니다. 그래서 함수 정의 부분에서는 기존의 스테이트의 다양한 처리 메서드가 컴포넌트 안에 작성될 때 어려웠던 코드 관리나 가독성 저하를 해결할 수 있기도 한거죠.
// useState의 객체 상태 초기화 const [state, setState] = useState({ count: 0, inputValue: '', }); // useReducer에서 사용할 객체 상태 정의 (초기화) const initialState = { count: 0, inputValue: '', };
다음은 초기 상태 정의
부분입니다. 정확히 말하자면 state의 초기값들을 정의하는 부분입니다. 편하게 객체 리터럴로 사용할 상태값들의 초기 상태를 지정해주면 됩니다.
const [state, dispatch] = useReducer(reducer, initialState); // state (useReducer에서 관리할 최신 스테이트)값들을 각 스테이트에 할당한 후 화면에 렌더링 const { count, inputValue } = state;
마지막 useReducer 할당
부분입니다. useReducer를 호출한 뒤 앞서 정의한 함수 부분과 초기 객체값을 useReducer로 처리한 뒤 그 결과값을 각각 state와 dispatch 메서드 할당해 준다고 보시면 되겠습니다.
이후에는 Dispatch 메서드가 실행될 때마다 reducer 함수에서 정의한 각각의 반환문(상태 변화 로직)이 실행된 뒤 state의 값을 반환해주는 것으로 useReducer의 최종 단계가 마무리 됩니다.
이때 두번째 매개변수 (초기값)
는 useReducer가 최초로 실행될 때 state에 전달해주는 초기값 이므로 이후의 렌더링에는 사용되지 않는다(영향을 미치지 않는다)는 점을 알아두시면 됩니다.
언뜻 텍스트로 보면 이해가 안가실텐데요, 앞서 소개해드린 useReducer의 정의 부분이 끝났다고 가정하고, 발생 단계에 대해서 소개해 드리겠습니다.
- 상태 변화 : 버튼을 누르면 onClick 이벤트가 발생한 뒤 해당 메서드에 1을 전달합니다.
<button onClick={() => onClickButton(1)}>Increment</button>
- Dispatch 호출 : 해당 이벤트 핸들러가 dispatch 메서드를 호출한 후 Action 오브젝트 객체를 생성하여 전달합니다. 이때 기본적으로 type (reducer 함수에서 switch문을 실행할 코드)와 상태값을 바꾸게 할 때 사용하는 변수를 한 쌍으로 한 객체를 dispatch 메서드에 전달합니다.
const onClickButton = (val) => { dispatch({ type: val > 0 ? 'INCREMENT' : 'DECREMENT', value: Math.abs(val) }); };
- reducer 함수 실행 : Dispatch는 넘겨 받은 Action 객체(오브젝트)를 reducer에 넘겨주는 역할을 하는데요. 이때 reducer의 첫번째 매개변수는 기존의 상태값이(state), 두번째 매개변수에는 Action 객체가 할당됩니다.
앞서 이벤트 핸들러에가 Dispatch 메서드를 호출할 때 객체를 넘겨준다고 했죠? 그리고 이 객체를 Dispatch 메서드가 reducer 함수로 넘겨줄 때 할당되어 action.type과 action.value를 이용하여 조건문의 반환문을 실행하여 상태값을 반환받도록 할 수 있는 것이죠.
const reducer = (state, action) => { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + action.value }; case 'DECREMENT': return { ...state, count: state.count - action.value }; case 'SET_INPUT_VALUE': return { ...state, inputValue: action.value }; default: return state; } };
정리하자면 useReducer
는 각 이벤트 핸들러가 Dispatch를 호출하고, 실제 실행되는 코드는 앞서 정의한 reducer 함수에서 처리하므로, 컴포넌트 내부의 코드가 간결해지고, 각 이벤트 핸들러의 실행 함수를 모아 놓아 관리까지 간단하게 할 수 있는 편의성을 제공하는 것이죠.
이는 내부 로직이 복잡해지는 코드, 그러니까 프로젝트를 하다보면 당연히 내부 로직이 복잡해질테고, 이때 useReducer가 유용하게 사용되는 것입니다.
(자료 출처 : https://blog.stackademic.com/understanding-usereducer-in-react-part-1-0994812b5a2a)
위의 이미지에서는 더 쉽게 useReducer가 처리되는 과정을 다음과 같이 설명하고 있습니다.
- state 업데이트
- 이벤트 핸들러에 리렌더링
- 이벤트 헨들러에서 Dispatch 호출
- Dispatch에서 Action 오브젝트를 Reducer 함수에 전달
- Type 값에 따라 반환문 실행 후 state에 상태값 반환
이러한 useReducer를 사용하는 것과 안하는 것의 예시는 useState를 예로 들어 설명할 수 있는데요.
앞서 소개해드린 useReducer의 장점 세 가지를 useState를 쓸 때와 안쓸 때를 들어 설명해 보겠습니다.
- 복잡한 상태 전이를 단순화 : useState로 개별적인 이벤트 핸들러(상태 변화) 메서드가 늘어날 때 코드가 복잡해지는 것을 단순화 한다는 의미입니다.
// useState를 사용할 때 const onClickButton = (val) => { if (val > 0) { setCount(count + val); } else { setCount(count - Math.abs(val)); } }; const onEnterPress = (e) => { if (e.key === 'Enter') { setInputValue(inputRef.current.value); } }; // useReducer를 사용할 때 const onClickButton = (val) => { dispatch({ type: val > 0 ? 'INCREMENT' : 'DECREMENT', value: Math.abs(val) }); }; const onEnterPress = (e) => { if (e.key === 'Enter') { dispatch({ type: 'SET_INPUT_VALUE', value: inputRef.current.value }); } };
- 여러 상태값을 일관성 있게 관리 : 모든 상태가 하나의 객체 안에 묶여 있기 때문에 상태 간의 관계를 보다 일관성 있게 유지할 수 있습니다. 이는 상태 업데이트 로직이 중앙(reducer)에서 관리되기 때문에 상태의 일관성 유지가 가능하다는 뜻입니다.
// useState를 사용할 때 const [count, setCount] = useState(0); const [inputValue, setInputValue] = useState(''); // useReducer를 사용할 때 const [state, dispatch] = useReducer(reducer, initialState); const { count, inputValue } = state;
- 리듀서 패턴을 사용하여 상태 전이 로직을 관리. : 1번의 장점과도 비슷한데, 결국 모든 이벤트 핸들러 함수가 reducer에서 작성되므로 Type에 따라 실행할 이벤트 핸들러 함수를 호출하여 보다 쉽게 해당 함수 로직을 관리할 수 있음을 의미합니다.