[멋쟁이사자처럼 부트캠프 프론트엔드 14기/REACT] useReducer Hook

minji·2025년 9월 12일
post-thumbnail

"관리해야하는 상태가 많아지고 힘들어질 땐 나를 찾아와!"


'상태관리하는 훅' 이라고하면 자연스럽게 useState 를 떠올리게 된다.
하지만, 관리해야하는 상태의 규모가 커지고 복잡해진다면 useState 만 사용해서 관리하기엔 (코드가독성저하, 유지보수등 여러 관리측면으로)어려워 질것이다.

그럴때 사용하는 것이 useReducer 라는 훅이다.

useReducer란?

| 컴포넌트에 reducer 함수를 추가하는 React Hook이다.

이 훅을 사용하면 상태와 상태를 변경하는 로직을 분리하여 관리할 수 있다.

상태가 어떻게 변해야하는지의 규칙리듀서 함수에 담고, 상태를 변경할 때는 액션을 전달하여 상태업데이트를 진행한다.
이 방식은 나중에 다룰 내용인 리덕스와 매우 유사하지만, 전역 상태에서 사용하는 리덕스와 달리 useReducer 는 컴포넌트 내부에서만 사용할 수 있고, 설정이 간단하다.

✋🏻 reduce 함수란 뭘까?
useState 사용 형태를 보면 const [state, setState] 라고해서 상태를 관리하는 함수인 setState 가 있다.
reduceuseReducer 에서는 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 업데이트를 진행하는 함수로 반드시 순수함수 여야한다.
  • 인수로 stateaction 을 받으며, 다음 state 를 반환해야한다.
  • 인수에는 모든 데이터타입이 할당가능하다.
  • 실제 상태변경 로직이 모여있는 곳
function (state, action) {
  swtich(action.type) {
    case 'added_todo" : {
    	return {
    		...state,
    		todos: [
              ...state.todos,
              {id: nextId++, text: action.text}
            ]
  		}
	}
	// ...
  }
}

2️⃣ initialArg

  • 초기 state 가 계산되는 값이다.
  • 모든 데이터타입이 할당될 수 있다.
  • 초기 state 가 어떻게 계산되는지는 다음 init 인수에 달렸다.

3️⃣ (optional)init

  • 초기 state를 반환하는 초기화 함수이다.
  • 선택사항인만큼 해당 인수가 할당되지 않았을 경우의 초기 state는 initialArg 로 설정된다.
  • 인수로 할당이 되었다면 초기 state는 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 프로퍼티로 구성되어있다.
  • actiontype 프로퍼티이름은 컴포넌트 내에 단일 상호작용을 설명하는 지역적 특성을 가지고 있다.
// 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 발생으로 인해 여러 데이터가 변경되더라도, 모든 데이터 변경을 포함하는 하나의 명확한 상호작용을 나타내야한다.

  • 모든 actionreducer에 기록할 경우, 어떤 상호작용이나 응답이 어떤 순서로 일어났는지를 재구성할 수 있을 정도로 로그가 명확해야 한다. → 디버깅이 용이해진다.
  • 예: reducer가 관리하는 5개의 필드가 있다. 해당 양식에서 '재설정' 버튼을 눌렀을 때, 각각의 필드에서 action 을 발생시키는 것 보다는 하나의 action 을 전송하는 것이 더 합리적이다.

useState vs. useReducer 비교표

비교항목useStateuseReducer
코드크기간단명료 → 단순한 상태에 적합작성해야하는 코드양 많음 → 여러 이벤트가 상태를 바꾸는 등의 복잡한 상태관리에서는 오히려 코드가 간결해짐
가독성상태가 단순하거나 컴포넌트가 작을 때 가독성이 좋음상태가 복잡하거나 컴포넌트의 규모가 커질수록 가독성이 좋음
디버깅잘못된 위치와 이유 파악이 어려움모든 상태 업데이트와 액션이 한 곳(리듀서)에 모여있어 파악이 쉬움
테스트상태 업데이트 로직이 컴포넌트에 의존하고있어 테스트가 어려움로직(리듀서)이 컴포넌트에 의존하지않는 순수함수로, 독립적으로 분리되어있어 테스트하기에 용이함
선호도 (취향에 맞게 선택가능)직관적이며 익숙한 방식구조적이며 명확한 방식
혼용 가능여부다른 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

profile
FE Developer .mjs

0개의 댓글