[XState] state machines

이 글은 XState 공식문서를 읽고 정리한 글이다.

State machines - 상태 머신

state machine(이하 상태 머신)은 actor 같은 무언가의 행동을 묘사하는 모델이다. Finite state machines는 actor의 state가 어떤 event 발생한 때에 다른 state로 트랜지션되는지를 묘사한다. (finite states(유한 상태)란 주어진 시간에서 있을 수 있는 가능한 state 중 하나이다.)

상태 머신의 이점

상태 머신은 안정적이고 강력한 소프트웨어를 구축하는데 도움이 된다.

상태 머신 생성하기

XState에서는 상태 머신을 createMachine(config) 함수를 사용하여 생성한다:

import { createMachine } from "xstate";

const feedbackMachine = createMachine({
  id: "feedback",
  initial: "question",
  states: {
    question: {
      on: {
        "feedback.good": {
          target: "thanks",
        },
      },
    },
    thanks: {
      // ...
    },
    // ...
  },
});

이 예시에서는 machine은 questionthanks 두 개의 state를 가지고 있다: question state는 feedback.good 이벤트가 머신에 전송될 때 thanks state로 트랜지션한다.

const feedbackActor = createActor(feedbackMachine);

feedbackActor.subscribe((state) => {
  console.log(state.value);
});

feedbackActor.start(); // logs 'question'
feedbackActor.send({ type: "feedback.good" }); // logs 'thanks'

머신에서 actors 생성하기

머신은 actor의 로직을 포함한다. actor란 실행중인 머신의 인스턴스이다; 다시 말하자면, 머신에 의해 묘사된 로직을 가지고 있는 개체이다. 여러 actors가 같은 머신에서 생성될 수 있고, 각각의 actors는 같은 행동을 보여주지만(수신된 이벤트에 따른 반응), 그것들은 각기에 대해 독립적이고, 각자의 상태를 가지고 있다.

actor를 생성하기 위해서는 createActor(machine) 함수를 사용한다:

import { createActor } from "xstate";

const feedbackActor = createActor(feedbackMachine);

feedbackActor.subscribe((state) => {
  console.log(state.value);
});
feedbackActor.start(); // logs 'question'

다른 타입의 로직을 가지고 있는 actor도 생성할 수 있다. (예를 들면 functions, promises, 그리고 observables 같은 로직)

Providing implementations

이는 머신 생성시 실행되지마느 머신의 state, transition과 직접적으로 관련이 없는 language-specific 코드이다. 이 머신 구현체들은 아래를 포함한다:

  • Actions : 실행되고 side-effects가 잊혀진다.
  • Actors : 머신 actor과 상호작용할 수 있는 개체들
  • Guards : 트랜지션 여부를 결정하는 조건
  • Delays : 지연된 트랜지션이 수행되거나, 지연된 이벤트가 전송되기까지의 시간을 지정한다.

이 기본적인 구현체들은 머신을 생성할 때의 setup({...}) 함수에 제공된다. 그리고 JSON 직렬화 가능한 문자열 또는 객체를 사용하여 해당 구현을 참조할 수 있다.

import { setup } from "xstate";

const feedbackMachine = setup({
  // Default implementations
  actions: {
    doSomething: () => {
      console.log("Doing");
    },
  },
  actors: {},
  guards: {},
  delays: {},
}).createMachine({
  entry: { type: "doSomething" },
});

const feedbackActor = createActor(feedbackMachine);
feedbackActor.start(); // logs 'Doing';

기본 구현체들을 제공해주는 machine.provide(...) 기능들로 하여금 오버라이딩할 수 있다. 이 함수는 같은 config로 하지만 제공된 기능들로 새로운 머신을 생성할 것이다.

// 기존에 feedbackMachine이 있음 (위)
const customFeedbackMachine = feedbackMachine.provide({
  actions: {
    doSomething: () => {
      console.log("Doing something");
    },
  },
});

const feedbackActor = createActor(customFeedbackMachine);
feedbackActor.start(); // logs 'Doing something'

타입 지정하기

머신 config 안에 .types 프로퍼티를 사용하여 Typescript 타입을 지정할 수 있다.

import { createMachine } from 'xstate';

const feedbackMachine = createMachine({
  types: {} as {
    context: { feedback: string };
    events: { type: 'feedback.good' } | { type: 'feedback.bad' };
    actions: { type: 'logTelemetry' };
  },
});

이 타입들은 머신 config 전체와 생성된 머신, actor에서 추론되므로 machine.transition(...), actor.send(...) 등의 메서드가 타입안전하다.

setup(...) 함수를 사용하는 경우, setup 함수 내부의 .types 프로퍼티에 타입을 제공해주어야한다:

import { setup } from 'xstate';

const feedbackMachine = setup({
  types: {} as {
    context: { feedback: string };
    events: { type: 'feedback.good' } | { type: 'feedback.bad' };
  },
  actions: {
    logTelemetry: () => {
      // TODO: implement
    }
  }
}).createMachine({
  // ...
})

Typegen

Typegen은 XState v5에서 아직 제공하지 않는다. setup(...) 함수에서 .types 프로퍼티를 기재해놓는다면, 머신에 강한 타입을 제공해줄 수 있다.

Typescript

머신에 강한 타입을 제공하는 방법은 setup(...) 함수를 사용하여 .types 프로퍼티를 넣는 것이다.

import { setup, fromPromise } from 'xstate';

const someAction = () => {};
const someGuard = ({ context }) => context.count <= 10;
const someActor = fromPromise(async () => {
  return 42;
});

const feedbackMachine = setup({
  types: {
    context: {} as { count: number };
    events: {} as { type: 'increment' } | { type: 'decrement' };
  },
  actions: {
    someAction,
  },
  guards: {
    someGuard,
  },
  actors: {
    someActor
  }
}).createMachine({
  initial: 'counting',
  states: {
    counting: {
      entry: { type: 'someAction' }, // strongly-typed
      invoke: {
        src: 'someActor', // strongly-typed
        onDone: {
          actions: ({event}) => {
            event.output;
          }
        }
      },
      on: {
        increment: {
          guard: { type: 'someGuard' }, // strongly-typed
          actions: assign({
            count: ({ context }) => context.count + 1
          })
        }
      }
    }
  }
})