React 상태 관리와 State 패턴이 만나는 지점

React의 useState·useReducer·Context 기반 상태 관리와 GoF State 패턴의 상태 전이 구조를 비교하고, 전이 명시화와 불변성 전략의 실무 적용을 코드로 정리한다.

2026-08-13 · 최초 발행 2025-10-14

React 컴포넌트의 상태 관리와 GoF의 State 패턴은 서로 다른 시대, 다른 언어 패러다임에서 나왔지만 풀고 있는 문제는 같다 — "지금 이 상태에서 무엇을 할 수 있는가"를 코드로 명시하는 일이다. 이 글은 두 모델을 나란히 놓고 어디가 같고 어디가 갈리는지를 정리한다.

React 상태 관리와 State 패턴이 정의하는 것

React 상태 관리는 컴포넌트 로컬 상태(useState), 리듀서(useReducer), 전역 컨텍스트(Context), 외부 스토어(Redux/Zustand 등)로 구성되는 상태 보유·전이 체계다. 입력 이벤트 → 상태 전이 → 렌더링 → 부수효과 실행이라는 선언적 파이프라인을 지향한다.

State 패턴은 객체 내부 상태에 따라 행위를 교체하는 행동 패턴이다. Context 객체가 State 인터페이스에 위임하고, 구체 상태 클래스가 전이와 행위를 캡슐화하는 구조다.

두 모델의 공통 핵심은 상태 전이의 명시화, 이벤트 드리븐 처리, 전이 규칙(전이표 또는 리듀서)의 단일 책임화다. 불변이냐 가변이냐의 전략 차이는 존재하지만, "합법 전이만 허용한다"는 일관성 목적은 공유한다.

상태 캡슐화·전이·불변성이 실제로 갈리는 지점

상태 캡슐화와 단일 책임에서, React는 상태와 렌더링을 분리하고 전이는 리듀서·세터로 집중시킨다. UI와 도메인 규칙을 분리해 테스트 용이성을 높인다. State 패턴은 전이를 구체 상태 클래스가 담당하고, Context는 위임과 현재 상태 보유에 집중한다.

전이와 이벤트 드리븐 메커니즘도 방식이 다르다. React는 액션/이벤트 → 리듀서 → 새 상태 → 렌더링의 단방향 데이터 흐름을 따르고, 미들웨어나 이펙트로 비동기 전이를 조율한다. State 패턴은 메서드 호출을 통해 전이하고, 상태 객체 자체를 교체해 행위를 바꾼다.

불변성과 가변 전략의 차이가 가장 크다. React는 불변 업데이트를 권장해 디퓨징·메모화 최적화가 쉽고 디버깅·타임트래블도 가능하다. State 패턴은 상태 객체 교체(가변 Context)를 허용하는 대신 객체 수명 관리와 참조 무결성에 주의해야 한다.

컨텍스트 구조도 갈린다. React는 Provider/Consumer로 의존 경로를 명시하고 스코프 기반으로 전파하며 성능 최적화가 필요하다. State 패턴에서는 Context가 상태를 보유·교체하며 외부 의존성 주입 지점으로도 활용된다.

부수효과 관리에서는 React가 useEffect·Redux-Thunk·Saga 등으로 I/O와 전이를 분리하고 재시도·취소·동시성을 체계화한다. State 패턴은 상태 객체 내부나 Context 계층에서 I/O를 실행하며, 전이 원자성을 보장하기 위한 예외 처리 설계가 필요하다.

실무에서 반복되는 적용 지점

폼 마법사·다단계 입력 흐름에서는 idle→editing→validating→submitting→success/error 같은 단계 상태를 정의하고, 전이표 기반으로 유효성·전송 에러 처리의 일관성을 확보한다. React 리듀서나 XState 같은 FSM 라이브러리를 적용한다.

비동기 요청·취소·재시도 흐름에서는 loading/success/error/empty 같은 표준 상태 세트를 정의한다. fetch → resolve/reject 전이를 명시하고 취소 토큰·디바운스를 포함시키며, Redux-Saga 채널이나 AbortController로 경쟁 상태를 방지한다.

권한·플래그 기반 UI 전환에서는 feature flag나 role에 따른 상태 전이 가드를 적용한다. 미노출·회색 처리·대체 플로우로 UX 일관성을 확보하고, 테스트에서 전이 경로 커버리지를 측정해 회귀를 방지한다.

코드로 본 React 상태 관리와 State 패턴

환경 전제는 Node.js 18+, React 18+, TypeScript 5(선택)이며, 네트워크 의존성 제거를 위해 모킹을 사용한다.

React의 useReducer로 FSM을 구현하면 다음과 같다.

import React, { useEffect, useReducer } from "react";

type Status = "idle" | "loading" | "success" | "error";
type State = { status: Status; data?: string; error?: string };
type Event =
  | { type: "FETCH" }
  | { type: "RESOLVE"; data: string }
  | { type: "REJECT"; error: string };

const reducer = (state: State, event: Event): State => {
  switch (event.type) {
    case "FETCH":
      if (state.status !== "idle" && state.status !== "error") return state;
      return { status: "loading" };
    case "RESOLVE":
      if (state.status !== "loading") return state;
      return { status: "success", data: event.data };
    case "REJECT":
      if (state.status !== "loading") return state;
      return { status: "error", error: event.error };
    default:
      return state;
  }
};

const mockApi = () =>
  new Promise<string>((resolve, reject) => {
    setTimeout(
      () => (Math.random() > 0.2 ? resolve("OK") : reject("FAIL")),
      400,
    );
  });

export default function App() {
  const [state, dispatch] = useReducer(reducer, { status: "idle" });

  useEffect(() => {
    let cancelled = false;
    if (state.status === "loading") {
      mockApi()
        .then((d) => !cancelled && dispatch({ type: "RESOLVE", data: d }))
        .catch(
          (e) => !cancelled && dispatch({ type: "REJECT", error: String(e) }),
        );
    }
    return () => {
      cancelled = true;
    };
  }, [state.status]);

  return (
    <div>
      <p>Status: {state.status}</p>
      {state.status === "success" && <p>Data: {state.data}</p>}
      {state.status === "error" && <p>Error: {state.error}</p>}
      <button
        onClick={() => dispatch({ type: "FETCH" })}
        disabled={state.status === "loading"}
      >
        Fetch
      </button>
    </div>
  );
}

이 구현은 합법 전이만 허용하는 가드를 각 케이스에 넣고, 부수효과는 useEffect로 분리하며, 취소 플래그로 언마운트 시 경쟁 상태를 예방한다.

같은 문제를 TypeScript로 State 패턴 등가 모델로 풀면 이렇게 된다.

// ts-node ≥10 또는 tsc target ES2020
type IO = { fetch(): Promise<string> };

interface IState {
  enter?(ctx: Context): void;
  fetch(ctx: Context): void;
  resolve(ctx: Context, data: string): void;
  reject(ctx: Context, error: string): void;
}

class Context {
  constructor(
    public io: IO,
    private _state: IState = new Idle(),
  ) {}
  data?: string;
  error?: string;
  transition(s: IState) {
    this._state = s;
    s.enter?.(this);
  }
  fetch() {
    this._state.fetch(this);
  }
  resolve(d: string) {
    this._state.resolve(this, d);
  }
  reject(e: string) {
    this._state.reject(this, e);
  }
}

class Idle implements IState {
  fetch(ctx: Context) {
    ctx.transition(new Loading());
  }
  resolve() {}
  reject() {}
}

class Loading implements IState {
  enter(ctx: Context) {
    ctx.io.fetch().then(
      (d) => ctx.resolve(d),
      (e) => ctx.reject(String(e)),
    );
  }
  fetch() {}
  resolve(ctx: Context, data: string) {
    ctx.data = data;
    ctx.transition(new Success());
  }
  reject(ctx: Context, error: string) {
    ctx.error = error;
    ctx.transition(new Failure());
  }
}

class Success implements IState {
  fetch() {}
  resolve() {}
  reject() {}
}
class Failure implements IState {
  fetch(ctx: Context) {
    ctx.transition(new Loading());
  }
  resolve() {}
  reject() {}
}

// 실행 예
const ctx = new Context({ fetch: () => Promise.resolve("OK") });
ctx.fetch();

여기서는 Context의 transition 메서드가 상태 객체를 교체하는 지점이고, I/O는 Loading.enter에서 실행돼 성공·실패에 따라 다음 상태로 전이한다.

상태가 흐르는 경로

New Stateyessuccesserrorguard: invalid transitionerror: revert & logUser EventDispatch Action/EventReducer/State HandlerState Store/ContextRender/Behavior ChangeSide Effect?Async I/ODispatch RESOLVEDispatch REJECTError Logger

입력은 사용자 이벤트, 네트워크 응답, 타이머다. 처리 단계에서는 전이 가드를 검증하고 리듀서나 상태 객체에 위임하며, 실패 시 롤백·로깅한다. 출력은 UI 렌더링, 후속 이펙트 트리거, 관찰 지표 기록이다.

React 상태 관리와 State 패턴을 나란히 놓고 보면

관점/지표 React 상태 관리 State 패턴 비고
일관성 불변 리듀서로 전이 결정성 확보 전이 메서드로 합법 전이 강제 전이표/타입 강제 시 강화
확장성 액션/케이스 추가로 기능 확장 상태 클래스 추가로 행위 확장 OCP 지향 구조 유사
성능 메모화/분할 렌더링 최적화 용이 객체 교체 비용 경미, UI 없음 UI 프레임워크 최적화 영향 큼
안정성 타입·훅 규칙·DevTools 지원 캡슐화로 사이드이펙트 국소화 예외 처리·락 필요 시 주의
운영 편의 타임트래블/로그·스토어 인스펙트 상태 로그 주입 필요 가시성 도구 성숙도 차이

실무에 가져갈 것

두 모델을 나란히 보면 결론은 하나로 수렴한다 — 전이를 명시화하고 부수효과를 외부화할 때 일관성과 테스트 가능성이 극대화된다는 것이다. 전이 가드와 타입 한정으로 불법 전이를 차단하면 복잡한 흐름 기준으로 결함률이 2040% 감소할 것으로 기대되고(팀·코드베이스에 따라 상이), 새로운 상태·액션을 추가할 때 파급이 최소화돼 평균 변경 영향 범위가 30% 이상 축소될 것으로 기대된다. 이벤트·전이 로그를 일원화하면 장애 재현 시간이 단축돼 MTTR이 1525% 개선될 것으로 기대되고, 불필요한 렌더를 차단하고 경쟁 상태를 줄이면 사용자 체감 지연 스파이크 빈도도 낮아진다.

실무 적용 권장 사항은 전이표 혹은 리듀서 중심 설계, 상태 세트 표준화(idle/loading/success/error), 전이 가드·타입 강화, 이펙트 격리와 로깅 일원화다. 복잡도가 임계점을 넘으면 FSM·XState 같은 전문 도구 도입도 고려할 만하다.

State 패턴React상태 관리유한 상태 기계리듀서