XState

회사 퍼널 코드 구조 개선

AS-IS: use-funnel + recoil

  • 스텝간 네비게이션 방식: setStep(from use-funnel)으로 직접코딩 (스텝이 다음 스텝명을 알고있음)
    • 스텝 이동 경로를 한 눈에 파악하기 위하여 useFunnel을 사용하는 상위 컴포넌트에서만 onNext를 하길 권장한다
  • 리필 방식: 리필시엔 다음스텝명이 달라야 하므로 리필 플로우를 위한 컴포넌트를 새로 만들어야한다 (onNext 커스텀 위하여)
  • 퍼널 상태 수집: recoil
    • 여기저기서 쓸 수 있는 특성이 장점이자 단점
      • 퍼널 외부에서도 업데이트할 수 있다 (얘도 장점이자 단점)
    • 퍼널 흐름과 수집된 상태가 실제로는 결합 되어있지만, 코드상으로 표현하기가 어렵다

TO-BE: use-funnel + xstate

  • 스텝간 네비게이션 방식: xstate의 전체 그래프에 의존 (스텝이 다음 스텝명을 모름)
    • 내부에서는 무지성으로 xstate 액션 호출. xstate가 액션에 따라 다음 스텝을 안내해줌.
  • 리필 방식: xstate 상태에 의존 (TBD)
    • 리필중이면 다른 경로를 탈 수 있도록 xstate 그래프에 코딩해둔다.
    • 퍼널 컴포넌트를 새로 만들지 않아도 된다
  • 퍼널 상태 수집: xstate context
    • 해당 퍼널 내부에서만 상태를 업데이트할 수 있다
    • 퍼널 흐름과 수집된 상태를 엮어 관리할 수 있다(TBD)
  • 강점: 퍼널 흐름과 내부상태를 centralize한다
    • 올바른 다이어그램만 그려두면 퍼널이 우리가 설계한대로 동작할거라고 기대할 수 있다
      • 다이어그램을 시각화하여 공유할 수 있다(sort of 문서화).
        • 개발자도 흐름을 파악하기 용이하고, 비개발자도 쉽게 볼 수 있다.
    • 퍼널 흐름 + 수집상태를 디버깅할 때 코드 한 군데만 보면 된다

TO-TO-BE: use-fsm-funnel

  • use-funnel + fsm방식을 한 번 추상화하여 재사용가능하도록 한다 (xstate 의존성은 넣을지말지 고민)

개발 전략

  1. 알뜰폰 전체 퍼널 중 일부(개통퍼널)에만 적용
  2. 고도화 시키며 쓸만한지 판별
  3. 괜찮으면 전체 적용
  4. 이후에 라이브러리 개발.

나의 이해

유한 상태 기계란?

  • like 신호등.
    • 빨강/노랑/초록 세개의 상태 사이를 넘나든다.
    • 한번에 빨강이면서 노랑일 순 없다.
    • 초록이 시간이 지나면 노랑이 되고, 노랑이 시간이 지나면 빨강이 됨 (상태 사이를 바꾸는 조건이 있다. 상태는 특정 조건이 되면 다른 상태로 변한다)

Boolean explosion

신호등을 코드로 짜기 (빨/초 만 있다 가정)

Step1. Boolean들로 짜기

  • 가장 쉬운 접근방법, boolean으로 코드 짜기
    • boolean 상태 2개: isRed, isGreen
    • 액션: 토글
      • 토글을 호출하면 red에서 green으로, 혹은 green에서 red로 색을 바꾼다
  • 여기에 상태 하나를 추가한다면?
    • boolean 상태 3개: isRed, isGreen, isBroken
    • 액션: 토글, 운석떨어짐
      • 운석이 떨어지면 isBroken: true로 바꾼다
  • 버그가 날 구멍이 생김!
    • 토글 액션에 isBroken이면 토글 안 되도록 추가 코딩 해줘야한다.
    • 이런식으로 boolean 값이 추가될수록 여기저기를 추가적으로 수정해줘야함…

Step2. 열거형으로 개선하기

  • 표현 가능한 상태를 하나만 둔다
    • state: RED | GREEN | BROKEN
    • 토글이든, 운석떨어짐이든 액션마다 현재 가용한 상태를 한 번에 관리할 수 있어(switchcase 사용 등) 버그가 날 구멍이 줄어든다

Step3. 상태머신으로 개선하기

const 신호등상태 = {
  RED: {
    액션: {
      토글: "GREEN",
      운석떨어짐: "BROKEN",
    },
  },
  GREEN: {
    액션: {
      토글: "RED",
      운석떨어짐: "BROKEN",
    },
  },
  BROKEN: {
    액션: {},
    특징: "끝", // 더 이상 할 액션이 없으니
  },
}
const 신호등상태머신 = Machine(신호등상태, { 초기값: "RED" })
const 신호등 = interpret(신호등상태머신).start()

신호등.이벤트보내("토글")
console.log(신호등.state.value) // GREEN
신호등.이벤트보내("토글") // RED
신호등.이벤트보내("운석떨어짐") // BROKEN
  • 가질 수 있는 상태와 이 상태가 다른 상태로 바뀔때의 액션을 적어준다.
  • 이벤트를 보내주면 상태가 알아서 숑숑 바뀐다

FSM의 좋은점

  • 흐름을 centralize하기
  • 디자이너는 이미 FSM의 다이어그램처럼 사고함.
    • 개발자는 개발할 때 바텀업으로 사고하기 쉬움
    • 디자이너와 개발자의 single source of truth를 맞추기
  • 디버깅 쉬움 (퍼널 흐름이 한군데서 관리되니)
    • I had checked 44 different places in the code to finally find the answer.
    • We had abstracted a ton of code for reuse (typically a good idea), but a majority of that abstracted code was capable of affecting the user flow: 너무 재사용가능하게 하면 온갖군데서 진입할수있어서 디버깅 힘들겠군

Building an acquisition Funnel in React with Xstate

https://dev.indooroutdoor.io/building-an-acquisition-funnel-in-react-with-xstate https://dev.indooroutdoor.io/building-an-acquisition-funnel-in-react-with-xstate-part-2 https://medium.com/the-arcadia-source/architecting-a-signup-funnel-app-with-react-and-xstate-part-1-f329b69c2f7e

  • 단단하고, 읽기 좋고, 결정론적인 모델링 프로세스
  • FSM이랑 퍼널의 궁합이 좋은 이유
    • 필요한 순간에 필요한 스텝을 보여줘야 하기 때문
    • 유저 액션 시퀀스에 따라 퍼널 상태가 결정되기 때문
    • 올바른 다이어그램만 그려두면 퍼널이 우리가 설계한대로 동작할거라고 기대할 수 있다!
import { createMachine, assign } from 'xstate';

export const stateMachine = Machine<FunnelData, FunnelEvent>(
  {
    id: "funnel-state-machine",
    initial: "activity", // 초기 스텝이름
    context: { // 퍼널 내부 상태
      activity: undefined,
      attendees: [],
      additional_information: undefined,
      payment: undefined,
    },
    states: {
      activity: {
        on: {
          SELECT_ACTIVITY: "register_attendee",
        },
        exit: ["setActivity"], // 액티비티 스텝 탈출하면 setActivity 액션 불러
      },
      register_attendee: {
        on: {
          ADD_ATTENDEE: "register_attendee",
          ADD_INFO: "additional_information",
          SUBMIT_ATTENDEE: "payment",
        },
        exit: ["addattendee"],
      },
      additional_information: {
        on: {
          SUBMIT_ADDITIONNAL_INFORMATION: "payment",
        },
        exit: ["setAdditionalInformation"],
      },
      payment: {
        type: "final",
      },
    },
  },
  {
    actions: {
      setActivity: assign({
        activity: (context, event) => event.data
      }),
      addattendee: assign(addAttendee),
      setAdditionalInformation: assign(setAddtionalInformation),
    },
    // 요렇게 할 수도 있음
    actions: assign({
      // 퍼널 내부상태(context)를 event.value를 사용해 업데이트 한다
      count: (context, event) => context.count + event.value,

      // assign static value to the message (no function needed)
      message: 'Count changed'
    }),
  }
);

const addAttendee = (context: FunnelData, event: FunnelEvent) => {
  switch (event.type) {
    case ADD_ATTENDEE:
      return {
        context,
        attendees: context.attendees.concat(event.data),
      };
    case SUBMIT_ATTENDEE:
      return {
        context,
        attendees: context.attendees.concat(event.data),
      };
    default:
      return context;
  }
};

Xstate Docs Speedrun

https://www.youtube.com/watch?v=2eurRx-tR-I&t=670s&ab_channel=MattPocock

XState 의 기본 익혀보기


Profile picture

진유림 a.k.a 테니스치다 손목 삔, 풋살하다 인대 나간 개발자 twitter

With love, Yurim Jin