React에서 XState 간단 사용 후기 😊
XState는 점점 더 단순화되어가는 상태관리 라이브러리들 중에서 좀 더 명확하게 역할을 나누고 책임을 부여한 컨셉의 상태관리 라이브러리이다.
❗ 이런 특징이 redux의 구조와 비슷하게 느껴지기도 한데, 이에 관해서는 직접 제작자가 밝힌 차이점이 있다.
What is an actual difference between redux and a state machine (e.g. xstate)?
I am working on investigation of one front-end application of medium complexity. At this moment it is written in pure javascript, it has a lot of different event-based messages connecting few main ...
stackoverflow.com
위의 글과 공식문서도 참조하고, 간단하게 예제를 사용하여 XState를 사용해본 뒤 XState는 어떤 장점으로 선택할지 고민해봤다 🙌
XState 사용 예제 (모달 상태 관리) 🙏
실전적인 예제를 작성하기 위해서, 모달을 만들 때 XState를 활용해보았다.
(🍔 예제에 사용된 버전은 "xstate": "^5.19.2, "@xstate/react": "^5.0.2" 입니다)
npm install xstate @xstate/react
yarn add xstate @xstate/react
react에서 활용하기 위해서 hook을 제공해주는 @xstate/react도 함께 설치한다.
그리고 타입스크립트를 사용한다면, 다음과 같이 tsconfig.json 설정을 해주자.
(xstate의 권장사항이다)
{
compilerOptions: {
strictNullChecks: true,
skipLibCheck: true,
},
}
타입스크립트 여부에 따라서 코드 작성도 좀 달라지는데, typescript를 사용하지 않는다면 createMachine으로 충분하지만, typescript 사용시 setup를 사용해 createMachine을 하는 것을 추천하고 있다.
(createMachine 에서도 타입을 작성할 수 있긴 하다 😅)
import { createMachine } from 'xstate';
// js
export const modalMachine = createMachine({
id: 'modal',
initial: 'close',
states: {
close: {
on: { OPEN: 'opene' },
},
open: {
on: { CLOSE: 'close' },
},
},
});
js에서는 이렇게 작성해도 괜찮지만, ts에서는 아래처럼 작성하는 것이 권장사항이다.
import { assign, setup } from 'xstate';
export const modalMachine = setup({
types: {
context: {} as { isOpen: boolean },
events: {} as { type: 'close' } | { type: 'open' },
},
actions: {
setOpen: assign({ isOpen: (_ctx, _event) => true }),
setClose: assign({ isOpen: (_ctx, _event) => false }),
},
}).createMachine({
context: { isOpen: false },
initial: 'closed',
states: {
closed: {
on: { open: { target: 'open', actions: 'setOpen' } },
},
open: {
on: { close: { target: 'closed', actions: 'setClose' } },
},
},
});
위와 같이 코드를 작성하고, 실제 해당 상태를 사용하는 페이지를 아주 간단하게 만들면 다음과 같이 모달 컴포넌트 상태를 작성할 수 있다.
'use client';
import { useMachine } from '@xstate/react';
import { modalMachine } from './store';
const ModalXstate = () => {
const [state, send] = useMachine(modalMachine);
return (
<div>
<button
onClick={() =>
send({
type: 'open',
})
}
>
모달 열기
</button>
{state.context.isOpen && (
<div className="modal">
<p>모달 내용</p>
<button
onClick={() =>
send({
type: 'close',
})
}
>
모달 닫기
</button>
</div>
)}
</div>
);
};
export default ModalXstate;
이제 코드를 작동해보면, 실제로 잘 작동함을 확인할 수 있다.
요즘 많이 선택되는 zustand나 jotai와 비교하면, 생각보다 초기 설정이 복잡하다.
그렇다면, 왜 그렇게 복잡한 xstate를 사용해야 할까?
하는 생각이 절로 드는데, 이에 대해서 조사한뒤 직접 사용하고 생각이 난 점들에 대해서 정리해봤다.
요약하자면, 다양한 상태변화가 있는 복잡한 상태관리 로직이 있을 때, XState를 사용하면 명확하게 상태를 관리할 수 있다 👍
XState를 선택해야 하는 이유 ✨
- 명확한 상태 모델링
- 상태를 Finite State Machine(FSM, 유한 상태 기계) 개념으로 관리하기 때문에, 컴포넌트가 어떤 상태에 있을지 한눈에 파악할 수 있다.
- 예를 들어, 모달이 open인지 close인지, 버튼이 loading인지, success인지 상태가 확실하게 정의된다.
- 상태를 Finite State Machine(FSM, 유한 상태 기계) 개념으로 관리하기 때문에, 컴포넌트가 어떤 상태에 있을지 한눈에 파악할 수 있다.
- 이벤트 기반 전이 (Event-Driven State Transitions)
- onClick 같은 이벤트를 단순히 상태 변경하는 게 아니라, 이벤트를 기반으로 상태가 변한다.
- 예를 들어, FETCH 이벤트가 발생하면 loading -> success | failure 상태로 이동할 수 있다.
- onClick 같은 이벤트를 단순히 상태 변경하는 게 아니라, 이벤트를 기반으로 상태가 변한다.
- Redux보다 간결한 상태 관리
- Redux처럼 전역 상태 관리가 필요한 경우에도 사용 가능하지만, Redux 보다는 코드가 훨씬 간결하다.
- Reducer에서 switch-case 남발하는 대신, 한눈에 보기 좋은 트랜지션으로 정리할 수 있다.
- Redux처럼 전역 상태 관리가 필요한 경우에도 사용 가능하지만, Redux 보다는 코드가 훨씬 간결하다.
- 비동기 작업 (Promise & Actor) 관리 편리
- invoke를 사용하면 비동기 API 호출을 자연스럽게 상태 흐름에 녹일 수 있다.
- useEffect 없이도 비동기 처리 가능해서, 클린한 코드를 유지할 수 있다.
- invoke를 사용하면 비동기 API 호출을 자연스럽게 상태 흐름에 녹일 수 있다.
- 불가능한 상태 방지
- Redux에서는 개발자가 직접 불가능한 상태(예: loading 상태에서 reset이 호출되는 경우)를 방지해야 하지만, XState는 Statechart 개념을 사용하여 자연스럽게 이를 차단한다.
- 상태 전이 시각화 가능
- XState는 Statechart Visualizer 를 제공하여 상태 전이를 시각적으로 확인할 수 있다.
- 구조적인 상태 관리
- Redux는 전역 상태를 관리하는 방식이지만, XState는 여러 개의 독립적인 상태 머신을 Actor 모델 기반으로 구성할 수 있어, 복잡한 UI에서도 확장성이 뛰어나다.
4번에서 비동기 상태 호출에 대해서 invoke를 언급했는데, 실제 invoke의 작동을 테스트해본 코드는 다음과 같다.
import { assign, fromPromise, setup } from 'xstate';
type ApiResult = {
userId: number;
id: number;
title: string;
completed: boolean;
};
const fakeApiCall = (userId: number): Promise<ApiResult> =>
fetch(`https://jsonplaceholder.typicode.com/todos/${userId}`).then((response) => response.json());
export const todoMachine = setup({
types: {
context: {} as {
result: ApiResult;
error: unknown;
},
// FETCH 이벤트에 userId가 포함되고, RETRY 이벤트도 정의합니다.
events: {} as { type: 'FETCH'; userId: number } | { type: 'RETRY' },
},
actors: {
// fromPromise를 사용하여 비동기 actor를 정의합니다.
// 이 actor는 input으로 { userId: number } 타입의 값을 받습니다.
fetchTodo: fromPromise<unknown, { userId: number }>(async ({ input }) => {
const user = await fakeApiCall(input.userId);
return user;
}),
},
}).createMachine({
id: 'todo',
initial: 'idle',
context: {
result: {
userId: 1,
id: 0,
title: '',
completed: false,
},
error: undefined,
},
states: {
idle: {
on: {
FETCH: { target: 'loading' },
},
},
loading: {
invoke: {
id: 'getTodo',
src: 'fetchTodo',
// event.type이 FETCH일 때는 event.userId를 input으로 사용하고, 그렇지 않으면 userId를 1로 사용합니다.
input: ({ event }) => {
if (event.type === 'FETCH') {
return { userId: event.userId };
}
return { userId: 1 };
},
onDone: {
target: 'success',
actions: assign({
result: ({ event }) => event.output as ApiResult,
}),
},
onError: {
target: 'failure',
actions: assign({
error: ({ event }) => event.error,
}),
},
},
},
success: {},
failure: {
on: {
RETRY: { target: 'loading' },
},
},
},
});
위와 같이 상태를 정의하고,
<div>
<button
onClick={() => {
sendApi({
type: 'FETCH',
userId: 1,
});
}}
>
Call Api
</button>
{apiState.value !== 'success' ? (
<div>Loading...</div>
) : (
<div>
<h1>API RESULT</h1>
<p>{apiState.context.result.title}</p>
</div>
)}
</div>
위와 같이 호출했다.
value 값에 loading, success와 같은 값이 들어가므로, 자연스럽게 api 로딩에 따른 화면 지연을 표현할 수 있다.
🚀 XState 요약
✅ 장점
- 복잡한 상태 관리가 눈에 보이게 정리됨
- useEffect 없이 비동기 작업 가능
- 불가능한 상태가 원천적으로 차단됨
- 상태 전이를 시각적으로 확인 가능
❌ 단점:
- 처음 배우는 데 약간의 러닝 커브가 있음 😵💫
- 간단한 상태 관리라면 useState보다 오버킬일 수 있음
- 현재 디버깅툴이 설치가 안됨?? (DevTools 크롬 익스텐션이 설치가 안됨....)
결론적으로, XState는 복잡한 상태를 다룰 때 진짜 강력한 도구이다! 🎯
단순한 상태 관리라면 필요 없지만, 다양한 상태 전이가 필요한 UI에서는 고려할만하다! 🚀
+ 다만, 이 글에서는 아주 단순한 예제로 xstate를 다뤄보았고, 모든 기능을 사용해본 것이 아니기에 좀 더 지속적으로 xstate를 다뤄보고 좀 더 테스트해볼 필요가 있을 것 같다.
👻 작성한 예제 코드
https://github.com/citron03/practice-next-15/tree/main/app/modal-xstate
practice-next-15/app/modal-xstate at main · citron03/practice-next-15
Practice React 19, Next 15 (with react compiler). Contribute to citron03/practice-next-15 development by creating an account on GitHub.
github.com
😊 참고 자료
https://stately.ai/docs/quick-start
Quick start | Stately
Start here to jump straight into XState and Stately Studio.
stately.ai
https://stately.ai/docs/xstate-react
@xstate/react | Stately
The @xstate/react package contains hooks and helper functions for using XState with React.
stately.ai
https://stately.ai/docs/typescript#set-up-your-tsconfigjson-file
TypeScript | Stately
XState v5 and its related libraries are written in TypeScript, and utilize complex types to provide the best type safety and inference possible for you.
stately.ai
https://ko.wikipedia.org/wiki/%EC%9C%A0%ED%95%9C_%EC%83%81%ED%83%9C_%EA%B8%B0%EA%B3%84
유한 상태 기계 - 위키백과, 우리 모두의 백과사전
위키백과, 우리 모두의 백과사전. 오토마타 벤 다이어그램 (각 레이어를 클릭하면 해당 글로 이동합니다.) 유한 상태 기계(finite-state machine, FSM) 또는 유한 오토마톤(finite automaton, FA; 복수형: 유한
ko.wikipedia.org