React 및 Vue에서 XState를 사용한 복잡한 컴포넌트 상태 마스터링
Takashi Yamamoto
Infrastructure Engineer · Leapcell

소개: UI 상태의 야수를 길들이기
현대의 웹 애플리케이션은 점점 더 복잡해지고 있으며, 이러한 복잡성의 상당 부분은 종종 개별 UI 컴포넌트의 상태 관리에 있습니다. 컴포넌트가 기능과 상호 작용이 증가함에 따라 내부 상태는 빠르게 불리언, 열거형 및 조건부 논리의 얽힌 망이 될 수 있습니다. 이 스파게티 코드는 디버깅을 어렵게 하고, 미묘한 버그를 도입하며, 협업을 악몽으로 만들어요. 우리는 끊임없이 예상치 못한 부작용, 불가능한 상태, 그리고 컴포넌트가 어떻게 작동하는지에 대한 명확성의 부족과 싸우고 있습니다. 바로 여기서 XState와 같은 상태 머신이 강력하고 우아한 솔루션을 제공합니다.
컴포넌트 동작을 모델링하는 공식적이고 예측 가능한 방법을 제공함으로써 XState는 상태 관리를 위험한 여정에서 잘 정의된 경로로 전환하여 더 강력하고 이해하기 쉬우며 유지보수 가능한 React 및 Vue 애플리케이션을 구축할 수 있게 합니다.
핵심 개념: 상태 머신의 언어 이해
실제 적용으로 들어가기 전에 XState의 기반을 형성하는 상태 머신 뒤에 있는 기본 개념을 파악하는 것이 중요합니다.
상태
상태는 컴포넌트 수명 주기에서 뚜렷한 순간 또는 조건을 나타냅니다. 예를 들어, 버튼은 idle
, loading
또는 success
상태일 수 있습니다. 중요한 것은 상태 머신은 특정 시간에 단 하나의 상태만 있을 수 있다는 것입니다. 이러한 배타성은 불가능한 조합을 방지하는 데 중요합니다.
이벤트
이벤트는 한 상태에서 다른 상태로의 전환을 유발하는 트리거입니다. 이벤트는 일반적으로 사용자 상호 작용(예: CLICK
, SUBMIT
), 데이터 가져오기 결과(예: FETCH_SUCCESS
, FETCH_ERROR
) 또는 시스템 생성 신호(예: TIMER_EXPIRED
)입니다.
전환
전환은 특정 이벤트에 의해 트리거되어 한 상태에서 다른 상태로 이동하는 것입니다. 전환은 기계가 특정 상태에 있을 때 특정 이벤트가 발생할 때 발생하는 일을 정의합니다. 예를 들어, idle
상태에 있을 때 CLICK
이벤트는 loading
상태로의 전환을 유발할 수 있습니다.
액션
액션은 전환 중 또는 상태에 들어가거나 나갈 때 발생하는 부작용입니다. API 호출, 로컬 스토리지 업데이트 또는 Redux 액션 디스패칭과 같은 작업을 수행하는 곳입니다. 액션은 전환과 구분됩니다. 전환은 어디로 가는지 정의하고, 액션은 해당 여정 중에 무엇을 하는지 정의합니다.
컨텍스트
컨텍스트(확장 상태라고도 함)는 시간이 지남에 따라 변경되지만 시스템의 근본적인 "상태"를 정의하지 않는 가변 데이터를 저장하는 곳입니다. 예를 들어, 양식에서 currentInput
값이나 errorMessage
는 일반적으로 컨텍스트에 있지만, 양식의 editing
또는 submitting
상태는 별도의 상태입니다.
XState: 원칙, 구현 및 실례
XState는 상태 머신 및 상태 차트를 정의, 해석 및 실행할 수 있는 라이브러리입니다. 프론트엔드 개발에 공식 시스템 모델링의 견고함과 예측 가능성을 가져옵니다.
공식 모델링의 힘
XState의 핵심 원칙은 컴포넌트 동작의 공식 모델링입니다. 가능한 모든 상태, 전환을 트리거하는 이벤트 및 발생하는 액션을 명시적으로 정의함으로써 모호성을 제거합니다. 이 선언적 접근 방식은 컴포넌트의 논리를 본질적으로 테스트 가능하고 이해하기 쉽게 만듭니다.
상태 머신 정의
간단한 시나리오를 생각해 보겠습니다. 데이터를 가져오는 버튼입니다. idle
, loading
, success
또는 error
상태일 수 있습니다.
// React 또는 Vue 컴포넌트 파일에서 import { createMachine, assign } from 'xstate'; const fetchMachine = createMachine({ id: 'fetch', initial: 'idle', context: { data: null, error: null, }, states: { idle: { on: { FETCH: 'loading', }, }, loading: { invoke: { id: 'fetchData', src: async (context, event) => { // API 호출 시뮬레이션 await new Promise(resolve => setTimeout(resolve, 1000)); if (Math.random() > 0.5) { return { data: 'Some fetched data!' }; } else { throw new Error('Failed to fetch data.'); } }, onDone: { target: 'success', actions: assign({ data: (context, event) => event.data.data, // event.data는 'src' promise의 결과를 포함합니다 error: null, }), }, onError: { target: 'error', actions: assign({ error: (context, event) => event.data.message, // event.data는 'src' promise의 오류를 포함합니다 data: null, }), }, }, }, success: { on: { DISMISS: 'idle', }, }, error: { on: { RETRY: 'loading', DISMISS: 'idle', }, }, }, });
여기서 우리는 다음을 정의합니다.
id
: 기계의 고유 식별자.initial
: 시작 상태.context
: 컴포넌트의 초기 데이터(예:data
및error
).states
: 가능한 모든 상태를 정의하는 객체.on
: 해당 상태의 이벤트에 의해 트리거되는 전환을 정의합니다.invoke
: 상태 수명 주기의 일부로 비동기 작업(API 호출과 같은)을 수행할 수 있게 합니다.onDone
및onError
는 결과를 처리합니다.actions
:assign
을 사용하여context
를 수정하는 함수.
React 통합 예제
@xstate/react
훅을 사용하여 React 컴포넌트에서 이를 사용하는 방법을 살펴 보겠습니다.
// MyFetcherButton.jsx (React) import React from 'react'; import { useMachine } from '@xstate/react'; import { createMachine, assign } from 'xstate'; // (위의 fetchMachine 정의가 여기에 들어갑니다) function MyFetcherButton() { const [current, send] = useMachine(fetchMachine); return ( <div> <p>Status: {current.value}</p> {current.matches('idle') && ( <button onClick={() => send('FETCH')}>Fetch Data</button> )} {current.matches('loading') && <p>Loading...</p>} {current.matches('success') && ( <> <p>Data: {current.context.data}</p> <button onClick={() => send('DISMISS')}>Dismiss</button> </> )} {current.matches('error') && ( <> <p style={{ color: 'red' }}>Error: {current.context.error}</p> <button onClick={() => send('RETRY')}>Retry</button> <button onClick={() => send('DISMISS')}>Dismiss</button> </> )} </div> ); } export default MyFetcherButton;
useMachine
은 current
(현재 상태 및 컨텍스트)과 send
(이벤트를 디스패치하는 함수)를 반환합니다. current.matches()
를 사용하여 활성 상태를 기반으로 UI를 조건부로 렌더링합니다.
Vue 통합 예제
Vue의 경우 @xstate/vue
패키지를 사용합니다.
<!-- MyFetcherButton.vue (Vue 3) --> <template> <div> <p>Status: {{ state.value }}</p> <button v-if="state.matches('idle')" @click="send('FETCH')">Fetch Data</button> <p v-if="state.matches('loading')">Loading...</p> <div v-if="state.matches('success')"> <p>Data: {{ state.context.data }}</p> <button @click="send('DISMISS')">Dismiss</button> </div> <div v-if="state.matches('error')"> <p style="color: red;">Error: {{ state.context.error }}</p> <button @click="send('RETRY')">Retry</button> <button @click="send('DISMISS')">Dismiss</button> </div> </div> </template> <script setup> import { useMachine } from '@xstate/vue'; import { createMachine, assign } from 'xstate'; // (위의 fetchMachine 정의가 여기에 들어갑니다) const { state, send } = useMachine(fetchMachine); </script>
Vue의 useMachine
과 유사하게 state
(반응형 현재 상태 및 컨텍스트)와 send
를 제공합니다.
고급 시나리오: 상태 차트 및 계층적 상태
진정으로 복잡한 컴포넌트의 경우 XState는 계층적 및 병렬 상태를 포함하여 상태 차트를 지원함으로써 탁월합니다.
VideoPlayer
컴포넌트를 생각해 봅시다. playing
또는 paused
상태이지만 playing
중에 buffering
상태이거나 paused
중에 seeking
상태일 수 있습니다.
const videoPlayerMachine = createMachine({ id: 'videoPlayer', initial: 'idle', states: { idle: { on: { PLAY: 'playing' } }, playing: { initial: 'playingVideo', states: { playingVideo: { on: { PAUSE: 'paused', BUFFER: 'buffering' } }, buffering: { on: { BUFFER_COMPLETE: 'playingVideo', PAUSE: 'paused' } } }, on: { STOP: 'idle' } // 상위에서 처리되는 이벤트 }, paused: { on: { PLAY: 'playing', SEEK: 'seeking' } }, seeking: { // ... 탐색 상태 on: { SEEK_COMPLETE: 'paused', PLAY: 'playing' // 탐색 후 재생 시작 가능 } } } });
여기서 playing
은 중첩된 하위 상태(playingVideo
, buffering
)가 있는 상위 상태입니다. 이를 통해 관련 동작을 그룹화하고 복잡성을 관리할 수 있습니다. 이벤트는 계층 구조의 모든 수준에서 처리될 수 있으며, 이는 버블링 메커니즘을 따릅니다.
애플리케이션 시나리오
XState는 다음과 같은 시나리오에서 특히 유용합니다.
- 복잡한 유효성 검사 및 제출 흐름이 있는 양식:
editing
,validating
,submitting
,submitted
,error
상태 추적. - 마법사 또는 다단계 프로세스: 단계 간 흐름, 조건부 탐색 관리.
- 미디어 플레이어 또는 대화형 UI 요소: 복잡한 상호 작용으로
playing
,paused
,buffering
,seeking
,error
상태 처리. - 드래그 앤 드롭 인터페이스:
idle
,dragging
,hovering
,dropping
상태 추적. - 상태 논리가 많은 수의 조건부 분기(if/else, switch 문)와 가능한 불가능한 상태를 초래하는 모든 컴포넌트.
결론: UI 상태의 패러다임 전환
복잡한 React 및 Vue 컴포넌트에서 상태를 관리하는 것은 더 이상 끊임없는 두통의 원인이 될 필요가 없습니다. 상태 머신을 수용하고 XState와 같은 강력한 라이브러리를 활용함으로써 UI 로직에 명확성, 예측 가능성 및 유지보수성을 가져올 수 있습니다. XState는 컴포넌트 동작을 명시적으로 정의하고, 불가능한 상태를 방지하며, 애플리케이션을 훨씬 쉽게 이해하고, 디버깅하고, 확장할 수 있도록 하는 강력한 프레임워크를 제공합니다. 개발자가 상태를 분산된 변수의 모음이 아니라 결정론적 시스템으로 모델링할 수 있게 해주는 패러다임 전환이며, 궁극적으로 더 안정적이고 즐거운 사용자 경험으로 이어집니다.