Blog

테크

XState로 FSM 구현하기

#상태관리#조사#xstate

2025-01-20

XState로 FSM 구현하기
🤖

XState로 FSM 구현하기

안녕하세요. 이번 글에서는 XState를 이용해 유한 상태 기계을 구현 방법하는 방법을 소개하고자 합니다.

유한 상태 기계이란 무엇이고, 왜 사용할까?

FSM(Finite State Machine, 유한 상태 기계)은 시스템이 가질 수 있는 상태들과 그 상태들 간의 전이를 명확하게 정의하는 모델입니다. 이를 통해 시스템의 동작을 체계적으로 관리하고 예측할 수 있습니다. 복잡한 시스템에서 FSM을 도입하면 다양한 서비스에 걸쳐 상태를 일관되게 관리할 수 있기 때문에 예기치 않은 동작이나 충돌을 방지할 수 있습니다. 또한 개별 상태에서 처리해야 할 작업이 명확하게 정의되어 있기 때문에 시스템을 유지보수하고 기능을 확장하기도 용이합니다.
 

왜 XState로 FSM을 구현할까?

XState는 JS로 작성된 상태 관리 라이브러리로 FSM과 상위 상태 기계(Hierarchical State Machine)을 지원하여 복잡한 상태 관리를 체계적으로 수행할 수 있게 해 주는 도구입니다. XState에는 아래와 같은 장점이 있습니다.
  • 명확하고 표준화된 상태 정의
    • 상태와 전이를 JSON 또는 TypeScript 형식으로 명시하여 코드의 가독성과 유지보수성을 높입니다.
  • 강력한 시뮬레이션 도구
    • 상태 전이를 시각화하고, 실시간 디버깅을 지원하여 설계된 상태 모델의 동작을 쉽게 테스트할 수 있습니다.
  • 확장 가능성
    • 복잡한 계층 구조나 병렬 상태를 지원하여 다양한 환경에서도 유연하게 활용할 수 있습니다.
 

XState 사용해 보기

그럼 간단한 예시를 하나 만들어 보겠습니다. 최신 버전의 Next.js를 기반으로 작업하며, 이렇게 생긴 문을 하나 만들어 볼 거에요. 예시 코드를 담은 레포지토리가 있으니, 글 끝에서 링크를 확인해 보세요!
notion image
 
  1. 환경 설정하기
    1. 먼저, Next.js 프로젝트를 생성합니다. (이 예시에서는 TypeScript와 tailwind를 사용합니다.)
      npx create-next-app@latest xstate-example
      이제 라이브러리를 의존성으로 추가합니다. @xstate/react 패키지는 React 컴포넌트와 XState 상태 기계을 쉽게 연동할 수 있도록 도와줍니다.
      yarn add xstate @xstate/react
       
  1. 상태 기계 정의하기
    1. 이제 XState로 상태 기계를 정의해 보겠습니다. XState에서 상태 기계는 createMachine 함수로 정의합니다. 가장 간단한 machineConfig는 아래와 같은 모습입니다. 최초 상태와 개별 상태에서의 이벤트와 전이가 명시되어 있습니다. fooMacine이 실행되면 INITIAL 상태(State)로 진입하고, INITIAL 상태의 fooMachineEVENT_A라는 이벤트(Event)가 발생하면 NEXT 상태로 전이(Transition)됩니다. 그럼 이제 위에서 봤던 문을 만들어 볼까요?
      const fooMachine = createMachine({ initial: FooStateType.INITIAL, sates: { FooStateType.INITIAL: { on: { FooEventType.EVENT_A: { target: FooStateType.NEXT } } } // ... other states ... } })
      XState의 Machine configuration
      우리가 만들어 볼 문에는 열림, 닫힘, 잠김의 세 가지 상태가 있습니다. 이 상태 기계에는 closed, open, locked라는 세 가지 상태가 있고, 각 상태는 open, close, lock, unlock 이벤트를 통해 전이됩니다.
      예시로 만들 Door state machine의 모식도
      예시로 만들 Door state machine의 모식도
      import { setup } from 'xstate'; export enum DoorEventType { OPEN = 'open', CLOSE = 'close', LOCK = 'lock', UNLOCK = 'unlock', } export enum DoorStateType { CLOSED = 'closed', OPEN = 'open', LOCKED = 'locked', } export const doorMachine = setup({ types: { events: {} as { type: DoorEventType }, }, }).createMachine({ id: 'door-state-machine', context: {}, initial: DoorStateType.CLOSED, states: { [DoorStateType.CLOSED]: { on: { [DoorEventType.OPEN]: { target: DoorStateType.OPEN }, [DoorEventType.LOCK]: { target: DoorStateType.LOCKED }, }, }, [DoorStateType.OPEN]: { on: { [DoorEventType.CLOSE]: { target: DoorStateType.CLOSED }, }, }, [DoorStateType.LOCKED]: { on: { [DoorEventType.UNLOCK]: { target: DoorStateType.CLOSED }, }, }, }, });
      Door state machine configuration
      타입 관리를 위해 setup({ … })을 추가해 주었습니다. setup함수는 상태 기계에서 사용되는 타입을 안전하게 관리할 수 있도록 도와줍니다.
       
  1. 상태 기계 작동 확인하기
    1. VSCode에는 XState 상태 기계을 시각적으로 확인하고 시뮬레이션할 수 있는 확장 기능이 있습니다. 이 확장 기능을 사용하면 상태 기계의 동작을 쉽게 확인하고 디버깅할 수 있습니다. VSCode에서 XState-VSCode 확장 프로그램을 설치합니다.확장 프로그램이 설치된 후 상태 기계 정의부를 보면 Open Visual Editor 표시를 확인할 수 있습니다.
      notion image
      notion image
      Visual Editor를 열어 상태 기계을 확인해 봅니다. 정의한 상태 기계이 차트로 표현되어 있고, 시뮬레이션을 해 볼 수도 있습니다. 정의한대로 잘 동작하는지 확인해 볼까요?
      XState VSCode simulator
      XState VSCode simulator
      Simulator를 통해 의도한대로 동작하는 모습을 볼 수 있습니다. 지금은 간단한 예시이지만, 복잡한 머신을 다룰 때에는 이 도구가 정말 유용하니 꼭 설치해 사용해 보시기를 바라요.
 
  1. Actor 만들기
    1. 이제 createMachien으로 정의한 기계를 사용할 수 있도록 Actor를 만듭니다. XState에서 Actor는 상태를 관리하는 객체로, 상태 기계이 실제로 동작하는 단위입니다. 우리는 Actor를 컴포넌트에서 사용할 것이므로, @xstate/react에서 제공하는 useActor hook을 감싸는 useDoor를 만들어 보겠습니다.
      // /hook/useDoor.ts import { useActor } from '@xstate/react'; import { doorMachine } from '@/machine/door-machine'; export function useDoor() { const [{ value: doorState }, trigger] = useActor(doorMachine); return { doorState, trigger }; }
      이제 doorState를 상태로 관리할 수 있게 되었습니다. triggerActor에 이벤트를 보낼 수도 있습니다.
       
  1. UI 만들기
    1. 상태 기계를 적용할 UI를 만들어 보겠습니다. 문 이미지와 OPEN-CLOSE 버튼, LOCK-UNLOCK 버튼으로 이루어진 간단한 디자인입니다. 그림은 door actor의 상태에 따라 변하며, DoorButton 컴포넌트의 onClickHandlerdoor actor의 이벤트를 유발합니다.
      // /components/doorButtons.tsx import { DoorEventType } from '@/machine/door-machine'; const style = { button: 'w-[200px] h-[100px] text-white font-bold text-[30px] uppercase rounded-[6px]', openButtons: 'bg-blue-400 hover:bg-blue-600', lockButtons: 'bg-red-400 hover:bg-red-600', }; interface ButtonProps { type: 'OPEN-CLOSE' | 'LOCK-UNLOCK'; name: DoorEventType; onClick: () => void; disabled: boolean; } export default function DoorButton({ onClick, disabled, type, name }: ButtonProps) { return ( <button className={`${style.button} ${type === 'OPEN-CLOSE' ? style.openButtons : style.lockButtons} disabled:bg-slate-100 disabled:text-slate-200`} disabled={disabled} onClick={onClick} > {name} </button> ); }
      // /components/doorImage.tsx import { DoorStateType } from '@/machine/door-machine'; interface DoorImageProps { doorState: DoorStateType; } export default function DoorImage({ doorState }: DoorImageProps) { switch (doorState) { case DoorStateType.OPEN: { return <img width={500} src="/door_opened.png" alt="open" className="ml-[70px]" />; } case DoorStateType.CLOSED: { return <img width={500} src="/door_closed.png" alt="close" className="ml-[70px]" />; } case DoorStateType.LOCKED: { return <img width={500} src="/door_locked.png" alt="locked" className="ml-[70px]" />; } } }
      // /components/door.tsx 'use client'; import { useDoor } from '@/hook/useDoor'; import DoorImage from '@/components/doorImage'; import DoorButton from '@/components/doorButton'; import { DoorEventType, DoorStateType } from '@/machine/door-machine'; export default function Door() { const { doorState, trigger } = useDoor(); const onOpen = () => { trigger({ type: DoorEventType.OPEN }); }; const onClose = () => { trigger({ type: DoorEventType.CLOSE }); }; const onLock = () => { trigger({ type: DoorEventType.LOCK }); }; const onUnlock = () => { trigger({ type: DoorEventType.UNLOCK }); }; return ( <div className="flex flex-col justify-center items-center gap-[30px]"> <DoorImage doorState={doorState} /> <div className="flex gap-[50px]"> <DoorButton type="OPEN-CLOSE" disabled={doorState === DoorStateType.LOCKED} onClick={doorState === DoorStateType.OPEN ? onClose : onOpen} name={doorState === DoorStateType.OPEN ? DoorEventType.CLOSE : DoorEventType.OPEN} /> <DoorButton type="LOCK-UNLOCK" disabled={doorState === DoorStateType.OPEN} onClick={doorState === DoorStateType.LOCKED ? onUnlock : onLock} name={doorState === DoorStateType.LOCKED ? DoorEventType.UNLOCK : DoorEventType.LOCK} /> </div> </div> ); }
      // /app/page.tsx import Door from '@/components/door'; export default function Home() { return ( <main className="w-screen h-screen flex justify-center items-center"> <Door /> </main> ); }
 

마무리하며

XState를 통한 상태 기계 구현 과정을 아주 간단한 예시를 통해 보여드렸습니다. 상태 기계는 각 단계를 명확하게 정의하고 있기 때문에 복잡한 로직 흐름을 관리해야 할 때 유용하게 사용할 수 있습니다. 팀엘리시움의 Bodydot Fitness 기기에도 FSM 설계가 적용되어 있기도 하고요! 회원가입이나 결제 등 여러 단계가 있고 UI를 함께 관리해야 하는 다중 단계 폼은 물론, 비동기 프로세스 흐름을 관리하는 데에도 유용하니 꼭 한번 사용해 보시기를 바랍니다. XState와 함께하면 보다 명확하고 빠르게 정의하고, 편하게 관리할 수 있을 거에요.
 

참고 자료
 

hyerexx

관련된 이야기