모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

17강 / 전체 30강

Context API

7분 읽기 조회 13

useReducer로 복잡한 상태 로직을 액션 기반으로 관리하는 방법, reducer 패턴의 장점, useState vs useReducer 선택 기준을 실전 예제로 배웁니다.

🤔 언제 useReducer가 필요한가

useState는 단순한 상태 관리에 완벽합니다. 그런데 상태가 복잡해지면 useState만으로 코드가 지저분해집니다. 특히 이런 경우에 useReducer를 고려합니다:

  • 여러 state가 서로 연관되어 함께 업데이트됩니다
  • 다음 상태가 현재 상태의 여러 필드에 의존합니다
  • 상태 업데이트 로직이 여러 핸들러에 분산됩니다
  • 가능한 상태 변경 경우의 수가 명확히 정의됩니다

⚙️ useReducer의 작동 원리

useReducer는 Redux의 아이디어를 리액트에 내장한 것입니다. 핵심 개념:

  • State: 현재 상태 객체
  • Action: 무슨 일이 일어났는지를 설명하는 객체 (type + 선택적 payload)
  • Reducer: (state, action) => newState 형태의 순수 함수
  • Dispatch: 액션을 발생시키는 함수
// 장바구니 상태 관리 예제
interface CartItem {
  id: number
  name: string
  price: number
  quantity: number
}

interface CartState {
  items: CartItem[]
  isOpen: boolean
}

// 액션 타입 정의 (TypeScript discriminated union)
type CartAction =
  | { type: 'ADD_ITEM'; payload: Omit<CartItem, 'quantity'> }
  | { type: 'REMOVE_ITEM'; payload: { id: number } }
  | { type: 'UPDATE_QUANTITY'; payload: { id: number; quantity: number } }
  | { type: 'CLEAR_CART' }
  | { type: 'TOGGLE_CART' }

// 리듀서: 순수 함수 — 항상 새 객체 반환
function cartReducer(state: CartState, action: CartAction): CartState {
  switch (action.type) {
    case 'ADD_ITEM': {
      const existing = state.items.find((item) => item.id === action.payload.id)
      if (existing) {
        return {
          ...state,
          items: state.items.map((item) =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          ),
        }
      }
      return { ...state, items: [...state.items, { ...action.payload, quantity: 1 }] }
    }
    case 'REMOVE_ITEM':
      return { ...state, items: state.items.filter((item) => item.id !== action.payload.id) }
    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map((item) =>
          item.id === action.payload.id
            ? { ...item, quantity: Math.max(0, action.payload.quantity) }
            : item
        ).filter((item) => item.quantity > 0),
      }
    case 'CLEAR_CART':
      return { ...state, items: [] }
    case 'TOGGLE_CART':
      return { ...state, isOpen: !state.isOpen }
    default:
      return state
  }
}

🔌 컴포넌트에서 사용하기

const initialState: CartState = { items: [], isOpen: false }

function ShoppingCart() {
  const [state, dispatch] = useReducer(cartReducer, initialState)

  const totalPrice = state.items.reduce(
    (sum, item) => sum + item.price * item.quantity, 0
  )

  return (
    <div>
      <button onClick={() => dispatch({ type: 'TOGGLE_CART' })}>
        장바구니 {state.items.length > 0 && `(${state.items.length})`}
      </button>

      {state.isOpen && (
        <div className="border rounded p-4">
          {state.items.length === 0 ? (
            <p>장바구니가 비었습니다</p>
          ) : (
            <>
              {state.items.map((item) => (
                <div key={item.id} className="flex items-center justify-between py-2">
                  <span>{item.name}</span>
                  <div className="flex items-center gap-2">
                    <button onClick={() => dispatch({ type: 'UPDATE_QUANTITY', payload: { id: item.id, quantity: item.quantity - 1 } })}>-</button>
                    <span>{item.quantity}</span>
                    <button onClick={() => dispatch({ type: 'UPDATE_QUANTITY', payload: { id: item.id, quantity: item.quantity + 1 } })}>+</button>
                    <button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: { id: item.id } })}>삭제</button>
                  </div>
                </div>
              ))}
              <div className="border-t pt-2 font-bold">합계: {totalPrice.toLocaleString()}원</div>
              <button onClick={() => dispatch({ type: 'CLEAR_CART' })}>전체 비우기</button>
            </>
          )}
        </div>
      )}
    </div>
  )
}

📊 useState vs useReducer — 선택 기준

기준useStateuseReducer
상태 복잡도단순한 값(숫자, 문자열, 불리언)연관된 여러 필드가 있는 객체
업데이트 패턴독립적인 단일 값 변경여러 필드가 함께 바뀌는 경우
가능한 액션명확하지 않음명확히 정의된 액션 목록이 있음
테스트컴포넌트 단위리듀서 함수 단독 테스트 가능

✅ 정리

  • useReducer: (state, action) => newState 패턴으로 복잡한 상태 관리.
  • dispatch({ type: 'ACTION_TYPE', payload: ... })로 상태 변경을 요청합니다.
  • 리듀서는 순수 함수여야 합니다 — 항상 새 객체를 반환하세요.
  • TypeScript discriminated union으로 액션 타입을 정의하면 타입 안전성이 높아집니다.

관련 주제

  • Props Drilling 문제
  • createContext
  • Provider 패턴
  • useContext
  • 전역 테마 사용자값
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 리액트 입문 — 컴포넌트로 만드는 웹
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

리액트 입문 — 컴포넌트로 만드는 웹

이 강의가 속한 시리즈는 총 30강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.

댓글

0/1000

불러오는 중...