이 글은 2024년 5월, 맡고 있던 프로젝트에서 퍼널 개편이 진행되었을 때 개선한 내용을 공유하는 글이에요.
우선, 유한 상태 기계가 뭘까요?
유한 상태 기계란 것을 처음으로 알게된 계기는 약 2년 전, chakra-ui
에서 운영 중인 ark 라이브러리였어요. 해당 라이브러리는 Framework Agnostic
하게 제작된 zag 라이브러리를 사용하고 있었고, 이 zag
라이브러리 내부에서 상태머신을 사용하고 있었어요.
토스의 임지훈 님께서 Framework Agnostic에 대한 좋은 글을 작성해주셨어요.
자바스크립트에서 XState
라는 유한 상태 기계 라이브러리가 있는데, zag
는 해당 라이브러리를 그대로 사용하지 않고 최소 기능만 구현하여 사용하고 있어요.
XState
를 언급한 이유는 유한 상태 기계를 가장 잘 구현한 라이브러리라고 생각하기 때문이에요.
XState? 그건 뭔데?
XState
는 상태머신을 구현하기 위한 라이브러리로, 상태머신은 상태와 상태 전환을 정의하고 이를 통해 복잡한 상태 관리를 할 수 있게 도와줘요. 특히 시각화를 훌륭하게 지원하는데요, 복잡한 로직의 경우 시각화를 통해 디버깅을 쉽게 할 수 있어요.
다시 돌아와서 유한 상태 기계에 대해 정리해볼게요.
유한 상태 기계란 다음과 같은 특징을 가진 프로그래밍 모델이에요:
- 유한한 상태(Finite States): 시스템이 가질 수 있는 모든 상태가 미리 정의되어 있어요.
- 상태 전환(Transitions): 한 상태에서 다른 상태로 전환되는 규칙이 명확하게 정의되어 있어요.
- 이벤트 기반(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번 문항에서 A 선택 시 → 2번으로 이동
- 예: 1번 문항에서 B 선택 시 → 3번으로 이동
- 이전 답변들의 조합에 따라 다음 문항이 결정되는 경우도 존재
- 예: 5번 문항은 3-1번과 4번 문항의 답변 조합에 따라 다른 문항으로 분기
추가 요구사항
- 양방향 이동: 퍼널 진행 중 이전 항목으로 돌아갈 수 있어야 함
- 상태 유지:
- 모든 답변은 서버에 실시간 저장
- 재접속 시 이전 진행 상태 복구 및 이전 답변 유지, 양방향 이동 가능
- 데이터 정합성: 이전 단계로 돌아가서 답변을 수정할 경우, 연관된 다음 단계들의 데이터 처리
이러한 복잡한 요구사항들을 기존의 방식으로 관리하려고 하니 다음과 같은 문제점들이 있었어요:
- 상태 관리 로직이 복잡해짐
- 예외 케이스 처리가 어려움
- 코드 유지보수가 힘들어짐
이러한 문제들을 해결하기 위해 유한 상태 기계 패턴을 도입하게 되었어요.
상태 기계 직접 구현하기
실제로 상태 기계를 구현하면서 어떻게 활용할 수 있는지 알아볼게요.
이해를 돕기 위해 예시 코드는 한글로 작성했어요.
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 상태기계빌더()
.전환추가('시작', '다음', '개인정보')
.전환추가('개인정보', '다음', '직업정보')
.전환추가('직업정보', '다음', '완료')
.생성();
빌더 패턴의 장점:
- 순차적 정의: 상태 전환을 순서대로 명확하게 정의할 수 있어요
- 가독성: 메서드 체이닝으로 코드가 깔끔해져요
- 유연성: 조건부 전환도 쉽게 추가할 수 있어요
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.현재상태;
}
전환 메서드는 상태 기계의 핵심이에요. 단계별로 살펴보면:
- 이전 상태로 돌아가기: '이전으로' 이벤트가 발생하면 스택에서 이전 상태를 꺼내 복원
- 가능한 전환 확인: 현재 상태에서 할 수 있는 전환들을 찾음
- 유효한 전환 찾기: 발생한 이벤트에 맞는 전환을 찾고, 조건이 있다면 확인
- 상태 전환: 현재 상태를 스택에 저장하고 새로운 상태로 전환
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>
);
}
이 방식의 문제점:
- 매번 비슷한 보일러플레이트 코드를 작성해야 함
- 상태 관리 로직이 컴포넌트마다 중복됨
- 실수로 상태 동기화가 깨질 수 있음
개선된 방식 (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을 사용했을 때의 장점:
- 코드 재사용: 상태 관리 로직을 재사용할 수 있어요
- 간결한 컴포넌트: 복잡한 상태 관리 로직이 Hook으로 추상화되어 컴포넌트가 깔끔해져요
- 안전한 상태 관리: 상태 동기화가 Hook 내부에서 자동으로 처리돼요
- 편리한 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>
);
}
이 예시에서는:
- 직업 선택에 따라 다른 설문 경로로 분기
- 각 상태별로 적절한 컴포넌트 렌더링
- 이전/다음 네비게이션 지원
- 상태에 따른 조건부 렌더링을
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>
);
}
이렇게 구현하면:
- 저장된 답변을 기반으로 적절한 상태로 자동 복구
- 각 입력 컴포넌트에 초기값 제공
- 기존 상태 기계의 장점을 유지하면서 영속성 추가
이렇게 구현한 상태 기계는 멀티 스텝 폼
과 같이 복잡한 폼이나 다단계 프로세스를 관리하는 데 유용해요. 특히 상태 전환이 명확하고, 이전/다음 단계 이동이 자유로우며, 각 상태에 따른 UI 렌더링도 쉽게 구현할 수 있어요.
결과
실제로 사용한 화면은 다음과 같아요.
이런 형태의 다단계 입력 폼을 어떻게 부르시나요?
저희 팀에서는 동료분의 제안으로 'Multi-step Form'
이라는 이름을 사용하고 있어요.
마치며
유한 상태 기계를 활용하면서 몇 가지 중요한 인사이트를 얻을 수 있었어요.
1. 명시적인 상태 관리의 중요성
복잡한 폼이나 다단계 프로세스를 관리할 때, 상태와 전환을 명시적으로 정의하면 코드의 예측 가능성이 크게 높아져요. 특히 상태 기계를 통해 가능한 모든 상태 전환을 한눈에 파악할 수 있다는 점이 큰 장점이었어요.
2. 조건부 전환과 명시적 이벤트의 트레이드오프
상태 전환을 구현하는 두 가지 방식에 대해 깊이 고민해보게 됐어요.
조건부 전환 방식
// 조건부 전환 사용
.전환추가('설문', '제출', '완료', (상태) => 검증.모든항목작성완료())
장점
- 사용처에서는 단순히 '제출' 이벤트만 발생시키면 됨
- 전환 조건의 복잡한 로직을 상태 기계 내부로 캡슐화
- 사용처의 코드가 더 단순해지고 관심사가 분리됨
단점
- 상태 전환의 흐름이 코드만 봐서는 명확하지 않음
- 디버깅이 어려울 수 있음
- 조건 로직이 상태 기계 내부에 숨어있어 전체 흐름 파악이 어려움
명시적 이벤트 방식
// 명시적 이벤트 사용
.전환추가('설문', '모든항목작성완료', '완료')
.전환추가('설문', '일부항목미작성', '설문')
장점
- 가능한 모든 상태 전환이 코드에 명시적으로 드러남
- 상태 기계의 흐름을 이해하기 쉬움
- 타입 시스템을 통한 안전성 확보가 용이
단점
- 사용처에서 상태를 판단하는 로직을 직접 관리해야 함
- 이벤트 발생 전에 상태 체크 로직이 필요
- 관련 상태와 로직이 사용처에 흩어질 수 있음
결론
두 방식 모두 각자의 장단점이 있어 상황에 따라 적절한 선택이 필요해요:
-
조건부 전환 선호 상황
- 전환 조건이 복잡하고 재사용 가능한 경우
- 사용처의 단순성이 더 중요한 경우
- 상태 전환 로직을 중앙화하고 싶은 경우
-
명시적 이벤트 선호 상황
- 상태 기계의 흐름을 명확하게 문서화하고 싶은 경우
- 타입 안정성이 매우 중요한 경우
- 디버깅 용이성이 중요한 경우
결국 프로젝트의 특성과 팀의 선호도에 따라 적절한 방식을 선택하는 것이 좋을 것 같아요.
3. 도입 시 고려사항
상태 기계 패턴은 강력한 도구지만, 도입을 고민하면서 몇 가지 현실적인 고려사항이 있었어요.
XState 같은 라이브러리를 사용할까 고민하다가 간단하게 구현해보니 라이브러리를 사용하는 것보다 더 간단하고 빠르게 구현할 수 있었어요.
라이브러리를 사용하면 더 많은 기능을 사용할 수 있을 것 같아요.
학습 곡선과 팀 적응
처음에는 팀원들의 우려가 있었어요:
- "새로운 개념을 배워야 하는 부담"
- "기존 방식으로도 충분하지 않을까?"
- "코드가 더 복잡해지는 것은 아닐까?"
하지만 실제 도입 후에는:
- 상태 관리 로직이 한 곳에 모여 유지보수가 쉬워짐
- 가능한 상태 전환이 명시적이라 버그 발생이 줄어듦
- 새로운 요구사항 추가가 용이해짐
적용하기 좋은 상황
- 복잡한 다단계 폼
- 명확한 상태 전환이 필요한 기능
- 양방향 이동이 필요한 UI
적용하기 어려운 상황
- 단순한 한 단계 폼
- 자유로운 상태 전환이 필요한 경우
- 빠른 프로토타이핑이 필요한 상황
4. 앞으로의 발전 방향
- 타입 안정성 강화: 상태와 이벤트의 타입 추론을 조금 더 strict 하게 강화할 수 있을 것 같아요
- 시각화 도구 개발: 상태 기계의 흐름을 시각적으로 보여주는 도구가 있으면 좋을 것 같아요
- 테스트 용이성: 상태 전환에 대한 테스트 케이스 작성이 더 쉬워질 수 있을 것 같아요
- 문서화: 주변 동료들이 쉽게 이해하고 사용할 수 있도록 문서화를 강화해야 할 것 같아요
유한 상태 기계는 복잡한 상태 관리를 단순화하고 예측 가능하게 만드는 강력한 도구예요. 특히 다단계 폼이나 복잡한 사용자 플로우를 다룰 때 큰 힘을 발휘한답니다.