๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

FrontEnd

XState์˜ Actor ์•Œ์•„๋ณด๊ธฐ

๋ฐ˜์‘ํ˜•

https://xstate.js.org/docs/guides/actors.html#actor-api

 

Actors | XState Docs

Actors 4.6+ ๐Ÿš€ Quick Reference The Actor model (opens new window) is a mathematical model of message-based computation that simplifies how multiple "entities" (or "actors") communicate with each other. Actors communicate by sending messages (events) to e

xstate.js.org

XState
 

 

์•กํ„ฐ(ํ–‰์œ„์ž) ๋ชจ๋ธ์€ ์—ฌ๋Ÿฌ "์—”ํ‹ฐํ‹ฐ"(๋˜๋Š” "ํ–‰์œ„์ž")๊ฐ€ ์„œ๋กœ ํ†ต์‹ ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹จ์ˆœํ™”ํ•˜๋Š” ๋ฉ”์‹œ์ง€ ๊ธฐ๋ฐ˜ ๊ณ„์‚ฐ์˜ ์ˆ˜ํ•™์  ๋ชจ๋ธ์ž…๋‹ˆ๋‹ค.
์•กํ„ฐ๋Š” ๋ฉ”์‹œ์ง€(์ด๋ฒคํŠธ)๋ฅผ ์„œ๋กœ ์ „์†กํ•˜์—ฌ ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค.
์•กํ„ฐ์˜ ๋กœ์ปฌ ์ƒํƒœ๋Š” ์ด๋ฒคํŠธ๋กœ ์ „์†กํ•˜์—ฌ ๋‹ค๋ฅธ ์•กํ„ฐ์™€ ๊ณต์œ ํ•˜๋ ค๋Š” ๊ฒฝ์šฐ๊ฐ€ ์•„๋‹ˆ๋ฉด private ํ•ฉ๋‹ˆ๋‹ค.
 
์•กํ„ฐ๊ฐ€ ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜๋ฉด ์„ธ ๊ฐ€์ง€ ์ผ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์œ ํ•œ ์ˆ˜์˜ ๋ฉ”์‹œ์ง€๋ฅผ ๋‹ค๋ฅธ ์•กํ„ฐ์—๊ฒŒ send ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์œ ํ•œ ์ˆ˜์˜ ์ƒˆ๋กœ์šด ์•กํ„ฐ๋ฅผ spawnํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์•กํ„ฐ์˜ ๋กœ์ปฌ ์ƒํƒœ๋Š” ๋ณ€๊ฒฝ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค(behavior์— ๋”ฐ๋ผ ๊ฒฐ์ •๋จ).

์ƒํƒœ ๋จธ์‹  ๋ฐ ์ƒํƒœ ์ฐจํŠธ๋Š” ํ–‰์œ„ ๋ฐ ๋…ผ๋ฆฌ์˜ ์ด๋ฒคํŠธ ๊ธฐ๋ฐ˜ ๋ชจ๋ธ์ด๊ธฐ ๋•Œ๋ฌธ์— ์•กํ„ฐ ๋ชจ๋ธ๊ณผ ๋งค์šฐ ์ž˜ ์ž‘๋™ํ•ฉ๋‹ˆ๋‹ค.

์ƒํƒœ ๋จธ์‹ ์ด ์ด๋ฒคํŠธ๋กœ ์ธํ•ด ์ „ํ™˜๋  ๋•Œ ๋‹ค์Œ ์ƒํƒœ์—๋Š” ๋‹ค์Œ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

  • ๋‹ค์Œ ๊ฐ’ ๋ฐ ์ปจํ…์ŠคํŠธ(์•กํ„ฐ์˜ ๋กœ์ปฌ ์ƒํƒœ)
  • ์‹คํ–‰ํ•  ๋‹ค์Œ ์ž‘์—…(์•„๋งˆ๋„ ์ƒˆ๋กœ ์ƒ์„ฑ๋˜์—ˆ์„ ์•กํ„ฐ ๋˜๋Š” ๋‹ค๋ฅธ ์•กํ„ฐ์—๊ฒŒ ๋ณด๋‚ด๋Š” ๋ฉ”์‹œ์ง€)

์•กํ„ฐ๋ฅผ spawnํ•˜๊ฑฐ๋‚˜ invokeํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์Šคํฐ๋œ ์•กํ„ฐ๋Š” ํ˜ธ์ถœ๋œ ์•กํ„ฐ์™€ ๋‘ ๊ฐ€์ง€ ์ฃผ์š” ์ฐจ์ด์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  • ๊ทธ๊ฒƒ๋“ค์€ ์–ธ์ œ๋“ ์ง€ ์Šคํฐ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (assign(...)์•ก์…˜ ๋‚ด๋ถ€์˜ spawn(...)์„ ํ†ตํ•ด)
  •  ์–ธ์ œ๋“ ์ง€ ์ค‘์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (stop(...) ์ž‘์—…์„ ํ†ตํ•ด). 

 

Actor API


์•กํ„ฐ(XState์—์„œ ๊ตฌํ˜„๋จ)์—๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

  • id ์†์„ฑ : ๋กœ์ปฌ ์‹œ์Šคํ…œ์—์„œ ์•กํ„ฐ๋ฅผ ๊ณ ์œ ํ•˜๊ฒŒ ์‹๋ณ„ํ•˜๋Š” 
  •  .send(...) ๋ฉ”์„œ๋“œ : ์ด ์•กํ„ฐ์— ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‚ด๋Š” ๋ฐ ์‚ฌ์šฉ
  • .getSnapshot() ๋ฉ”์„œ๋“œ. : ์•กํ„ฐ์˜ ๋งˆ์ง€๋ง‰ ๋ฐฉ์ถœ ๊ฐ’์„ ๋™๊ธฐ์ ์œผ๋กœ ๋ฐ˜ํ™˜

์„ ํƒ์  ๋ฉ”์„œ๋“œ๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  •  .stop() ๋ฉ”์„œ๋“œ : ์•กํ„ฐ๋ฅผ ์ค‘์ง€ํ•˜๊ณ  ํ•„์š”ํ•œ ์ •๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š”
  • .subscribe(...) ๋ฉ”์„œ๋“œ :์˜ต์ €๋ฒ„๋ธ” ์•กํ„ฐ๋ฅผ ์œ„ํ•œ 
๊ธฐ์กด (invoked service patterns)์€ ์ด ์ธํ„ฐํŽ˜์ด์Šค์— ๋งž์Šต๋‹ˆ๋‹ค.
  • Invoked promises ๋Š” ์ˆ˜์‹ ๋œ ์ด๋ฒคํŠธ๋ฅผ ๋ฌด์‹œํ•˜๊ณ  ์ตœ๋Œ€ ํ•˜๋‚˜์˜ ์ด๋ฒคํŠธ๋ฅผ ๋ถ€๋ชจ์—๊ฒŒ ๋‹ค์‹œ ๋ณด๋‚ด๋Š” ์•กํ„ฐ์ž…๋‹ˆ๋‹ค.
  • Invoked callbacks์€ ๋ถ€๋ชจ์—๊ฒŒ ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‚ด๊ณ (์ฒซ ๋ฒˆ์งธ ์ฝœ๋ฐฑ ํŒŒ๋ผ๋ฏธํ„ฐ), ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ (๋‘ ๋ฒˆ์งธ onReceive ํŒŒ๋ผ๋ฏธํ„ฐ)ํ•˜๊ณ , ์ด์— ๋Œ€ํ•ด ์กฐ์น˜๋ฅผ ์ทจํ•  ์ˆ˜ ์žˆ๋Š” ์•กํ„ฐ์ž…๋‹ˆ๋‹ค.
  • Invoked machines ์€ ์ด๋ฒคํŠธ๋ฅผ ๋ถ€๋ชจ(sendParent(...) ์•ก์…˜) ๋˜๋Š” ํ•ด๋‹น ๋จธ์‹ ์ด ์ฐธ์กฐํ•˜๋Š” ๋‹ค๋ฅธ ์•กํ„ฐ์— ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‚ด๊ณ (send(...) ์•ก์…˜), ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜๊ณ , ์ด์— ๋Œ€ํ•ด ๋ฐ˜์‘(์ƒํƒœ ์ „ํ™˜ ๋ฐ ์•ก์…˜)ํ•  ์ˆ˜ ์žˆ๋Š” ์•กํ„ฐ์ž…๋‹ˆ๋‹ค. ์ƒˆ๋กœ์šด ์•กํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ณ (spawn(...) ํ•จ์ˆ˜), ์•กํ„ฐ๋ฅผ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค.
  • Invoked observables ์€ ๋ฐฉ์ถœ๋œ ๊ฐ’์ด ๋ถ€๋ชจ์—๊ฒŒ ๋‹ค์‹œ ๋ณด๋‚ผ ์ด๋ฒคํŠธ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์•กํ„ฐ์ž…๋‹ˆ๋‹ค.
๋ฐฉ์ถœ๋œ ๊ฐ’์ด๋ž€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ?
์•กํ„ฐ๊ฐ€ ๋‚ด๋ณด๋‚ธ ๊ฐ’์€ ๊ตฌ๋…์ž๊ฐ€ ์•กํ„ฐ์˜ .subscribe(...) ๋ฉ”์„œ๋“œ์—์„œ ๋ฐ›๋Š” ๊ฐ’์ž…๋‹ˆ๋‹ค.
  • ์„œ๋น„์Šค์˜ ๊ฒฝ์šฐ ํ˜„์žฌ ์ƒํƒœ๊ฐ€ ๋ฐฉ์ถœ๋ฉ๋‹ˆ๋‹ค.
  • Promise์˜ ๊ฒฝ์šฐ resolved๋œ ๊ฐ’(๋˜๋Š” unfulfilled์˜ ๊ฒฝ์šฐ undefined)์ด ๋ฐฉ์ถœ๋ฉ๋‹ˆ๋‹ค
    • ๋Œ€๊ธฐ(pending): ์ดํ–‰ํ•˜์ง€๋„, ๊ฑฐ๋ถ€ํ•˜์ง€๋„ ์•Š์€ ์ดˆ๊ธฐ ์ƒํƒœ.
    • ์ดํ–‰(fulfilled): ์—ฐ์‚ฐ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋จ.
    • ๊ฑฐ๋ถ€(rejected): ์—ฐ์‚ฐ์ด ์‹คํŒจํ•จ.
  • Observable์˜ ๊ฒฝ์šฐ ๊ฐ€์žฅ ์ตœ๊ทผ์— ๋ฐฉ์ถœ๋œ ๊ฐ’์ด ๋ฐฉ์ถœ๋ฉ๋‹ˆ๋‹ค.
  • ์ฝœ๋ฐฑ์˜ ๊ฒฝ์šฐ ์•„๋ฌด ๊ฒƒ๋„ ๋‚ด๋ณด๋‚ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์•กํ„ฐ ์Šคํฌ๋‹(Spawn)


Akka ๋˜๋Š” Erlang๊ณผ ๊ฐ™์€ ์•กํ„ฐ ๋ชจ๋ธ ๊ธฐ๋ฐ˜ ์–ธ์–ด์™€ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์•กํ„ฐ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  ์ปจํ…์ŠคํŠธ์—์„œ ์ฐธ์กฐ๋ฉ๋‹ˆ๋‹ค. (assign(...) ์ž‘์—…์˜ ๊ฒฐ๊ณผ๋กœ).

  • 'xstate'์—์„œ ์Šคํฐ ๊ธฐ๋Šฅ ๊ฐ€์ ธ์˜ค๊ธฐ assign(...)
  • ์•ก์…˜์—์„œ spawn(...)์„ ์‚ฌ์šฉํ•˜์—ฌ ์ƒˆ ์•กํ„ฐ ์ฐธ์กฐ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
spawn(...) ํ•จ์ˆ˜๋Š” 1๊ฐœ ๋˜๋Š” 2๊ฐœ์˜ ์ธ์ˆ˜๋ฅผ ์ œ๊ณตํ•˜์—ฌ ์•กํ„ฐ ์ฐธ์กฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
  • entity - ์•กํ„ฐ์˜ ํ–‰๋™์„ ๋‚˜ํƒ€๋‚ด๋Š” (reactive) ๊ฐ’ ๋˜๋Š” ๊ธฐ๊ณ„. ์—”ํ‹ฐํ‹ฐ์— ๊ฐ€๋Šฅํ•œ ํƒ€์ž…:
    • ๋จธ์‹ 
    • ์ฝœ๋ฐฑ
    • ํ”„๋กœ๋ฏธ์Šค
    • ์˜ต์ €๋ฒ„๋ธ”
  • name(์„ ํƒ ์‚ฌํ•ญ) - ์•กํ„ฐ๋ฅผ ๊ณ ์œ ํ•˜๊ฒŒ ์‹๋ณ„ํ•˜๋Š” ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์ƒ์„ฑ๋œ ๋ชจ๋“  ์•กํ„ฐ์™€ ํ˜ธ์ถœ๋œ ์„œ๋น„์Šค์— ๋Œ€ํ•ด ๊ณ ์œ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

spawn์€ ๋‹ค์Œ ์˜ต์…˜์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ๋Š” ๋‘ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ options ๊ฐ์ฒด๋ฅผ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.

  • name(์„ ํƒ ์‚ฌํ•ญ) - ์•กํ„ฐ๋ฅผ ๊ณ ์œ ํ•˜๊ฒŒ ์‹๋ณ„ํ•˜๋Š” ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์€ ์ƒ์„ฑ๋œ ๋ชจ๋“  ์•กํ„ฐ์™€ ํ˜ธ์ถœ๋œ ์„œ๋น„์Šค์— ๋Œ€ํ•ด ๊ณ ์œ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. autoForward - (์„ ํƒ ์‚ฌํ•ญ) ์ด ์‹œ์Šคํ…œ์— ์ „์†ก๋œ ๋ชจ๋“  ์ด๋ฒคํŠธ๊ฐ€ ํ˜ธ์ถœ๋œ ์ž์‹์—๊ฒŒ๋„ ์ „์†ก(๋˜๋Š” ์ „๋‹ฌ)๋˜์–ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ true(๊ธฐ๋ณธ์ ์œผ๋กœ false)
  • sync - (์„ ํƒ ์‚ฌํ•ญ) ์ด ๋จธ์‹ ์ด ์ƒ์„ฑํ•œ ์ž์‹ ๋จธ์‹ ์˜ ์ƒํƒœ๋ฅผ ์ž๋™์œผ๋กœ ๊ตฌ๋…ํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ true, ์ƒํƒœ๋Š” ์ž์‹ ๋จธ์‹  ์ฐธ์กฐ์— .state๋กœ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
import { createMachine, spawn } from 'xstate';
import { todoMachine } from './todoMachine';

const todosMachine = createMachine({
  // ...
  on: {
    'NEW_TODO.ADD': {
      actions: assign({
        todos: (context, event) => [
          ...context.todos,
          {
            todo: event.todo,
            // add a new todoMachine actor with a unique name
            ref: spawn(todoMachine, `todo-${event.id}`)
          }
        ]
      })
    }
    // ...
  }
});

spawn(...)์— ์ด๋ฆ„ ์ธ์ˆ˜๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š์œผ๋ฉด ๊ณ ์œ ํ•œ ์ด๋ฆ„์ด ์ž๋™์œผ๋กœ ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค.

์ด ์ด๋ฆ„์€ ๊ฒฐ์ •์ ์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค โš ๏ธ.

ํŒ

const ActorRef = spawn(someMachine) ์„ ์ปจํ…์ŠคํŠธ์˜ ์ผ๋ฐ˜ ๊ฐ’์œผ๋กœ ์ทจ๊ธ‰ํ•˜์‹ญ์‹œ์˜ค.
๋…ผ๋ฆฌ์ ์ธ ์š”๊ตฌ ์‚ฌํ•ญ์— ๋”ฐ๋ผ ์ด ActorRef๋ฅผ ์ปจํ…์ŠคํŠธ ๋‚ด ์•„๋ฌด ๊ณณ์—๋‚˜ ๋ฐฐ์น˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
assign(...)์˜ ํ• ๋‹น ํ•จ์ˆ˜ ๋‚ด์— ์žˆ๋Š” ํ•œ ํ•ด๋‹น ์•กํ„ฐ๊ฐ€ ์ƒ์„ฑ๋œ ์„œ๋น„์Šค๋กœ ์ปจํ…์ŠคํŠธ ๋ฒ”์œ„๊ฐ€ ์ง€์ •๋ฉ๋‹ˆ๋‹ค.

๊ฒฝ๊ณ 

ํ• ๋‹น(assign-assignment) ํ•จ์ˆ˜ ์™ธ๋ถ€์—์„œ spawn(...)์„ ํ˜ธ์ถœํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค.

์ด๊ฒƒ์€ ํšจ๊ณผ๊ฐ€ ์—†๋Š” ๊ณ ์•„ ์•กํ„ฐ(๋ถ€๋ชจ ์—†๋Š”)๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

// โŒ Never call spawn(...) externally
const someActorRef = spawn(someMachine);

// โŒ spawn(...) is not an action creator
{
  actions: spawn(someMachine);
}

// โŒ Do not assign spawn(...) outside of an assignment function
{
  actions: assign({
    // remember: this is called immediately, before a service starts
    someActorRef: spawn(someMachine)
  });
}

// โœ… Assign spawn(...) inside an assignment function
{
  actions: assign({
    someActorRef: () => spawn(someMachine)
  });
}
๋‹ค์–‘ํ•œ ํƒ€์ž…์˜ ๊ฐ’์„ ์•กํ„ฐ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

Sending Events to Actors (์•กํ„ฐ๋กœ ์ด๋ฒคํŠธ ๋ณด๋‚ด๊ธฐ)


send() ์ž‘์—…์„ ์‚ฌ์šฉํ•˜๋ฉด ๋Œ€์ƒ ํ‘œํ˜„์‹์„ ํ†ตํ•ด ์ด๋ฒคํŠธ๋ฅผ ์•กํ„ฐ์—๊ฒŒ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

(ํ•ด๋‹น ์ƒํƒœ์— ์ง„์ž… ํ›„, ์•กํ„ฐ์— ์‹ ํ˜ธ๋ฅผ ๋ณด๋ƒ„)

const machine = createMachine({
  // ...
  states: {
    active: {
      entry: assign({
        someRef: () => spawn(someMachine)
      }),
      on: {
        SOME_EVENT: {
          // Use a target expression to send an event
          // to the actor reference
          actions: send({ type: 'PING' }, { to: (context) => context.someRef })
        }
      }
    }
  }
});
ํŒ
spawn(...)์— ๊ณ ์œ ํ•œ ์ด๋ฆ„ ์ธ์ˆ˜๋ฅผ ์ œ๊ณตํ•˜๋ฉด ๋Œ€์ƒ ํ‘œํ˜„์‹์—์„œ ์ฐธ์กฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
const loginMachine = createMachine({
  // ...
  entry: assign({
    formRef: () => spawn(formMachine, 'form')
  }),
  states: {
    idle: {
      on: {
        LOGIN: {
          actions: send({ type: 'SUBMIT' }, { to: 'form' })
        }
      }
    }
  }
});

Stopping Actors (์•กํ„ฐ ์ข…๋ฃŒ)


์•กํ„ฐ๋Š” stop(...) ์•ก์…˜ ํฌ๋ฆฌ์—์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ค‘์ง€๋ฉ๋‹ˆ๋‹ค.
const someMachine = createMachine({
  // ...
  entry: [
    // Stopping an actor by reference
    stop((context) => context.someActorRef),
    // Stopping an actor by ID
    stop('some-actor')
  ]
});

Spawning Promises (ํ”„๋กœ๋ฏธ์Šค ์•กํ„ฐ ์ƒ์„ฑ)


์•กํ„ฐ๊ฐ€ ๋‚ด๋ณด๋‚ธ ๊ฐ’์€ ๊ตฌ๋…์ž๊ฐ€ ์•กํ„ฐ์˜ .subscribe(...) ๋ฉ”์„œ๋“œ์—์„œ ๋ฐ›๋Š” ๊ฐ’์ž…๋‹ˆ๋‹ค.
ํ”„๋ผ๋ฏธ์Šค ํ˜ธ์ถœ๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ํ”„๋ผ๋ฏธ์Šค๋Š” ์•กํ„ฐ๋กœ ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
Promise์˜ ๊ฒฝ์šฐ resolved๋œ ๊ฐ’(๋˜๋Š” unfulfilled์˜ ๊ฒฝ์šฐ undefined)์ด ๋ฐฉ์ถœ๋ฉ๋‹ˆ๋‹ค

์ˆ˜์‹ ๋œ ์ด๋ฒคํŠธ๋ฅผ ๋ฌด์‹œํ•˜๊ณ  ์ตœ๋Œ€ ํ•˜๋‚˜์˜ ์ด๋ฒคํŠธ๋ฅผ ๋ถ€๋ชจ์—๊ฒŒ ๋‹ค์‹œ ๋ณด๋‚ด๋Š” ์•กํ„ฐ์ž…๋‹ˆ๋‹ค.

๋จธ์‹ ์œผ๋กœ ๋‹ค์‹œ ์ „์†ก๋˜๋Š” ์ด๋ฒคํŠธ๋Š” 'done.invoke.<ID>' ์ž‘์—…์ด ๋ฉ๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—๋Š” ํŽ˜์ด๋กœ๋“œ์˜ ๋ฐ์ดํ„ฐ ์†์„ฑ์œผ๋กœ ์•ฝ์† ์‘๋‹ต์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.
// Returns a promise
const fetchData = (query) => {
  return fetch(`http://example.com?query=${event.query}`).then((data) =>
    data.json()
  );
};

// ...
{
  actions: assign({
    ref: (_, event) => spawn(fetchData(event.query))
  });
}
// ...
 
๊ฒฝ๊ณ 
ํ”„๋ผ๋ฏธ์Šค ์•กํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์€ ๊ถŒ์žฅํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
ํ”„๋ผ๋ฏธ์Šค ํ˜ธ์ถœ์€ ์ƒํƒœ์— ์˜์กดํ•˜๊ณ (์ž์ฒด ์ทจ์†Œ) ๋” ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๋™์ž‘์„ ๊ฐ€์ง€๊ธฐ ๋•Œ๋ฌธ์— ์ด์— ๋Œ€ํ•œ ๋” ๋‚˜์€ ํŒจํ„ด์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

Spawning Callbacks (์ฝœ๋ฐฑ ์•กํ„ฐ ์ƒ์„ฑ)


๋ถ€๋ชจ์—๊ฒŒ ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‚ด๊ณ (์ฒซ ๋ฒˆ์งธ ์ฝœ๋ฐฑ ํŒŒ๋ผ๋ฏธํ„ฐ), ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ (๋‘ ๋ฒˆ์งธ onReceive ํŒŒ๋ผ๋ฏธํ„ฐ)ํ•˜๊ณ ,

์ด์— ๋Œ€ํ•ด ์กฐ์น˜๋ฅผ ์ทจํ•  ์ˆ˜ ์žˆ๋Š” ์•กํ„ฐ์ž…๋‹ˆ๋‹ค.

์•„๋ฌด ๊ฐ’๋„ ๋ฐฉ์ถœํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์ฝœ๋ฐฑ์„ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ๊ณผ ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์ฝœ๋ฐฑ์€ ์•กํ„ฐ๋กœ ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์ด ์˜ˆ์ œ๋Š” ๋งค์ดˆ ์ž์ฒด์ ์œผ๋กœ ์นด์šดํŠธ๋ฅผ ์ฆ๊ฐ€์‹œํ‚ค์ง€๋งŒ { type: 'INC' } ์ด๋ฒคํŠธ์—๋„ ๋ฐ˜์‘ํ•  ์ˆ˜ ์žˆ๋Š” ์นด์šดํ„ฐ ์ธํ„ฐ๋ฒŒ ์•กํ„ฐ๋ฅผ ๋ชจ๋ธ๋งํ•ฉ๋‹ˆ๋‹ค.
const counterInterval = (callback, receive) => {
  let count = 0;

  const intervalId = setInterval(() => {
    callback({ type: 'COUNT.UPDATE', count });
    count++;
  }, 1000);

  receive(event => {
    if (event.type === 'INC') {
      count++;
    }
  });

  return () => { clearInterval(intervalId); }
}

const machine = createMachine({
  // ...
  {
    actions: assign({
      counterRef: () => spawn(counterInterval)
    })
  }
  // ...
});
์ด๋ฒคํŠธ๋ฅผ ์•กํ„ฐ์—๊ฒŒ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
const machine = createMachine({
  // ...
  on: {
    'COUNTER.INC': {
      actions: send({ type: 'INC' }, { to: (context) => context.counterRef })
    }
  }
  // ...
});

 

Spawning Observables (์˜ต์ €๋ฒ„๋ธ” ์•กํ„ฐ ์ƒ์„ฑ)


Invoked observables ์€ ๋ฐฉ์ถœ๋œ ๊ฐ’์ด ๋ถ€๋ชจ์—๊ฒŒ ๋‹ค์‹œ ๋ณด๋‚ผ ์ด๋ฒคํŠธ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์•กํ„ฐ์ž…๋‹ˆ๋‹ค.

Observable์˜ ๊ฒฝ์šฐ ๊ฐ€์žฅ ์ตœ๊ทผ์— ๋ฐฉ์ถœ๋œ ๊ฐ’์ด ๋ฐฉ์ถœ๋ฉ๋‹ˆ๋‹ค.

 

import { interval } from 'rxjs';
import { map } from 'rxjs/operators';

const createCounterObservable = (ms) => interval(ms)
  .pipe(map(count => ({ type: 'COUNT.UPDATE', count })))

const machine = createMachine({
  context: { ms: 1000 },
  // ...
  {
    actions: assign({
      counterRef: ({ ms }) => spawn(createCounterObservable(ms))
    })
  }
  // ...
  on: {
    'COUNT.UPDATE': { /* ... */ }
  }
});

Spawning Machines (์ƒํƒœ ๊ธฐ๊ณ„ ์ƒ์„ฑ)


์ƒํƒœ ๊ธฐ๊ณ„๋Š” ๊ฐ€์žฅ ๋งŽ์€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์•กํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ€์žฅ ํšจ๊ณผ์ ์ธ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

์ƒ์„ฑํ•œ ๋จธ์‹ ์€ ๋จธ์‹ ์ด spawn(machine)์œผ๋กœ ์ „๋‹ฌ๋˜๋Š” invoking machine๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

์˜ˆ์ œ : ๋ถ€๋ชจ ๋จธ์‹ ์ธ ๋กœ์ปฌ ๋จธ์‹ ์— LOCAL.WAKE ๋ช…๋ น => ๋ถ€๋ชจ๋Š” ์ž์‹(Remote)๋ฅผ ๊นจ์›€. => ์ž์‹์€ online์ƒํƒœ๊ฐ€ ๋œ ํ›„ 1์ดˆ ํ›„ ๋ถ€๋ชจ์— REMOTE.ONLINE ์ด๋ฒคํŠธ ์ „๋‹ฌ. ๋ถ€๋ชจ๋Š” connected ์ƒํƒœ๋กœ ์ „ํ™˜.

// Remote
const remoteMachine = createMachine({
  id: 'remote',
  initial: 'offline',
  states: {
    offline: {
      on: {
        WAKE: 'online'
      }
    },
    online: {
      after: {
        1000: {
          actions: sendParent('REMOTE.ONLINE')
        }
      }
    }
  }
});

// Local
const parentMachine = createMachine({
  id: 'parent',
  initial: 'waiting',
  context: {
    localOne: null
  },
  states: {
    waiting: {
      entry: assign({
        localOne: () => spawn(remoteMachine)
      }),
      on: {
        'LOCAL.WAKE': {
          actions: send({ type: 'WAKE' }, { to: (context) => context.localOne })
        },
        'REMOTE.ONLINE': { target: 'connected' }
      }
    },
    connected: {}
  }
});

const parentService = interpret(parentMachine)
  .onTransition((state) => console.log(state.value))
  .start();

parentService.send({ type: 'LOCAL.WAKE' });
// => 'waiting'
// ... after 1000ms
// => 'connected'

Syncing and Reading State (์ƒํƒœ ๋™๊ธฐํ™” ๋ฐ ์ƒํƒœ ์ฝ๊ธฐ)


์•กํ„ฐ ๋ชจ๋ธ์˜ ์ฃผ์š” ์‹ ์กฐ(tenets) ์ค‘ ํ•˜๋‚˜๋Š” ์•กํ„ฐ ์ƒํƒœ๊ฐ€ ๋น„๊ณต๊ฐœ์ด๊ณ  ๋กœ์ปฌ์ด๋ผ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.
์•กํ„ฐ๊ฐ€ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ์„ ํ†ตํ•ด ๊ณต์œ ํ•˜๋„๋ก ์„ ํƒํ•˜์ง€ ์•Š๋Š” ํ•œ ํ•ด๋‹น ์ƒํƒœ๋Š” ์ ˆ๋Œ€ ๊ณต์œ ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
์ด ๋ชจ๋ธ์„ ๊ณ ์ˆ˜ํ•˜๋ฉด ์•กํ„ฐ๋Š” ์ตœ์‹  ์ƒํƒœ๋กœ ํŠน๋ณ„ํ•œ "์—…๋ฐ์ดํŠธ" ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‚ด ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งˆ๋‹ค ๋ถ€๋ชจ์—๊ฒŒ ์•Œ๋ฆด ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
์ฆ‰, ๋ถ€๋ชจ ์•กํ„ฐ๋Š” ์ž์‹ ์•กํ„ฐ์˜ ์ƒํƒœ๋ฅผ ๊ตฌ๋…ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
 
์ด๋ ‡๊ฒŒ ํ•˜๋ ค๋ฉด { sync: true }๋ฅผ spawn(...) ์˜ต์…˜์œผ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.
// ...
{
  actions: assign({
    // Actor will send update event to parent whenever its state changes
    someRef: () => spawn(todoMachine, { sync: true })
  });
}
// ...โ€‹

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ๋จธ์‹ ์ด ์ƒ์„ฑ๋œ ์ž์‹ ๋จธ์‹ ์˜ ์ƒํƒœ์— ์ž๋™์œผ๋กœ ๋“ฑ๋ก๋˜๋ฉฐ, ์ด ์ƒํƒœ๋Š” ๊ณ„์† ์—…๋ฐ์ดํŠธ๋˜๊ณ  getSnapshot()์„ ํ†ตํ•ด ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

someService.onTransition((state) => {
  const { someRef } = state.context;

  console.log(someRef.getSnapshot());
  // => State {
  //   value: ...,
  //   context: ...
  // }
});โ€‹
sync๋ฅผ ํ•ด๋‘๋ฉด ๋ถ€๋ชจ์—์„œ ์•„๋ž˜์ฒ˜๋Ÿผ ์ž์‹ ์ƒํƒœ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.
someService.onTransition((state) => {
  const { someRef } = state.context;
  // ์ž์‹ ์ƒํƒœ ์ ‘๊ทผ
  console.log(someRef.state);
  // => State {
  //   value: ...,
  //   context: ...
  // }
});
๊ฒฝ๊ณ 

๊ธฐ๋ณธ์ ์œผ๋กœ ๋™๊ธฐํ™”๋Š” false๋กœ ์„ค์ •๋ฉ๋‹ˆ๋‹ค.

๋™๊ธฐํ™”๊ฐ€ ๋น„ํ™œ์„ฑํ™”๋œ ๊ฒฝ์šฐ ์•กํ„ฐ์˜ .state๋ฅผ ์ฝ์ง€ ๋งˆ์‹ญ์‹œ์˜ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด staleํ•œ ์ƒํƒœ๋ฅผ ์ฐธ์กฐํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

์ฃผ : ํ•ด๋‹น ํŒจํ„ด์€ ๋ถ€๋ชจ๋Š” ์ž์‹์„ ๋ณผ ์ˆ˜ ์žˆ์œผ๋ฉฐ, subscribe ํ•˜์ง€๋งŒ ์ž์‹์€ ๋ถ€๋ชจ๋ฅผ ๋ชจ๋ฆ„

Sending Updates (์—…๋ฐ์ดํŠธ ๋ณด๋‚ด๊ธฐ) 


๋ถ€๋ชจ์™€ ๋™๊ธฐํ™”๋˜์ง€ ์•Š์€ ์•กํ„ฐ์˜ ๊ฒฝ์šฐ ์•กํ„ฐ๋Š” sendUpdate()๋ฅผ ํ†ตํ•ด ๋ถ€๋ชจ ์‹œ์Šคํ…œ์— ๋ช…์‹œ์  ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
import { createMachine, sendUpdate } from 'xstate';

const childMachine = createMachine({
  // ...
  on: {
    SOME_EVENT: {
      actions: [
        // ...
        // Creates an action that sends an update event to parent
        sendUpdate()
      ]
    }
  }
});

TIP

๋ชจ๋“  ์ƒํƒœ ๋ณ€๊ฒฝ์„ ๊ตฌ๋…ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ๋ช…์‹œ์ ์œผ๋กœ ๋ถ€๋ชจ์—๊ฒŒ ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‚ด๋Š” ๊ฒƒ์„ ์„ ํ˜ธํ•ฉ๋‹ˆ๋‹ค(sendUpdate() ์„ ํ˜ธ).
์ƒ์„ฑ๋œ ์‹œ์Šคํ…œ๊ณผ ๋™๊ธฐํ™”ํ•˜๋ฉด ์ž์‹์—์„œ ์—…๋ฐ์ดํŠธํ•  ๋•Œ๋งˆ๋‹ค ์ž์‹์—์„œ ๋ถ€๋ชจ๋กœ ์ƒˆ๋กœ์šด "xstate.update" ์ด๋ฒคํŠธ๊ฐ€ ์ „์†ก๋˜๊ธฐ ๋•Œ๋ฌธ์—
"์ˆ˜๋‹ค์Šค๋Ÿฌ์šดchatty" ์ด๋ฒคํŠธ ๋กœ๊ทธ๊ฐ€ ์ƒ์„ฑ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 
 

Quick Reference


Import spawn to spawn actors:

import { spawn } from 'xstate';

Spawn actors in assign action creators:

// ...
{
  actions: assign({
    someRef: (context, event) => spawn(someMachine)
  });
}
// ...

Spawn different types of actors: (๋‹ค์–‘ํ•œ ํƒ€์ž…์˜ ์•กํ„ฐ ์ƒ์„ฑ)

// ...
{
  actions: assign({
    // From a promise 
    // ์ˆ˜์‹ ๋œ ์ด๋ฒคํŠธ๋ฅผ ๋ฌด์‹œํ•˜๊ณ  ์ตœ๋Œ€ ํ•˜๋‚˜์˜ ์ด๋ฒคํŠธ๋ฅผ ๋ถ€๋ชจ์—๊ฒŒ ๋‹ค์‹œ ๋ณด๋‚ด๋Š” ์•กํ„ฐ์ž…๋‹ˆ๋‹ค.
    promiseRef: (context, event) =>
      spawn(
        new Promise((resolve, reject) => {
          // ...
        }),
        'my-promise'
      ),

    // From a callback
    // ๋ฆฌํ„ด ๊ฐ’์€ ๋ฌด์˜๋ฏธํ•จ
    callbackRef: (context, event) =>
      spawn((callback, receive) => {
        // send to parent
        callback('SOME_EVENT');

        // receive from parent
        receive((event) => {
          // handle event
        });

        // disposal
        return () => {
          /* do cleanup here */
        };
      }),

    // From an observable
    // ๋ฆฌํ„ด๊ฐ’์€ ์ด๋ฒคํŠธ ๊ฐ์ฒด
    observableRef: (context, event) => spawn(someEvent$),

    // From a machine    
    machineRef: (context, event) =>
      spawn(
        createMachine({
          // ...
        })
      )
  });
}
// ...
์•กํ„ฐ์™€ ์ƒํƒœ ๋™๊ธฐํ™”: (๋น„์ถ”)
// ...
{
  actions: assign({
    someRef: () => spawn(someMachine, { sync: true })
  });
}
// ...
์•กํ„ฐ๋กœ๋ถ€ํ„ฐ ์Šค๋ƒ…์ƒท ์–ป๊ธฐ: 4.20.0+ ๋ฒ„์ „ ์ด์ƒ๋ถ€ํ„ฐ
service.onTransition((state) => {
  const { someRef } = state.context;

  someRef.getSnapshot();
  // => State { ... }
});
์•ก์…˜ ํฌ๋ฆฌ์—์ดํ„ฐ send๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์•กํ„ฐ์—๊ฒŒ ์ด๋ฒคํŠธ ๋ณด๋‚ด๊ธฐ:
// ...
{
  actions: send(
    { type: 'SOME_EVENT' },
    {
      to: (context) => context.someRef
    }
  );
}
// ...
send ํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•˜์—ฌ ์•กํ„ฐ์— ๋ฐ์ดํ„ฐ์™€ ํ•จ๊ป˜ ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋‚ด๊ธฐ
// ...
{
  actions: send((context, event) => ({ ...event, type: 'SOME_EVENT' }), {
    to: (context) => context.someRef
  });
}
// ...
sendParent ์•ก์…˜ ํฌ๋ฆฌ์—์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์•กํ„ฐ์—์„œ ๋ถ€๋ชจ ์•กํ„ฐ๋กœ ์ด๋ฒคํŠธ ๋ณด๋‚ด๊ธฐ:
// ...
{
  actions: sendParent({ type: 'ANOTHER_EVENT' });
}
// ...
sendParent ํ‘œํ˜„์‹์„ ์‚ฌ์šฉํ•˜์—ฌ ์•กํ„ฐ์—์„œ ๋ถ€๋ชจ ์•กํ„ฐ๋กœ ๋ฐ์ดํ„ฐ๊ฐ€ ํฌํ•จ๋œ ์ด๋ฒคํŠธ๋ฅผ ๋ณด๋ƒ…๋‹ˆ๋‹ค.
// ...
{
  actions: sendParent((context, event) => ({
    ...context,
    type: 'ANOTHER_EVENT'
  }));
}
// ...
์ปจํ…์ŠคํŠธ์—์„œ ์•กํ„ฐ(์ž์‹) ์ฐธ์กฐ:
someService.onTransition((state) => {
  const { someRef } = state.context;

  console.log(someRef);
  // => { id: ..., send: ... }
});
๋ฐ˜์‘ํ˜•