Using XState Actors to Model Async Workflows Safely - This Dot Labs
In my previous post I discussed the challenges of writing async workflows in React that correctly deal with all possible edge cases. Even for a simple case of a…
www.thisdot.co
XState와 비동기 1편: useEffect 안의 비동기 코드는 위험합니다!
원문 보기 Async Code in useEffect is Dangerous. How Do We Deal with It? - This Dot Labs The introduction of async/await to Javascript has made it easy to express complex workflows that string togeth..
itchallenger.tistory.com
이전 게시물에서 React에서 엣지 케이스를 올바르게 처리하는 비동기 워크플로를 작성하는 문제에 대해 논의했습니다.
오류 처리가 없는 두 개의 종속성이 있는 간단한 클라이언트 로직인 경우에도 다음과 같이 장황하게 끝납니다.
const useClient = (user) => {
const [client, setClient] = useState(null);
useEffect(() => {
let cancelled;
(async () => {
const clientAuthToken = await fetchClientToken(user);
if (cancelled) return;
const connection = await createWebsocketConnection();
if (cancelled) {
connection.close();
return;
}
const client = await createClient(connection, clientAuthToken);
if (cancelled) {
client.disconnect();
return;
}
setClient(client);
})();
return () => {
cancelled = true;
};
}, [user]);
useEffect(() => {
return () => {
client.disconnect();
};
}, [client]);
return client;
};
State machines? Actors? (상태 기계? 액터?)
상태 머신은 상태와 ,이전 상태와 수신하는 외부 이벤트의 조합에서 다음 상태를 결정하기 위해 따라야 하는 일련의 규칙으로 구성된 엔터티입니다. 상태 머신은 어디에나 있습니다. 예를 들어, Promise는 랩핑 중인 비동기 계산에서 값을 수신할 때 pending 상태에서 resolved 상태로 전환되는 상태 머신입니다.
액터 모델은 액터라고 하는 자체-포함된 단위의 상호 작용으로 비동기 워크플로를 모델링하는 컴퓨팅 아키텍처입니다. 이러한 단위는 이벤트를 보내고 수신하여 서로 통신하고 상태를 캡슐화하며 상위 액터가 하위 액터를 생성하여 수명 주기를 연결하는 계층적 관계에 존재합니다.
Setting the stage (단계를 나누기)
- 워크플로가 처음 생성되면 즉시 연결 생성 및 인증 토큰 가져오기를 시작할 수 있습니다. 클라이언트를 생성하기 전에 해당 작업들이 완료될 때까지 기다려야 합니다. 이 상태를 "preparing"이라고 합니다.
- 그런 다음 클라이언트 생성 프로세스를 시작했지만 클라이언트 생성이 클라이언트를 반환할 때까지 사용할 수 없습니다. 이 상태를 "creatingClient"라고 부를 것입니다.
- 마지막으로 모든 것이 준비되었으며, 클라이언트를 사용할 수 있습니다. 기계는 자원을 해제하고 스스로를 파괴할 수 있도록 종료 신호만 기다리고 있습니다. 이 상태를 "clientReady"라고 부를 것입니다.
![](https://blog.kakaocdn.net/dn/GFE32/btrARpE7pFt/grfnK0eXhmPbOobzipGyh0/img.png)
코드는 다음과 같습니다.
export const clientFactory = createMachine({
id: "clientFactory",
initial: "preparing",
states: {
preparing: {
on: {
"preparations.complete": {
target: "creatingClient",
},
},
},
creatingClient: {
on: {
"client.ready": {
target: "clientReady",
},
},
},
clientReady: {},
},
});
export const clientFactory = createMachine({
id: "clientFactory",
initial: "preparing",
states: {
preparing: {
// Parallel state nodes are a good way to model two independent
// workflows that should happen at the same time
type: "parallel",
states: {
// The token child node
token: {
initial: "fetching",
states: {
fetching: {
on: {
"token.ready": {
target: "done",
},
},
},
done: {
type: "final", // 종료
},
},
},
// The connection child node
connection: {
initial: "connecting",
states: {
connecting: {
on: {
"connection.ready": {
target: "done",
},
},
},
done: {
type: "final", // 종료
},
},
},
},
// The "onDone" transition on parallel state nodes gets called when all child
// nodes have entered their "final" state. It's a great way to wait until
// various workflows have completed before moving to the next step!
onDone: {
target: "creatingClient",
},
},
creatingClient: {
on: {
"client.ready": {
target: "clientReady",
},
},
},
clientReady: {},
},
});
Casting call (각 리소스를 Actor로, 즉 배우로 캐스팅 한다는 의미. 푸하하!)
// Demonstrated here is the simplest and most versatile form of actor: a function that
// takes a callback that sends events to the parent actor, and returns a function that
// will be called when it is stopped.
// 두번째 인자로 부모에게 메세지(이벤트)를 보내는 콜백을 전달받습니다.
const createConnectionActor = () => (send) => {
const connection = new WebSocket("wss://example.com");
// connection 연결 성공 시 부모 액터에게 이벤트를 전달합니다.
connection.onopen = () =>
// We send an event to the parent that contains the ready connection
send({ type: "connection.ready", data: connection });
// Actors are automatically stopped when their parent stops so simple actors are a great
// way to manage resources that need to be disposed of. The function returned by an
// actor will be called when it receives the stop signal.
// stop 신호를 받으면 호출할 함수를 전달합니다.
return () => {
connection.close();
};
};
// We can have the actor creation function take arguments, which we will populate
// when we spawn it
// 액터 생성 함수가 인수를 가져오도록 할 수 있습니다.
// 자식 프로세스를 스폰할 때
const createClientActor => (token, connection) => (send) => {
const clientPromise = createClient(token, connection);
clientPromise.then((client) =>
send({ type: "client.ready", data: client })
);
return () => {
// A good way to make sure the result of an async function is
// always cleaned up is by invoking cleanup through .then()
// If this executes before the promise is resolved, it will cleanup
// on resolution, whereas if it executes after it's resolved, it will
// clean up immediately
// 비동기 함수의 결과가 제대로 정리하는 좋은 방법은
// .then()을 통해 정리 함수를 호출하는 것입니다.
// Promise가 resolved 되기 전에 실행되면 resolved 된 후 정리됩니다.
// resolved 된 후면 즉시 정리됩니다.
clientPromise.then((client) => {
client.disconnect();
});
};
};
액터는 XState의 spawn action creator와 함께 스폰되지만,
실행 중인 액터에 대한 참조를 어딘가에 저장해야 하므로 일반적으로 spawn을 assign과 결합하여 액터를 만들고 부모 컨텍스트에 저장합니다.
// We put this as the machine options. Machine options can be customised when the
// machine is interpreted, which gives us a way to use values from e.g. React context
// to define our actions, although this is not demonstrated here
const clientFactoryOptions = {
spawnConnection: assign({
connectionRef: () => spawn(createConnectionActor()),
}),
spawnClient: assign({
// The assign action creator lets us use the machine context when defining the
// state to be assigned, this way actors can inherit parent state
clientRef: (context) =>
spawn(createClientActor(context.token, context.connection)),
}),
};
주 : assign은 context(상태의 부가 정보)에 데이터를 할당하는데 사용함.
Context | XState Docs
Context 🚀 Quick Reference While finite states are well-defined in finite state machines and statecharts, state that represents quantitative data (e.g., arbitrary strings, numbers, objects, etc.) that can be potentially infinite is represented as extende
xstate.js.org
주 : action은 해당 작업이 완료되었을 때 호출하고 싶은 다른 함수들을 의미함.
상세한 구현을 보고싶으면 맨 아래 예제 코드로 이동하기.
지금까지 한 작업은 client, token 액터들이 자신의 리소스를 관리하도록 한 것임!
export const clientFactory = createMachine({
id: "clientFactory",
initial: "preparing",
states: {
preparing: {
type: "parallel",
states: {
token: {
initial: "fetching",
states: {
fetching: {
// Because there's no resource to manage once it's done, we
// can simply invoke a promise here. Invoked services are like
// actors, but they're automatically spawned when the state node
// is entered, and destroyed when it is exited,
invoke: {
src: "fetchToken",
// Invoking a promise provides us with a handy "onDone" transition
// that triggers when the promise resolves. To handle rejections,
// we would similarly implement "onError"
onDone: {
// These "save" actions will save the result to the machine
// context. They're simple assigners, but you can see them in
// the full code example linked at the end.
actions: "saveToken",
target: "done",
},
},
},
done: {
type: "final",
},
},
},
// The connection child node
connection: {
initial: "connecting",
states: {
connecting: {
// We want our connection actor to stick around, because by design,
// the actor destroys the connection when it exits, so we store
// it in state by using a "spawn" action
entry: "spawnConnection",
on: {
// Since we're dealing with a persistent actor, we don't get an
// automatic "onDone" transition. Instead, we rely on the actor
// to send us an event.
"connection.ready": {
actions: "saveConnection",
target: "done",
},
},
},
done: {
type: "final",
},
},
},
},
onDone: {
target: "creatingClient",
},
},
creatingClient: {
// The same pattern as the connection actor. We spawn a persistent actor
// that takes care of creating and destroying the client.
// entry: "spawnClient",
on: {
"client.ready": {
actions: "saveClient",
target: "clientReady",
},
},
},
// Even though this node can't be exited, it is not "final". A final node would
// cause the machine to stop operating, which would stop the child actors!
clientReady: {},
},
});
성능과 관련하여
const useClient = (user) => {
const [state] = useMachine(clientFactory, clientFactoryOptions);
if (state.matches("clientReady")) return state.context.client;
return null;
};
물론, 상태 기계 정의와 결합하면, 액션 정의와 액터 코드는 거의 코드가 적거나 더 단순한 코드가 아닙니다.
다음과 같이 워크플로를 분해할 때의 이점은 다음과 같습니다.
XState Visualizer
Visualizer for XState state machines and statecharts
stately.ai
'FrontEnd' 카테고리의 다른 글
XState의 Actor 알아보기 (0) | 2022.05.04 |
---|---|
XState 공식 문서 번역 : 서비스 호출(Invoking Services) (0) | 2022.05.01 |
XState와 비동기 1편: useEffect 안의 비동기 코드는 위험합니다! (0) | 2022.04.30 |
XState : 상태 머신과 상태차트 소개 (0) | 2022.04.30 |
[TypeOrm]ORM을 프로젝트에 도입할 때 주의할점 (1) | 2022.04.30 |