React

React에서 XState 간단 사용 후기 😊

citron031 2025. 2. 13. 22:58

 

XState는 점점 더 단순화되어가는 상태관리 라이브러리들 중에서 좀 더 명확하게 역할을 나누고 책임을 부여한 컨셉의 상태관리 라이브러리이다.

 

❗ 이런 특징이 redux의 구조와 비슷하게 느껴지기도 한데, 이에 관해서는 직접 제작자가 밝힌 차이점이 있다.

https://stackoverflow.com/questions/54482695/what-is-an-actual-difference-between-redux-and-a-state-machine-e-g-xstate

 

What is an actual difference between redux and a state machine (e.g. xstate)?

I am working on investigation of one front-end application of medium complexity. At this moment it is written in pure javascript, it has a lot of different event-based messages connecting few main ...

stackoverflow.com

위의 글과 공식문서도 참조하고, 간단하게 예제를 사용하여 XState를 사용해본 뒤 XState는 어떤 장점으로 선택할지 고민해봤다 🙌

 

XState 사용 예제 (모달 상태 관리) 🙏

실전적인 예제를 작성하기 위해서, 모달을 만들 때 XState를 활용해보았다.

(🍔 예제에 사용된 버전은 "xstate": "^5.19.2, "@xstate/react": "^5.0.2" 입니다)

npm install xstate @xstate/react
yarn add xstate @xstate/react

react에서 활용하기 위해서 hook을 제공해주는 @xstate/react도 함께 설치한다.

 

그리고 타입스크립트를 사용한다면, 다음과 같이 tsconfig.json 설정을 해주자.

(xstate의 권장사항이다)

{
  compilerOptions: {
    strictNullChecks: true,
    skipLibCheck: true,
  },
}

 

타입스크립트 여부에 따라서 코드 작성도 좀 달라지는데, typescript를 사용하지 않는다면 createMachine으로 충분하지만, typescript 사용시 setup를 사용해 createMachine을 하는 것을 추천하고 있다.

(createMachine 에서도 타입을 작성할 수 있긴 하다 😅)

import { createMachine } from 'xstate';

// js
export const modalMachine = createMachine({
  id: 'modal',
  initial: 'close',
  states: {
    close: {
      on: { OPEN: 'opene' },
    },
    open: {
      on: { CLOSE: 'close' },
    },
  },
});

 js에서는 이렇게 작성해도 괜찮지만, ts에서는 아래처럼 작성하는 것이 권장사항이다.

import { assign, setup } from 'xstate';

export const modalMachine = setup({
  types: {
    context: {} as { isOpen: boolean },
    events: {} as { type: 'close' } | { type: 'open' },
  },
  actions: {
    setOpen: assign({ isOpen: (_ctx, _event) => true }),
    setClose: assign({ isOpen: (_ctx, _event) => false }),
  },
}).createMachine({
  context: { isOpen: false },
  initial: 'closed',
  states: {
    closed: {
      on: { open: { target: 'open', actions: 'setOpen' } },
    },
    open: {
      on: { close: { target: 'closed', actions: 'setClose' } },
    },
  },
});

 

위와 같이 코드를 작성하고, 실제 해당 상태를 사용하는 페이지를 아주 간단하게 만들면 다음과 같이 모달 컴포넌트 상태를 작성할 수 있다.

'use client';
import { useMachine } from '@xstate/react';

import { modalMachine } from './store';

const ModalXstate = () => {
  const [state, send] = useMachine(modalMachine);

  return (
    <div>
      <button
        onClick={() =>
          send({
            type: 'open',
          })
        }
      >
        모달 열기
      </button>
      {state.context.isOpen && (
        <div className="modal">
          <p>모달 내용</p>
          <button
            onClick={() =>
              send({
                type: 'close',
              })
            }
          >
            모달 닫기
          </button>
        </div>
      )}
    </div>
  );
};

export default ModalXstate;

 

이제 코드를 작동해보면, 실제로 잘 작동함을 확인할 수 있다.

요즘 많이 선택되는 zustand나 jotai와 비교하면, 생각보다 초기 설정이 복잡하다.

그렇다면, 왜 그렇게 복잡한 xstate를 사용해야 할까?

하는 생각이 절로 드는데, 이에 대해서 조사한뒤 직접 사용하고 생각이 난 점들에 대해서 정리해봤다.

 

요약하자면, 다양한 상태변화가 있는 복잡한 상태관리 로직이 있을 때, XState를 사용하면 명확하게 상태를 관리할 수 있다 👍

XState를 선택해야 하는 이유 ✨

  1. 명확한 상태 모델링
    • 상태를 Finite State Machine(FSM, 유한 상태 기계) 개념으로 관리하기 때문에, 컴포넌트가 어떤 상태에 있을지 한눈에 파악할 수 있다.
      • 예를 들어, 모달이 open인지 close인지, 버튼이 loading인지, success인지 상태가 확실하게 정의된다.
  2. 이벤트 기반 전이 (Event-Driven State Transitions)
    • onClick 같은 이벤트를 단순히 상태 변경하는 게 아니라, 이벤트를 기반으로 상태가 변한다.
      • 예를 들어, FETCH 이벤트가 발생하면 loading -> success | failure 상태로 이동할 수 있다.
  3. Redux보다 간결한 상태 관리
    • Redux처럼 전역 상태 관리가 필요한 경우에도 사용 가능하지만, Redux 보다는 코드가 훨씬 간결하다.
      • Reducer에서 switch-case 남발하는 대신, 한눈에 보기 좋은 트랜지션으로 정리할 수 있다.
  4. 비동기 작업 (Promise & Actor) 관리 편리
    • invoke를 사용하면 비동기 API 호출을 자연스럽게 상태 흐름에 녹일 수 있다.
      • useEffect 없이도 비동기 처리 가능해서, 클린한 코드를 유지할 수 있다.
  5. 불가능한 상태 방지
    • Redux에서는 개발자가 직접 불가능한 상태(예: loading 상태에서 reset이 호출되는 경우)를 방지해야 하지만, XState는 Statechart 개념을 사용하여 자연스럽게 이를 차단한다.
  6. 상태 전이 시각화 가능
    • XState는 Statechart Visualizer 를 제공하여 상태 전이를 시각적으로 확인할 수 있다.
  7. 구조적인 상태 관리
    • Redux는 전역 상태를 관리하는 방식이지만, XState는 여러 개의 독립적인 상태 머신을 Actor 모델 기반으로 구성할 수 있어, 복잡한 UI에서도 확장성이 뛰어나다.

 

4번에서 비동기 상태 호출에 대해서 invoke를 언급했는데, 실제 invoke의 작동을 테스트해본 코드는 다음과 같다.

import { assign, fromPromise, setup } from 'xstate';

type ApiResult = {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
};

const fakeApiCall = (userId: number): Promise<ApiResult> =>
  fetch(`https://jsonplaceholder.typicode.com/todos/${userId}`).then((response) => response.json());

export const todoMachine = setup({
  types: {
    context: {} as {
      result: ApiResult;
      error: unknown;
    },
    // FETCH 이벤트에 userId가 포함되고, RETRY 이벤트도 정의합니다.
    events: {} as { type: 'FETCH'; userId: number } | { type: 'RETRY' },
  },
  actors: {
    // fromPromise를 사용하여 비동기 actor를 정의합니다.
    // 이 actor는 input으로 { userId: number } 타입의 값을 받습니다.
    fetchTodo: fromPromise<unknown, { userId: number }>(async ({ input }) => {
      const user = await fakeApiCall(input.userId);
      return user;
    }),
  },
}).createMachine({
  id: 'todo',
  initial: 'idle',
  context: {
    result: {
      userId: 1,
      id: 0,
      title: '',
      completed: false,
    },
    error: undefined,
  },
  states: {
    idle: {
      on: {
        FETCH: { target: 'loading' },
      },
    },
    loading: {
      invoke: {
        id: 'getTodo',
        src: 'fetchTodo',
        // event.type이 FETCH일 때는 event.userId를 input으로 사용하고, 그렇지 않으면 userId를 1로 사용합니다.
        input: ({ event }) => {
          if (event.type === 'FETCH') {
            return { userId: event.userId };
          }
          return { userId: 1 };
        },
        onDone: {
          target: 'success',
          actions: assign({
            result: ({ event }) => event.output as ApiResult,
          }),
        },
        onError: {
          target: 'failure',
          actions: assign({
            error: ({ event }) => event.error,
          }),
        },
      },
    },
    success: {},
    failure: {
      on: {
        RETRY: { target: 'loading' },
      },
    },
  },
});

위와 같이 상태를 정의하고,

<div>
  <button
    onClick={() => {
      sendApi({
        type: 'FETCH',
        userId: 1,
      });
    }}
  >
    Call Api
  </button>
  {apiState.value !== 'success' ? (
    <div>Loading...</div>
  ) : (
    <div>
      <h1>API RESULT</h1>
      <p>{apiState.context.result.title}</p>
    </div>
  )}
</div>

위와 같이 호출했다.

value 값에 loading, success와 같은 값이 들어가므로, 자연스럽게 api 로딩에 따른 화면 지연을 표현할 수 있다.

🚀 XState 요약

장점

  • 복잡한 상태 관리가 눈에 보이게 정리됨
  • useEffect 없이 비동기 작업 가능
  • 불가능한 상태가 원천적으로 차단됨
  • 상태 전이를 시각적으로 확인 가능

단점:

  • 처음 배우는 데 약간의 러닝 커브가 있음 😵‍💫
  • 간단한 상태 관리라면 useState보다 오버킬일 수 있음
  • 현재 디버깅툴이 설치가 안됨?? (DevTools 크롬 익스텐션이 설치가 안됨....)

 

결론적으로, XState는 복잡한 상태를 다룰 때 진짜 강력한 도구이다! 🎯
단순한 상태 관리라면 필요 없지만, 다양한 상태 전이가 필요한 UI에서는 고려할만하다! 🚀

 

+ 다만, 이 글에서는 아주 단순한 예제로 xstate를 다뤄보았고, 모든 기능을 사용해본 것이 아니기에 좀 더 지속적으로 xstate를 다뤄보고 좀 더 테스트해볼 필요가 있을 것 같다.

 

👻 작성한 예제 코드

https://github.com/citron03/practice-next-15/tree/main/app/modal-xstate

 

practice-next-15/app/modal-xstate at main · citron03/practice-next-15

Practice React 19, Next 15 (with react compiler). Contribute to citron03/practice-next-15 development by creating an account on GitHub.

github.com

 

 

😊 참고 자료

https://stately.ai/docs/quick-start

 

Quick start | Stately

Start here to jump straight into XState and Stately Studio.

stately.ai

https://stately.ai/docs/xstate-react

 

@xstate/react | Stately

The @xstate/react package contains hooks and helper functions for using XState with React.

stately.ai

https://stately.ai/docs/typescript#set-up-your-tsconfigjson-file

 

TypeScript | Stately

XState v5 and its related libraries are written in TypeScript, and utilize complex types to provide the best type safety and inference possible for you.

stately.ai

https://ko.wikipedia.org/wiki/%EC%9C%A0%ED%95%9C_%EC%83%81%ED%83%9C_%EA%B8%B0%EA%B3%84

 

유한 상태 기계 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 오토마타 벤 다이어그램 (각 레이어를 클릭하면 해당 글로 이동합니다.) 유한 상태 기계(finite-state machine, FSM) 또는 유한 오토마톤(finite automaton, FA; 복수형: 유한

ko.wikipedia.org