안녕하세요. 이번 글에서는 XState를 이용해 유한 상태 기계을 구현 방법하는 방법을 소개하고자 합니다.
유한 상태 기계이란 무엇이고, 왜 사용할까?
FSM(Finite State Machine, 유한 상태 기계)은 시스템이 가질 수 있는 상태들과 그 상태들 간의 전이를 명확하게 정의하는 모델입니다. 이를 통해 시스템의 동작을 체계적으로 관리하고 예측할 수 있습니다. 복잡한 시스템에서 FSM을 도입하면 다양한 서비스에 걸쳐 상태를 일관되게 관리할 수 있기 때문에 예기치 않은 동작이나 충돌을 방지할 수 있습니다. 또한 개별 상태에서 처리해야 할 작업이 명확하게 정의되어 있기 때문에 시스템을 유지보수하고 기능을 확장하기도 용이합니다.
왜 XState로 FSM을 구현할까?
XState는 JS로 작성된 상태 관리 라이브러리로 FSM과 상위 상태 기계(Hierarchical State Machine)을 지원하여 복잡한 상태 관리를 체계적으로 수행할 수 있게 해 주는 도구입니다. XState에는 아래와 같은 장점이 있습니다.
- 명확하고 표준화된 상태 정의
상태와 전이를 JSON 또는 TypeScript 형식으로 명시하여 코드의 가독성과 유지보수성을 높입니다.
- 강력한 시뮬레이션 도구
상태 전이를 시각화하고, 실시간 디버깅을 지원하여 설계된 상태 모델의 동작을 쉽게 테스트할 수 있습니다.
- 확장 가능성
복잡한 계층 구조나 병렬 상태를 지원하여 다양한 환경에서도 유연하게 활용할 수 있습니다.
XState 사용해 보기
그럼 간단한 예시를 하나 만들어 보겠습니다. 최신 버전의 Next.js를 기반으로 작업하며, 이렇게 생긴 문을 하나 만들어 볼 거에요. 예시 코드를 담은 레포지토리가 있으니, 글 끝에서 링크를 확인해 보세요!

- 환경 설정하기
먼저, Next.js 프로젝트를 생성합니다. (이 예시에서는 TypeScript와 tailwind를 사용합니다.)
npx create-next-app@latest xstate-example
이제 라이브러리를 의존성으로 추가합니다. @xstate/react 패키지는 React 컴포넌트와 XState 상태 기계을 쉽게 연동할 수 있도록 도와줍니다.
yarn add xstate @xstate/react
- 상태 기계 정의하기
이제 XState로 상태 기계를 정의해 보겠습니다. XState에서 상태 기계는
createMachine
함수로 정의합니다. 가장 간단한 machineConfig
는 아래와 같은 모습입니다. 최초 상태와 개별 상태에서의 이벤트와 전이가 명시되어 있습니다.
fooMacine
이 실행되면 INITIAL
상태(State)로 진입하고, INITIAL
상태의 fooMachine
에 EVENT_A
라는 이벤트(Event)가 발생하면 NEXT
상태로 전이(Transition)됩니다. 그럼 이제 위에서 봤던 문을 만들어 볼까요?const fooMachine = createMachine({ initial: FooStateType.INITIAL, sates: { FooStateType.INITIAL: { on: { FooEventType.EVENT_A: { target: FooStateType.NEXT } } } // ... other states ... } })
우리가 만들어 볼 문에는 열림, 닫힘, 잠김의 세 가지 상태가 있습니다. 이 상태 기계에는
closed
, open
, locked
라는 세 가지 상태가 있고, 각 상태는 open
, close
, lock
, unlock
이벤트를 통해 전이됩니다.
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 }, }, }, }, });
타입 관리를 위해
setup({ … })
을 추가해 주었습니다. setup
함수는 상태 기계에서 사용되는 타입을 안전하게 관리할 수 있도록 도와줍니다. - 상태 기계 작동 확인하기
VSCode에는 XState 상태 기계을 시각적으로 확인하고 시뮬레이션할 수 있는 확장 기능이 있습니다. 이 확장 기능을 사용하면 상태 기계의 동작을 쉽게 확인하고 디버깅할 수 있습니다. VSCode에서 XState-VSCode 확장 프로그램을 설치합니다.확장 프로그램이 설치된 후 상태 기계 정의부를 보면 Open Visual Editor 표시를 확인할 수 있습니다.


Visual Editor를 열어 상태 기계을 확인해 봅니다. 정의한 상태 기계이 차트로 표현되어 있고, 시뮬레이션을 해 볼 수도 있습니다. 정의한대로 잘 동작하는지 확인해 볼까요?

Simulator를 통해 의도한대로 동작하는 모습을 볼 수 있습니다. 지금은 간단한 예시이지만, 복잡한 머신을 다룰 때에는 이 도구가 정말 유용하니 꼭 설치해 사용해 보시기를 바라요.
- Actor 만들기
이제
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
를 상태로 관리할 수 있게 되었습니다. trigger
로 Actor
에 이벤트를 보낼 수도 있습니다.- UI 만들기
상태 기계를 적용할 UI를 만들어 보겠습니다. 문 이미지와 OPEN-CLOSE 버튼, LOCK-UNLOCK 버튼으로 이루어진 간단한 디자인입니다. 그림은
door actor
의 상태에 따라 변하며, DoorButton
컴포넌트의 onClickHandler
로 door 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와 함께하면 보다 명확하고 빠르게 정의하고, 편하게 관리할 수 있을 거에요.
참고 자료