React의 Reducer API에서 "Reducer"는 무슨 뜻일까?
이 "Reducer"라는 이름은 리듀서 패턴(reducer pattern)이라는 디자인 패턴에서 유래했으며, Redux 같은 상태 관리 라이브러리에서 사용된다.
리듀서 패턴이란, 현재 상태(state)와 액션(action)을 받아 새로운 상태를 반환하는 순수 함수를 의미한다.
순수 함수와 부수 효과
https://velog.io/@juwon98/React-pure-function
// Reference
const [state, dispatch] = useReducer(reducer, initialArg, init?)
reducer: state가 어떻게 업데이트 되는지 지정하는 리듀서 함수. 리듀서 함수는 반드시 순수 함수여야 하며, state와 action을 인수로 받아야 하고, 다음 state를 반환해야 한다.
initialArg: state의 초기 값. (정확히는 init 인수에 따라 초기 state가 계산되는 값)
init: 초기 state를 반환하는 초기화 함수. 이 함수가 인수에 할당되지 않으면 초기 state는 initialArg, 할당되었다면 init(initialArg)를 호출한 결과가 할당된다.
useReducer - React
https://ko.react.dev/reference/react/useReducer
// 리듀서 함수
function reducer(state, action) {
switch (action.type) {
// age +1
case 'increment_age': {
return {
name: state.name,
age: state.age + 1
};
}
// name 변경
case 'change_name': {
return {
name: action.newName,
age: state.age
};
}
}
// 설정된 type의 action이 아니라면 오류
throw Error('Unknown action: ' + action.type);
}
위의 reducer 함수는 dispatch 메서드를 통해 전달받는 action에 따라 설정해둔 'increment_age'와 'change_name' 두 가지의 상태 업데이트를 실행하고, action의 type이 미리 지정해둔 case가 아니라면 허용하지 않고 오류를 발생시킨다.
function MyComponent() {
const [state, dispatch] = useReducer(reducer, { name: 'Joowon', age: 27 });
// 버튼 클릭 시 age +1
const handleButtonClick = () => {
dispatch({ type: 'increment_age' });
}
// input에 값 입력 후 제출 시 name 변경
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const newName = formData.get('new-name'); // input의 name 속성값을 기준으로 가져옴
if (newName.trim()) {
dispatch({ type: 'change_name', newName: newName });
}
e.target.reset(); // 폼 초기화
};
return (
<div>
<button type='button' onClick={handleButtonClick}>늙기</button>
<form onSubmit={handleSubmit}>
<label htmlFor='new-name'>새 이름</label>
<input id='new-name' type='text' name='new-name' />
<button type="submit">변경</button>
</form>
<div/>
)
}
이벤트 핸들링 함수에서 dispatch 메서드를 실행해 type(과 newName)을 포함하는action 객체를 전달하면, reducer 함수에 작성해 둔 상태 업데이트를 실행한다.
사실, useReducer를 사용하면 여러 가지 장점이 있긴 하지만, useState를 사용해서 상태를 관리하는 것이 훨씬 간단하고, useReducer의 필요성을 못느낄 수 있다.
대부분의 경우, useReducer를 사용하는 것이 코드를 이해하기 어렵게 만들기도 하고, 비슷한 기능을 제공하는 라이브러리로 대체하기도 하기 때문에 실제로 사용할 일이 많지 않다...
하지만, 리듀서 패턴을 사용하는 만큼, 이해하고 있으면 Redux같은 다른 도구를 사용하는 데에도 도움이 된다.
reducer 함수로 분리하는 것이 편리할 수 있음.reducer 함수를 컴포넌트 외부에 둘 수 있기 때문에 깔끔한 코드를 작성 가능.dispatch를 이용한 명확한 액션 기반 상태 관리useState와 useReducer의 비동기 업데이트 비교
useState의 경우
const Counter = () => { const [count, setCount] = useState(0); const handleClick = () => { setTimeout(() => { setCount(count + 1); }, 2000); }; return ( <div> <h1>Count: {count}</h1> <button onClick={handleClick}>+1 (2초 후 증가)</button> </div> ); }; export default Counter;버튼을 누르면 setTimeout이 2초 후에 실행된다.
하지만setCount(count + 1)의count값은 이벤트 핸들러가 실행될 당시의 값(클로저 내부의 값)을 참조하기 때문에, 버튼을 빠르게 여러 번 클릭하면, 모든 setTimeout에서 참조하는 count 값이 같은 값(업데이트 전 값)일 가능성이 높다. (클릭 횟수보다 count가 낮을 수 있음)useReducer의 경우
const reducer = (state, action) => { switch (action.type) { case "INCREMENT": return { count: state.count + 1 }; default: return state; } }; const Counter = () => { const [state, dispatch] = useReducer(reducer, { count: 0 }); const handleClick = () => { setTimeout(() => { dispatch({ type: "INCREMENT" }); }, 2000); }; return ( <div> <h1>Count: {state.count}</h1> <button onClick={handleClick}>+1 (2초 후 증가)</button> </div> ); }; export default Counter;
dispatch({ type: "INCREMENT" })가 호출되면, 현재 상태(state)가reducer함수에 전달된다.
reducer함수는 현재의 최신 state를 기반으로 새로운 상태를 반환한다.
따라서 setTimeout이 여러 개 실행되더라도, 각dispatch는 최신 state를 참조한다.
✔ 즉, 이전 상태를 클로저로 유지하지 않고, 항상 최신 상태를 reducer 함수에서 전달받아 업데이트하기 때문에 꼬일 위험이 없음!