C.W.K.
Stream
Lesson 06 of 07 · published

useReducer — state 에 동사가 붙을 때

~12 min · usereducer, reducer, state-machine

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
useReducer = 의도 붙은 useState. setX(newValue) 호출이 컴포넌트에 흩뿌려지는 대신, 이름 붙은 action ({ type: 'INCREMENT' }) 을 dispatch 하고 단일 reducer 함수가 state 진화 방식 결정.

useState 에서 갈아탈 시점

컴포넌트가 useState 를 벗어났다는 표시 셋:

  1. 패턴으로 같이 업데이트 되는 state 값 여러 개 (예: 'status 가 error 로 뒤집히면 data 도 비우고 retry count 도 셋').
  2. 같은 논리적 이동이어야 할 부분 업데이트들이 핸들러 한 다스에 흩뿌려진 setter.
  3. Setter 호출로 묘사 하는 대신 — 'submit_started', 'submit_succeeded', 'submit_failed' — 처럼 이름 짓고 싶은 state 전환.

모양

const [state, dispatch] = useReducer(reducer, initialState). Reducer 는 순수 함수: (state, action) => nextState. 새 state 객체 반환해야 — in-place mutate 는 useState 와 같은 버그.

Discriminated-union action

useReducer 를 Track 2 lesson 5 의 discriminated union 과 결합: 각 action type 이 자기 타입 잡힌 payload 운반. Reducer 의 switch 가 action type narrow, TypeScript 가 모든 케이스 핸들링 보장.

useReducer 로 손이 안 가야 할 때

State 가 단일 숫자, 단일 문자열, 단일 boolean, 또는 무관 값 둘 — useState. useReducer 는 state 에 동사 (이름 붙은 방식으로 바꾸는 action) 가 있을 때 이김 — 명사 만 있을 때 아님.

Reducer 는 순수여야. 같은 state + action 주면 항상 같은 next state 반환. fetch 호출 없음, 타임스탬프 있는 console.log 없음, Math.random() 없음. Side effect 는 이벤트 핸들러 (그 후 dispatch) 또는 effect 에 — 절대 reducer 안에 아님.

Code

useReducer + discriminated union 으로 form 제출 state machine·tsx
import { useReducer } from "react";

type State =
  | { status: "idle" }
  | { status: "submitting"; draft: { title: string } }
  | { status: "succeeded"; result: { id: string } }
  | { status: "failed"; error: string; draft: { title: string } };

type Action =
  | { type: "submit_started"; draft: { title: string } }
  | { type: "submit_succeeded"; result: { id: string } }
  | { type: "submit_failed"; error: string }
  | { type: "reset" };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "submit_started":
      return { status: "submitting", draft: action.draft };
    case "submit_succeeded":
      return { status: "succeeded", result: action.result };
    case "submit_failed":
      // Submitting state 의 draft 재사용 (있으면).
      if (state.status !== "submitting") return state;
      return { status: "failed", error: action.error, draft: state.draft };
    case "reset":
      return { status: "idle" };
    default: {
      // Exhaustiveness 체크 — 새 action type 추가하고 여기서 핸들 안 하면
      // 컴파일 fail.
      const _exhaustive: never = action;
      return state;
    }
  }
}

export function NewPostForm() {
  const [state, dispatch] = useReducer(reducer, { status: "idle" });
  // ... 핸들러들이 dispatch({ type: 'submit_started', draft }) 등 호출.
  return null;
}
같은 카운터의 useReducer vs useState — 각각 이기는 자리·tsx
// useState 가 이김 — 단일 값, 이름 붙은 전환 없음.
function CounterA() {
  const [count, setCount] = useState(0);
  return (
    <>
      <button onClick={() => setCount((n) => n + 1)}>+</button>
      <span>{count}</span>
      <button onClick={() => setCount((n) => n - 1)}>-</button>
    </>
  );
}

// useReducer 가 이김 — state 에 규칙 (0 미만 금지, jump 가 리셋).
type CountState = { value: number };
type CountAction =
  | { type: "inc" }
  | { type: "dec" }
  | { type: "jump"; to: number };
function CounterB() {
  const [state, dispatch] = useReducer(
    (s: CountState, a: CountAction): CountState => {
      switch (a.type) {
        case "inc": return { value: s.value + 1 };
        case "dec": return { value: Math.max(0, s.value - 1) }; // floor
        case "jump": return { value: a.to };
      }
    },
    { value: 0 }
  );
  return (
    <>
      <button onClick={() => dispatch({ type: "inc" })}>+</button>
      <span>{state.value}</span>
      <button onClick={() => dispatch({ type: "dec" })}>-</button>
      <button onClick={() => dispatch({ type: "jump", to: 100 })}>jump</button>
    </>
  );
}

External links

Exercise

useState 기반 todo 리스트 (todos, filter, editingId 독립 추적) 를 useReducer 로 변환 — action type 예: add, toggle, remove, set_filter, start_edit, commit_edit, cancel_edit. Action type 에 discriminated union 사용. 컴포넌트 로직이 setter 호출 시퀀스보다 사용자의 mental model (이름 붙은 동사) 처럼 읽히는지 확인.
Hint
컴포넌트가 처리하는 모든 state-mutate 이벤트 나열부터. 각각이 action type 하나. Reducer 의 switch 가 이벤트당 case 하나.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.