ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • React에서 XState 간단 사용 후기 😊
    React 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

     

Designed by Tistory.