XState (1)
Tags: javascript, xstate, react, typescript
Published: 2021-05-12 14:42:09

최근 상태 관리 라이브러리에 대한 리서치를 하다가 관심있게 보고있던 XState를 공부해보고 정리할 겸 작성. 내용이 너무 많을 것 같아서 틈날때마다 업데이트 해나아갈 예정이다.

## Xstate

XState는 FSM(Finite State Machine)과 Statecharts를 생성, 해석, 실행하기 위한 라이브러리이며, 해당 machine의 호출을 actor로 관리하기 위한 라이브러리이다.

알아야할 개념은 FSM, Statecharts, Actor model들에 대한 내용인데 [이 곳](https://xstate.js.org/docs/about/concepts.html#finite-state-machines)에 읽어볼만한 레퍼런스들이 있다. 과거 대학교에서 이산 수학 강의를 들었을 때 마지막 챕터였던 FSM에 대해서 수학의 정수라고 말씀주셨던 기억이 난다.

[위키](https://ko.wikipedia.org/wiki/%EC%9C%A0%ED%95%9C_%EC%83%81%ED%83%9C_%EA%B8%B0%EA%B3%84)의 내용을 보자면 유한한 개수의 상태를 가질 수 있는 오토마타로서, 오직 하나의 상태(State)만을 가지게 되고 어떠한 사건(Event)에 의해 한 상태에서 다른 상태로 변환 할 수 있는 전이(Transition)라고 하는데, 상태와 전이의 집합을 FSM으로 정의하는 것 같다.

많이 알려진 활용 분야중에 게임에서 몬스터의 행동 패턴, 공학에서의 논리 회로등 다양한 분야에서 쓰이고 있고, 요약하자면 아래와 같은 조건들로 정의가 된다.
* 유한개의 상태 (states)
* 유한개의 사건 (events)
* 초기 상태
* 현재 상태와 사건을 다음 상태로 결정하는 전이 함수 (transition function)
* (비어있을 수 있는) 최종 상태 집합 (final states)

주요 내용은 [공식 문서](https://xstate.js.org)를 바탕으로 진행하며 설치는 아래와 같이 한다.
```bash
npm install xstate --save
yarn add xstate
```

## 개요
factory function인 `createMachine`을 이용하여 state machine 혹은 statecharts 을 생성할 수 있다.

```ts
import { createMachine } from 'xstate';

const promiseMachine = createMachine({
  id: 'promise',
  initial: 'pending',
  states: {
    pending: {
      on: {
        RESOLVE: { target: 'resolved' },
        REJECT: { target: 'rejected' }
      }
    },
    resolved: {
      type: 'final'
    },
    rejected: {
      type: 'final'
    },
  },
});
```

`id`는 machine의 고유값이면서 자식 상태 노드 id의 base string이 된다.
`initial`은 초기 상태 노드를 지정하며, `states`는 각각의 자식 상태를 정의한다.
위 FSM의 개념에서의 전이(transition)은 `resolved`와 `rejected`라는 상태로 지정하며, 각 각의 상태는 `final` 이라는 machine이 종료에 도달하는 상태임을 지정한다.

생성한 machine을 해석하고 실행하려면 `interpret`를 이용하여 interpreter를 추가해야 한다. 아래와 같이 사용 가능하다.

```ts
import { createMachine, interpret } from 'xstate';

const promiseMachine = createMachine({
  /* ... */
});

const promiseService = interpret(promiseMachine).onTransition((state) =>
  console.log(state.value)
);

// Start the service
promiseService.start();
// => 'pending'

promiseService.send({ type: 'RESOLVE' });
// => 'resolved'
```

그리고 `createMachine`으로 생성한 코드는 [이 곳](https://xstate.js.org/viz/)에서 시각화 하고 실행해 볼 수 있다.

## States
상태는 특정 시점의 추상적 표현이다. FSM은 주어진 시간에 유한한 수의 상태중 하나만 가능하다. 상태 정의는 아래와 같이 정의 하고 확인할 수 있다.

```ts
const lightMachine = createMachine({
  id: 'light',
  initial: 'green',
  states: {
    green: {
      /* ... */
    }
    // ...
  }
});

console.log(lightMachine.initialState);
// State {
//   value: 'green',
//   actions: [],
//   context: undefined,
//   // ...
// }
```

State 객체는 JSON 직렬화가 가능하며 아래와 같은 속성이 있다.

* value: 현재 상태 값 (예를 들어 { red: 'walk' } )
* context: 상태의 현재 context
* event: 해당 상태로 transition을 트리거한 event 오브젝트
* actions: 실행할 액션의 배열
* activities: 액티비티가 실행되면 true, 중지되면 false
* history: 이전 상태 인스턴스
* meta: 상태 노드의 meta 프로퍼티로 정의된 메타 데이터
* done: 상태가 최종 상태를 나타내는지에 대한 여부

### State의 메소드와 속성들
#### state.matches(parentStateValue)
state.value가 주어진 `parentStateValue`의 subset인지 확인한다. 예를들어 `state.value`가 `{ red: ‘stop’ }` 이라면?

```ts
console.log(state.value);
// => { red: 'stop' }

console.log(state.matches('red'));
// => true

console.log(state.matches('red.stop'));
// => true

console.log(state.matches({ red: 'stop' }));
// => true

console.log(state.matches('green'));
// => false
```

만약 여러 상태에 대한 match를 원한다면?

```ts
const isMatch = [{ customer: 'deposit' }, { customer: 'withdrawal' }].some(
  state.matches
);

```

#### state.nextEvents
현재 상태에서 가능한 다음 transition을 확인해볼 수 있다.
```ts
const { initialState } = lightMachine;
console.log(initialState.nextEvents);
// => ['TIMER', 'EMERGENCY']
```

#### state.changed
이전 상태로부터 변화되었는지에 대한 여부를 알수 있다. 상태가 아래와 같다면 `changed`로 간주한다.

* 이전 값과 값이 같지 않다
* 어떠한 새로운 액션 (side-effects)가 실행되었다.

```ts
const { initialState } = lightMachine;

console.log(initialState.changed);
// => undefined

const nextState = lightMachine.transition(initialState, { type: 'TIMER' });

console.log(nextState.changed);
// => true

const unchangedState = lightMachine.transition(nextState, {
  type: 'UNKNOWN_EVENT'
});

console.log(unchangedState.changed);
// => false
```	

최초 상태(히스토리가 없는 것)는 `undefined`이다.

#### state.done
이 상태가 최종 상태인지에 대한 여부를 알 수 있다.
```ts
const answeringMachine = createMachine({
  initial: 'unanswered',
  states: {
    unanswered: {
      on: {
        ANSWER: { target: 'answered' }
      }
    },
    answered: {
      type: 'final'
    }
  }
});

const { initialState } = answeringMachine;
initialState.done; // false

const answeredState = answeringMachine.transition(initialState, {
  type: 'ANSWER'
});
answeredState.done; // true
```

#### state.toStrings()
상태값의 패스에 대한 모든 표현의 문자열 배열을 리턴한다.
```ts
console.log(state.value);
// => { red: 'stop' }

console.log(state.toStrings());
// => ['red', 'red.stop']

```

문서에서는 CSS class나 data-attributes 같은 string-based 환경에서 유용하게 사용될 수 있다고 하는데 아마 해당 요소에 state의 value값을 바로 사용하면 되겠지 라는 생각이 들었다.

이후 내용은 조금 더 내용을 읽어보고 작성해야할 것 같다.

to be continue..