
'상태관리하는 훅' 이라고하면 자연스럽게 useState 를 떠올리게 된다.
하지만, 관리해야하는 상태의 규모가 커지고 복잡해진다면 useState 만 사용해서 관리하기엔 (코드가독성저하, 유지보수등 여러 관리측면으로)어려워 질것이다.
그럴때 사용하는 것이 useReducer 라는 훅이다.
| 컴포넌트에 reducer 함수를 추가하는 React Hook이다.
이 훅을 사용하면 상태와 상태를 변경하는 로직을 분리하여 관리할 수 있다.
상태가 어떻게 변해야하는지의 규칙은 리듀서 함수에 담고, 상태를 변경할 때는 액션을 전달하여 상태업데이트를 진행한다.
이 방식은 나중에 다룰 내용인 리덕스와 매우 유사하지만, 전역 상태에서 사용하는 리덕스와 달리 useReducer 는 컴포넌트 내부에서만 사용할 수 있고, 설정이 간단하다.
✋🏻 reduce 함수란 뭘까?
useState사용 형태를 보면const [state, setState]라고해서 상태를 관리하는 함수인setState가 있다.
reduce가useReducer에서는setState의 역할을 하는 것이다.
즉, 컴포넌트 내부에 있는 state 로직을reduce가 받는것이다.
// 형태
const [state, dispatch] = useReducer(reducer, initialArg, init?)
위와같이 생긴 useReducer 는 다음과 같이 컴포넌트 최상위에서 호출하고 사용(state 관리)한다.
// useReducer 호출
import { useReducer } from 'react'
// reducer 함수 선언
function reducer(state, action) {
// ...
}
function Component() {
const [state, dispatch] = useReducer(reducer, { key: value })
// ...
위의 기본형태를 활용하여 어떻게 사용하는지에 대한 간단한 예시는 다음과 같다.
import { useReducer } from 'react'
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 })
return (
<div className="counter">
<p>카운트: {state.count}</p>
<button
type="button"
onClick={() => dispatch({ type: 'decrement' })}
aria-label="카운트 감소"
>
-
</button>
<button
type="button"
onClick={() => dispatch({ type: 'increment' })}
aria-label="카운트 증가"
>
-
</button>
</div>
)
}
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 }
case 'decrement': return { count: state.count - 1 }
default: return state
}
}
핵심은 상태를 변경하려면
dispatch함수를 사용해야하고, 실제 상태 변경 로직은 전부reducer함수 내부에 모여있다는 점이다.
1️⃣ reducer
순수함수 여야한다.state 와 action 을 받으며, 다음 state 를 반환해야한다.function (state, action) {
swtich(action.type) {
case 'added_todo" : {
return {
...state,
todos: [
...state.todos,
{id: nextId++, text: action.text}
]
}
}
// ...
}
}
2️⃣ initialArg
init 인수에 달렸다.3️⃣ (optional)init
초기화 함수이다.initialArg 로 설정된다.init(initialArg) 를 호출한 결과가 할당된다.2개의 엘리먼트로 구성된 배열을 반환한다.
1️⃣ 현재 state: 첫 번째 렌더링에서의 state는 init(initialArg)의 결과 또는 initialArg로 설정된다. (읽기전용으로 변경할 수 없다 ❌)
2️⃣ dispatch 함수: state를 새로운 값으로 업데이트하고 리렌더링을 일으킨다. (상태변경함수)
⇒ 화면을 업데이트하려면 action 객체(사용자가 수행한 활동)를 인수로하여 dispatch함수를 호출해야한다.
✋🏻 dispatch란?
useReducer에 의해 반환되는 함수이며, state를 새로운 값으로 업데이트하고 리렌더링을 일으킨다.dispatch함수의 유일한 인수는action이며,dispatch함수는 어떤 값도 반환하지 않는다. → 다음 렌더링에 사용할 state 변수 업데이트만 진행한다.dispatch함수 호출은 현재 동작하고 있는 코드의 state를 변경하지 않는다. → 함수 호출 직후에 state변수를 읽으면 호출 이전의 최신화되기전 값을 참조하게 된다.dispatch함수의 매개변수인action은 사용자에 의해 수행된 활동이며, 모든 데이터 타입이 할당될 수 있다.action은 객체형태로 이루어져있으며,type프로퍼티와payload프로퍼티로 구성되어있다.action의type프로퍼티이름은 컴포넌트 내에 단일 상호작용을 설명하는 지역적 특성을 가지고 있다.// action 형태 type Action = {type: /*액션이 무엇인지 정의*/, payload: ...} // dispatch 형태 const [state, dispatch] = useReducer(reducer, { key: value }) function handleClick() { dispatch({type: action.type /*reducer가 다음 state를 계산하는데 필요한 정보*/}) // ...
useReducer 잘 작성하는 방법
1. 반드시 순수해야 한다.
reducer 는 렌더링 중에 실행된다. (action은 다음 렌더링까지 대기) → 동일입력 동일출력이 이루어져야한다. ⇒ 순수해야 한다. 2. action 발생으로 인해 여러 데이터가 변경되더라도, 모든 데이터 변경을 포함하는 하나의 명확한 상호작용을 나타내야한다.
action 을 reducer에 기록할 경우, 어떤 상호작용이나 응답이 어떤 순서로 일어났는지를 재구성할 수 있을 정도로 로그가 명확해야 한다. → 디버깅이 용이해진다.reducer가 관리하는 5개의 필드가 있다. 해당 양식에서 '재설정' 버튼을 눌렀을 때, 각각의 필드에서 action 을 발생시키는 것 보다는 하나의 action 을 전송하는 것이 더 합리적이다.useState vs. useReducer 비교표| 비교항목 | useState | useReducer |
|---|---|---|
| 코드크기 | 간단명료 → 단순한 상태에 적합 | 작성해야하는 코드양 많음 → 여러 이벤트가 상태를 바꾸는 등의 복잡한 상태관리에서는 오히려 코드가 간결해짐 |
| 가독성 | 상태가 단순하거나 컴포넌트가 작을 때 가독성이 좋음 | 상태가 복잡하거나 컴포넌트의 규모가 커질수록 가독성이 좋음 |
| 디버깅 | 잘못된 위치와 이유 파악이 어려움 | 모든 상태 업데이트와 액션이 한 곳(리듀서)에 모여있어 파악이 쉬움 |
| 테스트 | 상태 업데이트 로직이 컴포넌트에 의존하고있어 테스트가 어려움 | 로직(리듀서)이 컴포넌트에 의존하지않는 순수함수로, 독립적으로 분리되어있어 테스트하기에 용이함 |
| 선호도 (취향에 맞게 선택가능) | 직관적이며 익숙한 방식 | 구조적이며 명확한 방식 |
| 혼용 가능여부 | 다른 hook과 함께 사용가능 / useReducer 와 혼용가능 | 다른 hook과 함께 사용가능 / useState 와 혼용가능 |
여기서 조금 더 파고들어가본다면, 배열을 사용해서 리액트 상태를 관리할 때 useState 대신 useImmer 로 사용이 가능하다고 했다.
Immer 라이브러리를 이용하여 일반적인 state에서 객체와 배열을 업데이트 했던 것처럼, Immer 라이브러리를 사용하면 reducer 를 더 간결하게 작성할 수 있다.
Immer 라이브러리에서 제공하는 useImmerReducer 를 import 하여 push 또는 arr[i] = 으로 값을 할당함으로써 state 변경이 가능하다.
그런데 여기서 드는 의문은ruducer는 순수함수라고 했는데, state 변경을 하면 동일입력 동일출력이 이루어지지않지 않을까?
⇒ Immer 는 내부적으로 변경사항이 반영된 draft 를 이용하여 state 복사본을 생성한다. 그렇기때문에 state 변경이 가능한 것이다.
🔗 참고자료
https://ko.react.dev/reference/react/useReducer
https://ko.react.dev/learn/extracting-state-logic-into-a-reducer