Finite State Machine(유한 상태 기계)을 활용해 복잡한 퍼널 관리하기

유한 상태 기계를 활용해 복잡한 퍼널을 관리하는 방법을 알아봅니다.

📝

이 글은 2024년 5월, 맡고 있던 프로젝트에서 퍼널 개편이 진행되었을 때 개선한 내용을 공유하는 글이에요.

우선, 유한 상태 기계가 뭘까요?

유한 상태 기계란 것을 처음으로 알게된 계기는 약 2년 전, chakra-ui에서 운영 중인 ark 라이브러리였어요. 해당 라이브러리는 Framework Agnostic하게 제작된 zag 라이브러리를 사용하고 있었고, 이 zag 라이브러리 내부에서 상태머신을 사용하고 있었어요.

💡

토스의 임지훈 님께서 Framework Agnostic에 대한 좋은 글을 작성해주셨어요.

자바스크립트에서 XState라는 유한 상태 기계 라이브러리가 있는데, zag는 해당 라이브러리를 그대로 사용하지 않고 최소 기능만 구현하여 사용하고 있어요.
XState를 언급한 이유는 유한 상태 기계를 가장 잘 구현한 라이브러리라고 생각하기 때문이에요.

XState? 그건 뭔데?

XState
XState 시각화 예시

XState는 상태머신을 구현하기 위한 라이브러리로, 상태머신은 상태와 상태 전환을 정의하고 이를 통해 복잡한 상태 관리를 할 수 있게 도와줘요. 특히 시각화를 훌륭하게 지원하는데요, 복잡한 로직의 경우 시각화를 통해 디버깅을 쉽게 할 수 있어요.


다시 돌아와서 유한 상태 기계에 대해 정리해볼게요.

유한 상태 기계란 다음과 같은 특징을 가진 프로그래밍 모델이에요:

  1. 유한한 상태(Finite States): 시스템이 가질 수 있는 모든 상태가 미리 정의되어 있어요.
  2. 상태 전환(Transitions): 한 상태에서 다른 상태로 전환되는 규칙이 명확하게 정의되어 있어요.
  3. 이벤트 기반(Event-driven): 상태 전환은 특정 이벤트에 의해 발생해요.

특히 복잡한 폼이나 다단계 프로세스를 구현할 때 유용하게 사용할 수 있어요.

시각화와 예측 가능성

유한 상태 기계의 가장 큰 장점 중 하나는 시각화가 가능하다는 점이에요. 상태와 전환을 그래프로 표현할 수 있어서, 복잡한 로직도 한눈에 파악할 수 있죠.

예를 들어, 결제 프로세스를 생각해볼까요?

const paymentMachine = {
  initial: 'idle',
  states: {
    idle: {
      on: { START_PAYMENT: 'processing' },
    },
    processing: {
      on: {
        SUCCESS: 'success',
        ERROR: 'error',
      },
    },
    success: {
      on: { RESET: 'idle' },
    },
    error: {
      on: { RETRY: 'processing' },
    },
  },
};

이 코드는 다음과 같이 시각화할 수 있어요:

┌───────┐   (START_PAYMENT)   ┌───────────┐
│  idle ├─────────────────────> processing│
└───────┘                     └───────────┘
                                    ├─ (SUCCESS) → success ──(RESET)──> idle
                                    └─ (ERROR)   → error   ──(RETRY)──> processing

이런 시각화를 통해 얻을 수 있는 이점들이 있어요:

  1. 버그 예방: 모든 상태와 전환이 명시적이라 예상치 못한 상태 변화를 방지할 수 있어요
  2. 코드 리뷰 용이성: 시각적 다이어그램으로 로직을 쉽게 공유하고 검토할 수 있어요
  3. 유지보수성: 새로운 상태나 전환을 추가할 때 영향도를 쉽게 파악할 수 있어요

지원서 퍼널 관리하기

실제 프로젝트에서 유한 상태 기계를 도입하게 된 계기는 복잡한 지원서 퍼널을 관리하기 위해서였어요. 기존 지원서가 다음과 같은 복잡한 요구사항을 가지고 있었거든요:

복잡한 분기 처리

  • 각 문항의 답변에 따라 다음 문항이 동적으로 결정됨
    • 예: 1번 문항에서 A 선택 시 → 2번으로 이동
    • 예: 1번 문항에서 B 선택 시 → 3번으로 이동
  • 이전 답변들의 조합에 따라 다음 문항이 결정되는 경우도 존재
    • 예: 5번 문항은 3-1번과 4번 문항의 답변 조합에 따라 다른 문항으로 분기

추가 요구사항

  1. 양방향 이동: 퍼널 진행 중 이전 항목으로 돌아갈 수 있어야 함
  2. 상태 유지:
    • 모든 답변은 서버에 실시간 저장
    • 재접속 시 이전 진행 상태 복구 및 이전 답변 유지, 양방향 이동 가능
  3. 데이터 정합성: 이전 단계로 돌아가서 답변을 수정할 경우, 연관된 다음 단계들의 데이터 처리

이러한 복잡한 요구사항들을 기존의 방식으로 관리하려고 하니 다음과 같은 문제점들이 있었어요:

  • 상태 관리 로직이 복잡해짐
  • 예외 케이스 처리가 어려움
  • 코드 유지보수가 힘들어짐

이러한 문제들을 해결하기 위해 유한 상태 기계 패턴을 도입하게 되었어요.


상태 기계 직접 구현하기

실제로 상태 기계를 구현하면서 어떻게 활용할 수 있는지 알아볼게요.

🇰🇷

이해를 돕기 위해 예시 코드는 한글로 작성했어요.

1단계: 기본 인터페이스 정의

상태 기계를 만들기 위한 가장 기본적인 구조를 정의해볼게요. 두 가지 핵심 인터페이스가 필요해요.

1) 상태 전환 인터페이스

interface 상태전환<상태, 이벤트> {
  다음상태: 상태;     // 전환 후 도달할 상태
  발생이벤트: 이벤트;  // 전환을 발생시키는 이벤트
  조건?: (현재상태: 상태, 이벤트: 이벤트) => boolean;  // 전환 조건 (선택사항)
}

이 인터페이스는 하나의 상태 전환을 정의해요:

  • 다음상태: 전환 후에 도달할 상태를 지정
  • 발생이벤트: 어떤 이벤트가 발생했을 때 전환할지 정의
  • 조건: 전환이 발생할 조건을 지정 (선택사항)

2) 상태 기계 인터페이스

interface 상태기계<상태 extends string, 이벤트 extends string> {
  [상태: string]: 상태전환<상태, 이벤트>[];
}

전체 상태 기계의 구조를 정의해요:

  • 각 상태를 키로 가지는 객체
  • 각 상태에서 가능한 전환들의 배열을 값으로 가짐

예를 들어, 이렇게 정의할 수 있어요:

const 예시상태기계: 상태기계<'시작' | '진행중' | '완료', '다음' | '이전'> = {
  시작: [
    { 다음상태: '진행중', 발생이벤트: '다음' }
  ],
  진행중: [
    { 다음상태: '완료', 발생이벤트: '다음' },
    { 다음상태: '시작', 발생이벤트: '이전' }
  ]
};

이러한 기본 구조를 바탕으로 상태 기계의 나머지 부분들을 구현할 수 있어요.


2단계: 상태 기계 빌더 구현

상태 기계를 쉽게 구축할 수 있는 빌더 클래스를 만들어볼게요.

💡

지원서 퍼널은 보통 순서대로 진행되기에 빌더 패턴을 사용하면 상태 전환을 순차적으로 정의할 수 있어 코드의 가독성이 좋아져요.

1) 빌더 클래스의 기본 구조

class 상태기계빌더<상태 extends string, 이벤트 extends string> {
  // 상태 기계의 모든 상태와 전환을 저장하는 객체
  private 상태기계: 상태기계<상태, 이벤트> = {};
}

2) 전환 추가 메서드

전환추가(
  현재상태: 상태,    // 시작 상태
  이벤트: 이벤트,    // 발생하는 이벤트
  다음상태: 상태,    // 도착할 상태
  조건?: (현재상태: 상태, 이벤트: 이벤트) => boolean,  // 선택적 전환 조건
) {
  // 1. 현재 상태에 대한 전환 배열이 없다면 생성
  if (!this.상태기계[현재상태]) {
    this.상태기계[현재상태] = [];
  }
 
  // 2. 새로운 전환 추가
  this.상태기계[현재상태].push({ 
    다음상태, 
    발생이벤트: 이벤트, 
    조건 
  });
 
  // 3. 메서드 체이닝을 위해 this 반환
  return this;
}

3) 상태 기계 생성 메서드

생성(): 상태기계<상태, 이벤트> {
  return this.상태기계;
}

이렇게 구현된 빌더를 사용하면 다음과 같이 상태 기계를 만들 수 있어요:

const 설문상태기계 = new 상태기계빌더()
  .전환추가('시작', '다음', '개인정보')
  .전환추가('개인정보', '다음', '직업정보')
  .전환추가('직업정보', '다음', '완료')
  .생성();

빌더 패턴의 장점:

  1. 순차적 정의: 상태 전환을 순서대로 명확하게 정의할 수 있어요
  2. 가독성: 메서드 체이닝으로 코드가 깔끔해져요
  3. 유연성: 조건부 전환도 쉽게 추가할 수 있어요

3단계: 상태 기계 핸들러 구현

상태 기계의 실제 동작을 처리하는 핸들러를 구현해볼게요. 코드가 조금 복잡해 보일 수 있으니 하나씩 살펴보겠습니다.

1) 클래스의 기본 구조

class 상태기계핸들러<상태 extends string, 이벤트 extends string> {
  private 현재상태: 상태;
  private 상태스택: 상태[] = [];
  
  constructor(
    private 상태기계: 상태기계<상태, 이벤트>, // 상태 기계 인터페이스
    private 초기상태: 상태, // 초기 상태
    private 이전상태이벤트: 이벤트, // 이전 상태로 돌아가기 위한 이벤트 (오동작을 막기 위해 주입받아요)
  ) {
    this.현재상태 = 초기상태;
  }
}

여기서는 상태 기계의 핵심 요소들을 정의해요:

  • 현재상태: 지금 어떤 상태인지 저장
  • 상태스택: 이전 상태들을 순서대로 저장하는 배열 (뒤로 가기를 위해 필요)
  • constructor: 상태 기계를 시작하기 위한 기본 설정을 받아요

2) 상태 전환 메서드

전환(이벤트: 이벤트): 상태 {
  // 1. 이전 상태로 돌아가기 처리
  if (이벤트 === this.이전상태이벤트) {
    if (this.상태스택.length > 0) {
      this.현재상태 = this.상태스택.pop()!;
    }
    return this.현재상태;
  }
 
  // 2. 현재 상태에서 가능한 전환들 찾기
  const 가능한전환 = this.상태기계[this.현재상태];
  if (!가능한전환) return this.현재상태;
 
  // 3. 현재 이벤트에 맞는 전환 찾기
  const 유효한전환 = 가능한전환.find(
    (전환) => 전환.발생이벤트 === 이벤트 && (!전환.조건 || 전환.조건(this.현재상태, 이벤트))
  );
 
  // 4. 상태 전환 실행
  if (유효한전환) {
    this.상태스택.push(this.현재상태);  // 현재 상태를 스택에 저장
    this.현재상태 = 유효한전환.다음상태;  // 새로운 상태로 전환
  }
 
  return this.현재상태;
}

전환 메서드는 상태 기계의 핵심이에요. 단계별로 살펴보면:

  1. 이전 상태로 돌아가기: '이전으로' 이벤트가 발생하면 스택에서 이전 상태를 꺼내 복원
  2. 가능한 전환 확인: 현재 상태에서 할 수 있는 전환들을 찾음
  3. 유효한 전환 찾기: 발생한 이벤트에 맞는 전환을 찾고, 조건이 있다면 확인
  4. 상태 전환: 현재 상태를 스택에 저장하고 새로운 상태로 전환

3) 유틸리티 메서드들

현재상태가져오기(): 상태 {
  return this.현재상태;
}
 
초기화(): void {
  this.현재상태 = this.초기상태;
  this.상태스택 = [];
}
  • 현재상태가져오기: 현재 상태를 확인할 수 있는 메서드
  • 초기화: 모든 상태를 처음으로 되돌리는 메서드

4단계: React Hook 구현

상태 기계를 React와 통합하는 방법을 알아볼게요. 먼저 이전에 사용했던 방식을 보여드릴게요:

이전 사용 방식 (Hook 없이)

function 설문컴포넌트() {
  // 상태 기계 인스턴스 생성
  const [상태기계핸들러] = useState(() => 
    new 상태기계핸들러(설문상태기계, '시작', '이전'));
  
  // 현재 상태 관리
  const [현재상태, 상태설정] = useState(상태기계핸들러.현재상태가져오기());
  
  // 상태 전환 함수
  const 다음으로 = useCallback(() => {
    const 다음상태 = 상태기계핸들러.전환('다음');
    상태설정(다음상태);
  }, [상태기계핸들러]);
 
  const 이전으로 = useCallback(() => {
    const 이전상태 = 상태기계핸들러.전환('이전');
    상태설정(이전상태);
  }, [상태기계핸들러]);
 
  return (
    <div>
      <h2>현재 단계: {현재상태}</h2>
      <button onClick={다음으로}>다음</button>
      <button onClick={이전으로}>이전</button>
    </div>
  );
}

이 방식의 문제점:

  1. 매번 비슷한 보일러플레이트 코드를 작성해야 함
  2. 상태 관리 로직이 컴포넌트마다 중복됨
  3. 실수로 상태 동기화가 깨질 수 있음

개선된 방식 (Custom Hook 사용)

function useStateMachine<상태 extends string, 이벤트 extends string>(
  상태기계: 상태기계<상태, 이벤트>,
  초기상태: 상태,
  이전상태이벤트: 이벤트,
) {
  // 상태 기계 핸들러 생성 (메모이제이션)
  const 핸들러 = useMemo(
    () => new 상태기계핸들러(상태기계, 초기상태, 이전상태이벤트),
    [상태기계, 초기상태, 이전상태이벤트],
  );
 
  // 현재 상태 관리
  const [상태, 상태설정] = useState(핸들러.현재상태가져오기());
 
  // 상태 전환 함수
  const 전환 = useCallback((이벤트: 이벤트) => {
    const 다음상태 = 핸들러.전환(이벤트);
    상태설정(다음상태);
    return 다음상태;
  }, [핸들러]);
 
  // 유용한 메서드들을 객체로 반환
  return {
    상태,
    전환,
    이전으로: () => 전환(이전상태이벤트),
    초기화: () => 핸들러.초기화(),
  };
}
 
// Hook을 사용한 컴포넌트 예시
function 설문컴포넌트() {
  const { 상태, 전환, 이전으로 } = useStateMachine(
    설문상태기계,
    '시작',
    '이전',
  );
 
  return (
    <div>
      <h2>현재 단계: {상태}</h2>
      <button onClick={() => 전환('다음')}>다음</button>
      <button onClick={이전으로}>이전</button>
    </div>
  );
}

Hook을 사용했을 때의 장점:

  1. 코드 재사용: 상태 관리 로직을 재사용할 수 있어요
  2. 간결한 컴포넌트: 복잡한 상태 관리 로직이 Hook으로 추상화되어 컴포넌트가 깔끔해져요
  3. 안전한 상태 관리: 상태 동기화가 Hook 내부에서 자동으로 처리돼요
  4. 편리한 API: 필요한 기능들이 잘 정리된 형태로 제공돼요

5단계: 실제 사용 예시

간단한 설문 시스템을 예로 들어볼게요:

// 가능한 모든 상태 정의
const 설문상태 = {
  시작: '시작',
  개인정보: '개인정보',
  직업정보: '직업정보',
  개발자설문: '개발자설문',  // 개발자일 경우만 보이는 화면
  일반설문: '일반설문',      // 비개발자일 경우 보이는 화면
  관심분야: '관심분야',
  완료: '완료',
} as const;
 
// 발생 가능한 이벤트 정의
const 설문이벤트 = {
  다음: '다음',
  이전: '이전',
  개발자: '개발자',          // 직업이 개발자인 경우
  비개발자: '비개발자',      // 직업이 개발자가 아닌 경우
} as const;
 
// 상태 기계 정의
const 설문상태기계 = new 상태기계빌더()
  // 시작 → 개인정보
  .전환추가(설문상태.시작, 설문이벤트.다음, 설문상태.개인정보)
  
  // 개인정보 → 직업정보
  .전환추가(설문상태.개인정보, 설문이벤트.다음, 설문상태.직업정보)
  
  // 직업정보에서 분기 처리
  .전환추가(설문상태.직업정보, 설문이벤트.개발자, 설문상태.개발자설문)
  .전환추가(설문상태.직업정보, 설문이벤트.비개발자, 설문상태.일반설문)
  
  // 각 설문에서 관심분야로
  .전환추가(설문상태.개발자설문, 설문이벤트.다음, 설문상태.관심분야)
  .전환추가(설문상태.일반설문, 설문이벤트.다음, 설문상태.관심분야)
  
  // 관심분야 → 완료
  .전환추가(설문상태.관심분야, 설문이벤트.다음, 설문상태.완료)
  .생성();
 
// React 컴포넌트에서 사용
function 설문() {
  const { 상태, 전환, 이전으로 } = useStateMachine(
    설문상태기계,
    설문상태.시작,
    설문이벤트.이전,
  );
 
  return (
    <div>
      <button onClick={이전으로}>이전</button>
      <SwitchCase
        case={상태}
        caseBy={{
          // 시작 화면
          [설문상태.시작]: (
            <개인정보입력 onNext={() => 전환(설문이벤트.다음)} />
          ),
          
          // 개인정보 입력 화면
          [설문상태.개인정보]: (
            <직업정보입력
              onSelect={(직업: string) => {
                if (직업 === '개발자') {
                  전환(설문이벤트.개발자);
                } else {
                  전환(설문이벤트.비개발자);
                }
              }}
            />;
          )
          
          // 직업에 따른 분기 화면
          [설문상태.개발자설문]: (
            <개발자추가설문 onNext={() => 전환(설문이벤트.다음)} />
          ),
          [설문상태.일반설문]: (
            <일반추가설문 onNext={() => 전환(설문이벤트.다음)} />
          ),
          
          // 관심분야 및 완료 화면
          [설문상태.관심분야]: (
            <관심분야입력 onNext={() => 전환(설문이벤트.다음)} />
          ),
          [설문상태.완료]: <완료화면 />,
        }}
      />
    </div>
  );
}

이 예시에서는:

  1. 직업 선택에 따라 다른 설문 경로로 분기
  2. 각 상태별로 적절한 컴포넌트 렌더링
  3. 이전/다음 네비게이션 지원
  4. 상태에 따른 조건부 렌더링을 SwitchCase 컴포넌트로 깔끔하게 처리

이렇게 구현하면 복잡한 설문 흐름도 명확하게 관리할 수 있어요.


6단계: 상태 유지와 복구 구현하기

5단계에서 만든 설문 시스템에 상태 유지와 복구 기능을 추가해볼게요:

// 이전 단계의 상태와 이벤트 정의 사용
// ... 기존 설문상태, 설문이벤트 정의 ...
 
function use설문상태기계({
  상태기계,
  저장된답변,
}: {
  상태기계: 상태기계<설문상태, 설문이벤트>;
  저장된답변?: {
    개인정보?: { 이름: string; 이메일: string };
    직업정보?: { 직무: string };
    // ... 다른 답변 타입 정의 ...
  };
}) {
  const { 상태, 전환, 이전으로 } = useStateMachine(
    상태기계,
    설문상태.시작,
    설문이벤트.이전,
  );
 
  // 저장된 답변 기반으로 상태 복구
  const 상태복구 = useCallback(() => {
    if (!저장된답변) return;
 
    // 개인정보 복구
    if (저장된답변.개인정보?.이름) {
      전환(설문이벤트.다음);  // 시작 → 개인정보 → 직업정보
    }
 
    // 직업정보 복구 및 분기 처리
    if (저장된답변.직업정보?.직무) {
      if (저장된답변.직업정보.직무 === '개발자') {
        전환(설문이벤트.개발자);
      } else {
        전환(설문이벤트.비개발자);
      }
    }
    // ... 나머지 상태 복구 로직 ...
  }, [저장된답변, 전환]);
 
  useEffect(() => {
    상태복구();
  }, [상태복구]);
 
  return { 상태, 전환, 이전으로 };
}
 
// 실제 사용 예시
function 설문() {
  const { 상태, 전환, 이전으로 } = use설문상태기계({
    상태기계: 설문상태기계,
    저장된답변: {
      개인정보: { 이름: '김개발', 이메일: 'dev@email.com' },
      직업정보: { 직무: '개발자' },
      // ... 저장된 다른 답변들 ...
    },
  });
 
  return (
    <div>
      <button onClick={이전으로}>이전</button>
      <SwitchCase
        case={상태}
        caseBy={{
          [설문상태.시작]: (
            <개인정보입력 
              초기값={저장된답변?.개인정보}
              onNext={() => 전환(설문이벤트.다음)} 
            />
          ),
          [설문상태.직업정보]: (
            <직업정보입력
              초기값={저장된답변?.직업정보}
              onSelect={(직업) => {
                if (직업 === '개발자') {
                  전환(설문이벤트.개발자);
                } else {
                  전환(설문이벤트.비개발자);
                }
              }}
            />
          ),
          // ... 다른 상태에 대한 컴포넌트 ...
        }}
      />
    </div>
  );
}

이렇게 구현하면:

  1. 저장된 답변을 기반으로 적절한 상태로 자동 복구
  2. 각 입력 컴포넌트에 초기값 제공
  3. 기존 상태 기계의 장점을 유지하면서 영속성 추가

이렇게 구현한 상태 기계는 멀티 스텝 폼과 같이 복잡한 폼이나 다단계 프로세스를 관리하는 데 유용해요. 특히 상태 전환이 명확하고, 이전/다음 단계 이동이 자유로우며, 각 상태에 따른 UI 렌더링도 쉽게 구현할 수 있어요.


결과

실제로 사용한 화면은 다음과 같아요.

이런 형태의 다단계 입력 폼을 어떻게 부르시나요?
저희 팀에서는 동료분의 제안으로 'Multi-step Form'이라는 이름을 사용하고 있어요.


마치며

유한 상태 기계를 활용하면서 몇 가지 중요한 인사이트를 얻을 수 있었어요.

1. 명시적인 상태 관리의 중요성

복잡한 폼이나 다단계 프로세스를 관리할 때, 상태와 전환을 명시적으로 정의하면 코드의 예측 가능성이 크게 높아져요. 특히 상태 기계를 통해 가능한 모든 상태 전환을 한눈에 파악할 수 있다는 점이 큰 장점이었어요.

2. 조건부 전환과 명시적 이벤트의 트레이드오프

상태 전환을 구현하는 두 가지 방식에 대해 깊이 고민해보게 됐어요.

조건부 전환 방식

// 조건부 전환 사용
.전환추가('설문', '제출', '완료', (상태) => 검증.모든항목작성완료())

장점

  • 사용처에서는 단순히 '제출' 이벤트만 발생시키면 됨
  • 전환 조건의 복잡한 로직을 상태 기계 내부로 캡슐화
  • 사용처의 코드가 더 단순해지고 관심사가 분리됨

단점

  • 상태 전환의 흐름이 코드만 봐서는 명확하지 않음
  • 디버깅이 어려울 수 있음
  • 조건 로직이 상태 기계 내부에 숨어있어 전체 흐름 파악이 어려움

명시적 이벤트 방식

// 명시적 이벤트 사용
.전환추가('설문', '모든항목작성완료', '완료')
.전환추가('설문', '일부항목미작성', '설문')

장점

  • 가능한 모든 상태 전환이 코드에 명시적으로 드러남
  • 상태 기계의 흐름을 이해하기 쉬움
  • 타입 시스템을 통한 안전성 확보가 용이

단점

  • 사용처에서 상태를 판단하는 로직을 직접 관리해야 함
  • 이벤트 발생 전에 상태 체크 로직이 필요
  • 관련 상태와 로직이 사용처에 흩어질 수 있음

결론

두 방식 모두 각자의 장단점이 있어 상황에 따라 적절한 선택이 필요해요:

  • 조건부 전환 선호 상황

    • 전환 조건이 복잡하고 재사용 가능한 경우
    • 사용처의 단순성이 더 중요한 경우
    • 상태 전환 로직을 중앙화하고 싶은 경우
  • 명시적 이벤트 선호 상황

    • 상태 기계의 흐름을 명확하게 문서화하고 싶은 경우
    • 타입 안정성이 매우 중요한 경우
    • 디버깅 용이성이 중요한 경우

결국 프로젝트의 특성과 팀의 선호도에 따라 적절한 방식을 선택하는 것이 좋을 것 같아요.

3. 도입 시 고려사항

상태 기계 패턴은 강력한 도구지만, 도입을 고민하면서 몇 가지 현실적인 고려사항이 있었어요.

😓

XState 같은 라이브러리를 사용할까 고민하다가 간단하게 구현해보니 라이브러리를 사용하는 것보다 더 간단하고 빠르게 구현할 수 있었어요.
라이브러리를 사용하면 더 많은 기능을 사용할 수 있을 것 같아요.

학습 곡선과 팀 적응

처음에는 팀원들의 우려가 있었어요:

  • "새로운 개념을 배워야 하는 부담"
  • "기존 방식으로도 충분하지 않을까?"
  • "코드가 더 복잡해지는 것은 아닐까?"

하지만 실제 도입 후에는:

  • 상태 관리 로직이 한 곳에 모여 유지보수가 쉬워짐
  • 가능한 상태 전환이 명시적이라 버그 발생이 줄어듦
  • 새로운 요구사항 추가가 용이해짐

적용하기 좋은 상황

  • 복잡한 다단계 폼
  • 명확한 상태 전환이 필요한 기능
  • 양방향 이동이 필요한 UI

적용하기 어려운 상황

  • 단순한 한 단계 폼
  • 자유로운 상태 전환이 필요한 경우
  • 빠른 프로토타이핑이 필요한 상황

4. 앞으로의 발전 방향

  • 타입 안정성 강화: 상태와 이벤트의 타입 추론을 조금 더 strict 하게 강화할 수 있을 것 같아요
  • 시각화 도구 개발: 상태 기계의 흐름을 시각적으로 보여주는 도구가 있으면 좋을 것 같아요
  • 테스트 용이성: 상태 전환에 대한 테스트 케이스 작성이 더 쉬워질 수 있을 것 같아요
  • 문서화: 주변 동료들이 쉽게 이해하고 사용할 수 있도록 문서화를 강화해야 할 것 같아요

유한 상태 기계는 복잡한 상태 관리를 단순화하고 예측 가능하게 만드는 강력한 도구예요. 특히 다단계 폼이나 복잡한 사용자 플로우를 다룰 때 큰 힘을 발휘한답니다.