본문 바로가기

FrontEnd

XState : 타입스크립트와 같이 활용하기

반응형
XState는 TypeScript로 작성되었으므로, 상태 차트를 강력하게 타이핑 하는 것이 유용하고 권장됩니다.
import { createMachine } from 'xstate';

const lightMachine = createMachine({
  schema: {
    // The context (extended state) of the machine
    context: {} as { elapsed: number },
    // The events this machine handles
    events: {} as
      | { type: 'TIMER' }
      | { type: 'POWER_OUTAGE' }
      | { type: 'PED_COUNTDOWN'; duration: number }
  }
  /* Other config... */
});
schema 속성에 컨텍스트와 이벤트를 제공하면 다음과 같은 많은 이점이 있습니다.
  • 컨텍스트 타입 / 인터페이스 타입(TContext)은 액션, 가드, 서비스 등에 전달됩니다. 또한 깊게 중첩된 상태로 전달됩니다.
  • 이벤트 타입(TEvent)은 지정된 이벤트(및 기본 제공 XState 관련 이벤트)에 대해서만 전환 구성에 사용될 수 있도록 합니다.
  • 제공된 이벤트 객체 모양은 작업, 가드 및 서비스에도 전달됩니다.
  • 기계로 보내는 이벤트는 강력한 형식으로 지정되어 수신할 페이로드 모양에 대해 훨씬 더 확신을 갖게 됩니다.

Typegen 4.29


실험적 기능

이 기능은 베타 버전입니다!

개선을 위해 적극적으로 노력하고 있는 사항을 확인하려면 아래의 알려진 제한 사항 섹션을 참조하세요.

 

 VS Code extension (opens new window)or our CLI 를 이용하여
XState에 대한 지능적인 타이핑을 자동으로 생성할 수 있습니다.
시작하는 방법은 다음과 같습니다.
  1. VS Code 확장을 다운로드 및 설치하거나 CLI를 설치하고 --watch 플래그와 함께 xstate typegen 명령을 실행합니다.
  2. 새 파일을 열고 스키마 속성을 전달하여 새 시스템을 만듭니다.
import { createMachine } from 'xstate';

const machine = createMachine({
  schema: {
    context: {} as { value: string },
    events: {} as { type: 'FOO'; value: string } | { type: 'BAR' }
  },
  initial: 'a',
  states: {
    a: {
      on: {
        FOO: {
          actions: 'consoleLogValue',
          target: 'b'
        }
      }
    },
    b: {
      entry: 'consoleLogValueAgain'
    }
  }
});
3. tsTypes: {}를 머신에 추가하고 파일을 저장합니다.
const machine = createMachine({
  tsTypes: {},
  schema: {
    context: {} as { value: string },
    events: {} as { type: 'FOO'; value: string } | { type: 'BAR' }
  },
  initial: 'a',
  states: {
    /* ... */
  }
});
4. 확장 프로그램은 자동으로 제네릭을 머신에 추가합니다.
const machine = createMachine({
  tsTypes: {} as import('./filename.typegen').Typegen0
  /* ... */
});
5. createMachine 호출에 두 번째 매개변수를 추가합니다. 여기에서 머신에 대한 액션, 서비스, 가드 및 딜레이를 구현합니다.
const machine = createMachine(
  {
    /* ... */
  },
  {
    actions: {
      consoleLogValue: (context, event) => {
        // Wow! event is typed to { type: 'FOO' }
        console.log(event.value);
      },
      consoleLogValueAgain: (context, event) => {
        // Wow! event is typed to { type: 'FOO' }
        console.log(event.value);
      }
    }
  }
);
옵션의 이벤트는 액션을 트리거하는 이벤트에 대해 강력하게 타입이 지정되어 있음을 알 수 있습니다.
이것은 액션, 서비스, 가드 및 딜레이에 해당됩니다.
또한 state.matches, 태그 및 기타 시스템 부분이 이제 타입 안전하다는 것을 알 수 있습니다.

Typing promise services (프라미스 반환 서비스 타이핑)


서비스 스키마 속성을 사용하여 생성된 타입을 사용하여 프라미스 기반 서비스의 반환 타입을 지정할 수 있습니다.
import { createMachine } from 'xstate';

createMachine(
  {
    schema: {
      services: {} as {
        myService: {
          // The data that gets returned from the service
          data: { id: string };
        };
      }
    },
    invoke: {
      src: 'myService',
      onDone: {
        actions: 'consoleLogId'
      }
    }
  },
  {
    services: {
      myService: async () => {
        // This return type is now type-safe
        return {
          id: '1'
        };
      }
    },
    actions: {
      consoleLogId: (context, event) => {
        // This event type is now type-safe
        console.log(event.data.id);
      }
    }
  }
);

VS Code 확장을 최대한 활용하는 방법

명명된 작업/가드/서비스 사용

권장 사항은 대부분 인라인 액션이 아닌 명명된 액션/가드/서비스를 사용하는 것입니다.

이것은 최적입니다:

createMachine(
  {
    entry: ['sayHello']
  },
  {
    actions: {
      sayHello: () => {
        console.log('Hello!');
      }
    }
  }
);
이것은 유용하지만 덜 최적입니다.
createMachine({
  entry: [
    () => {
      console.log('Hello!');
    }
  ]
});
명명된 작업/서비스/가드는 다음을 허용합니다.
  • 이름이 상태 차트에 표시되기 때문에 더 나은 시각화
  • 이해하기 쉬운 코드
  • useMachine 또는 machine.withConfig에서 재정의

The generated files

저장소에서 생성된 파일(*filename*.typegen.ts)을 gitignore하는 것이 좋습니다.
CLI를 사용하여 예를 들어 사후 설치 스크립트를 통해 CI에서 다시 생성할 수 있습니다.
{
  "scripts": {
    "postinstall": "xstate typegen \"./src/**/*.ts?(x)\""
  }
}

Don't use enums

이넘은 XState TypeScript와 함께 사용되는 일반적인 패턴입니다.

그들은 종종 주 이름을 선언하는 데 사용되었습니다. 이와 같이:

enum States {
  A,
  B
}

createMachine({
  initial: States.A,
  states: {
    [States.A]: {},
    [States.B]: {}
  }
});
그런 다음 결과 시스템에서 state.matches(States.A)를 확인할 수 있습니다. 이렇게 하면 상태 이름에 대한 타입 안전 검사가 가능합니다.
 
typegen을 사용하면 더 이상 이넘을 사용할 필요가 없습니다.
 
모든 state.matches 타입은 타입 안전합니다. 이넘은 현재 정적 분석 도구에서 지원되지 않습니다.
 
또한 상대적으로 적은 이득을 위해 추가하는 복잡성으로 인해 typegen으로 지원하지 않을 것입니다.
이넘 대신 typegen을 사용하고 그것이 제공하는 타입 안전성의 강도에 의존하십시오. 
 
 

알려진 제한 사항 

Always transitions/raised events

 
액션/서비스/가드/딜레이는 항상 전환 또는 발생 이벤트에 대해 "응답"으로 호출되는 경우 현재 잘못 주석 처리될 수 있습니다.
우리는 XState와 typegen 모두에서 이 문제를 해결하기 위해 노력하고 있습니다. 

 

Config Objects


MachineConfig<TContext, any, TEvent>의 일반 타입은 createMachine<TContext, TEvent>의 일반 타입과 동일합니다.
이것은 createMachine(...) 함수 외부에서 머신 구성 객체를 정의할 때 유용하며 추론 오류를 방지하는 데 도움이 됩니다.


import { MachineConfig } from 'xstate';

const myMachineConfig: MachineConfig<TContext, any, TEvent> = {
  id: 'controller',
  initial: 'stopped',
  states: {
    stopped: {
      /* ... */
    },
    started: {
      /* ... */
    }
  }
  // ...
};



Typestates 4.7+


Typestates는 상태 값을 기반으로 전체 상태 컨텍스트의 모양을 좁히는 개념입니다.

이것은 과도한 타입 단언을 작성하지 않고도 불가능한 상태를 방지하고 주어진 상태에서 컨텍스트가 어떻게 되어야 하는지를 좁히는 데 도움이 될 수 있습니다.

 

Typestate는 두 가지 속성으로 구성된 인터페이스입니다.
  • value - typestate의 상태 값(복합 상태는 개체 구문을 사용하여 참조해야 합니다. 예: 'idle.error' 대신 { idle: 'error' })
  • context - 상태가 주어진 값과 일치할 때 typestate의 축소된 컨텍스트
머신의 타입 상태는 createMachine<TContext, TEvent, TTypestate>에서 세 번째 제네릭 타입으로 지정됩니다.

Example:

import { createMachine, interpret } from 'xstate';

interface User {
  name: string;
}

interface UserContext {
  user?: User;
  error?: string;
}

type UserEvent =
  | { type: 'FETCH'; id: string }
  | { type: 'RESOLVE'; user: User }
  | { type: 'REJECT'; error: string };

type UserTypestate =
  | {
      value: 'idle';
      context: UserContext & {
        user: undefined;
        error: undefined;
      };
    }
  | {
      value: 'loading';
      context: UserContext;
    }
  | {
      value: 'success';
      context: UserContext & { user: User; error: undefined };
    }
  | {
      value: 'failure';
      context: UserContext & { user: undefined; error: string };
    };

const userMachine = createMachine<UserContext, UserEvent, UserTypestate>({
  id: 'user',
  initial: 'idle',
  states: {
    idle: {
      /* ... */
    },
    loading: {
      /* ... */
    },
    success: {
      /* ... */
    },
    failure: {
      /* ... */
    }
  }
});

const userService = interpret(userMachine);

userService.subscribe((state) => {
  if (state.matches('success')) {
    // from the UserState typestate, `user` will be defined
    state.context.user.name;
  }
});

 

경고
복합 상태에는 자식 상태를 테스트할 때 타입 오류를 방지하기 위해 명시적으로 모델링된 모든 부모 상태 값이 있어야 합니다.
type State =
  /* ... */
  | {
      value: 'parent';
      context: Context;
    }
  | {
      value: { parent: 'child' };
      context: Context;
    };
/* ... */
두 상태의 컨텍스트 타입이 동일한 경우 해당 값에 대한 유니언 타입을 사용하여 해당 선언을 병합할 수 있습니다.
type State =
  /* ... */
  {
    value: 'parent' | { parent: 'child' };
    context: Context;
  };
/* ... */

Troubleshooting


XState 및 TypeScript에는 몇 가지 알려진 제한 사항이 있습니다.
우리는 TypeScript를 사랑하며 XState에서 더 나은 경험을 제공하기 위해 끊임없이 노력하고 있습니다. 다음은 몇 가지 알려진 문제이며 모두 해결할 수 있습니다. 

 

Events in machine options

createMachine을 사용할 때 구성에서 명명(스트링으로)된 actions/services/guards에 구현을 전달할 수 있습니다. 예를 들어:
import { createMachine } from 'xstate';

interface Context {}

type Event =
  | { type: 'EVENT_WITH_FLAG'; flag: boolean }
  | {
      type: 'EVENT_WITHOUT_FLAG';
    };

createMachine(
  {
    schema: {
      context: {} as Context,
      events: {} as Event
    },
    on: {
      EVENT_WITH_FLAG: {
        actions: 'consoleLogData'
      }
    }
  },
  {
    actions: {
      consoleLogData: (context, event) => {
        // This will error at .flag
        console.log(event.flag);
      }
    }
  }
);

 

이 오류가 발생하는 이유는 consoleLogData 함수 내부에서 어떤 이벤트가 발생했는지 알 수 없기 때문입니다.

이를 관리하는 가장 깔끔한 방법은 이벤트 타입을 직접 단언하는 것입니다. (타입 가드)

createMachine(config, {
  actions: {
    consoleLogData: (context, event) => {
      if (event.type !== 'EVENT_WITH_FLAG') return
      // No more error at .flag!
      console.log(event.flag);
    };
  }
})
구현을 인라인으로 이동하는 것도 가능합니다. (bad)
import { createMachine } from 'xstate';

createMachine({
  schema: {
    context: {} as Context,
    events: {} as Event
  },
  on: {
    EVENT_WITH_FLAG: {
      actions: (context, event) => {
        // No more error, because we know which event
        // is responsible for calling this action
        console.log(event.flag);
      }
    }
  }
});

 

 

이 접근 방식은 모든 경우에 작동하지 않습니다. 액션은 이름을 잃어버리기 때문에 비주얼라이저에서 보기 좋지 않습니다.

또한 작업이 여러 위치에 복제된 경우 필요한 모든 위치에 복사하여 붙여넣어야 함을 의미합니다. 



Event types in entry actions

인라인 entry 액션의 이벤트 타입은 현재 해당 이벤트로 이어진 이벤트에 입력되지 않습니다. 다음 예를 고려하십시오.
import { createMachine } from 'xstate';

interface Context {}

type Event =
  | { type: 'EVENT_WITH_FLAG'; flag: boolean }
  | {
      type: 'EVENT_WITHOUT_FLAG';
    };

createMachine({
  schema: {
    context: {} as Context,
    events: {} as Event
  },
  initial: 'state1',
  states: {
    state1: {
      on: {
        EVENT_WITH_FLAG: {
          target: 'state2'
        }
      }
    },
    state2: {
      entry: [
        (context, event) => {
          // This will error at .flag
          console.log(event.flag);
        }
      ]
    }
  }
});

여기서 state2에 대한 entry action로 이어진 event가 무엇인지 알 수 없습니다.

이 문제를 해결하는 유일한 방법은 위와 유사한 트릭을 수행하는 것입니다.

// 타입 가드
entry: [
  (context, event) => {
    if (event.type !== 'EVENT_WITH_FLAG') return;
    // No more error at .flag!
    console.log(event.flag);
  }
];

Assign action behaving strangely

strict: true 모드에서 실행하면 assign 액션이 때때로 매우 이상하게 작동할 수 있습니다.
interface Context {
  something: boolean;
}

createMachine({
  schema: {
    context: {} as Context
  },
  context: {
    something: true
  },
  entry: [
    // Type 'AssignAction<{ something: false; }, AnyEventObject>' is not assignable to type 'string'.
    assign(() => {
      return {
        something: false
      };
    }),
    // Type 'AssignAction<{ something: false; }, AnyEventObject>' is not assignable to type 'string'.
    assign({
      something: false
    }),
    // Type 'AssignAction<{ something: false; }, AnyEventObject>' is not assignable to type 'string'.
    assign({
      something: () => false
    })
  ]
});

 

시도한 모든 것이 작동하지 않는 것처럼 보일 수 있습니다. 모든 구문은 버그가 있습니다.

수정 사항은 매우 이상하지만 일관되게 작동합니다. 할당자 함수의 첫 번째 인수에 사용되지 않은 컨텍스트 인수를 추가합니다.

entry: [
  // No more error!
  assign((context) => {
    return {
      something: false,
    };
  }),
  // No more error!
  assign({
    something: (context) => false,
  }),
  // Unfortunately this technique doesn't work for this syntax
  // assign({
  //   something: false
  // }),
],
이것은 수정해야 할 불쾌한 버그이며 코드베이스를 엄격 모드로 이동하는 것과 관련이 있지만 V5에서 이를 수행할 계획입니다.

keyofStringsOnly

이 오류가 표시되는 경우:
Type error: Type 'string | number' does not satisfy the constraint 'string'.
Type 'number' is not assignable to type 'string'. TS2344
tsconfig 파일에 "keyofStringsOnly": true가 포함되어 있지 않은지 확인하십시오.











반응형