XState는 Recoil, Jotai 등에 비해 상대적으로 잘 알려지지 않았으나,
상태 머신, 상태차트의 개념을 활용하여 이벤트 드리븐, 선언적 상태 관리를 실현하는 라이브러리 입니다.
해당 라이브러리의 개발자는 Microsoft 직원이며, 의외로 굉장히 많은 회사에서 해당 라이브러리를 사용하고 있습니다.
특히 프론트엔드 개발 시 상태 차트 시각화와,
상태 기반 테스트 자동 생성(Model Based Testing)이라는 특별한 장점이 있다 생각하여 리서치를 진행해 보았습니다.
이 아티클에선 XState를 이용해 간단한 UI 상태관리를 구현해봅니다.
XState는 뭔가요?
입문자 레벨 : Counter
리액트 환경에선 아래 2가지 라이브러리가 필요합니다.
npm install xstate
npm install @xstate/react
그 다음 counterMachine이라는 파일을 만듭니다.
import { createMachine, assign } from "xstate";
export const counterMachine = createMachine({
// 여기서 우리는 머신의 초기 상태를 정의합니다.
context: { count: 0 },
on: {
// 여기에서 상태 전환을 트리거할 이벤트를 정의합니다.
INCREMENT: {
actions: assign({
count: (context) => context.count + 1
})
},
DECREMENT: {
actions: assign({
count: (context) => context.count - 1
})
}
}
});
context 속성을 사용하여 머신의 초기 상태를 정의합니다.
on 속성을 사용하여 전환을 트리거할 이벤트를 정의합니다.
이제 상태 기계가 준비되었으니, 리액트 컴포넌트에서 사용해봅시다.
// 아까 만든 상태 기계를 임포트합니다.
import { counterMachine } from "./counterMachine";
// useMachine 훅을 import 합니다.
import { useMachine } from "@xstate/react";
import styled from "styled-components";
const Wrapper = styled.section`
// ...
`;
export default function Index() {
// useMachine 훅은 현재 상태와 send 함수가 포함된 배열을 반환합니다.
// send 함수를 사용하여 이벤트를 트리거할 수 있습니다.
const [state, send] = useMachine(counterMachine);
return (
<Wrapper>
<h1>Counter</h1>
<h2>{state.context.count}</h2>
<button onClick={() => send("INCREMENT")}>Increment</button>
<button onClick={() => send("DECREMENT")}>Decrement</button>
</Wrapper>
);
}
중급 레벨 : Todo
상태 머신을 만드는 방법과 React에서 사용하는 방법을 이해했습니다.
페이로드를 상태 머신에 전달하는 방법을 배우기 위해 Todo 애플리케이션을 만들어 봅시다.
이전과 유사한 템플릿을 사용합니다.
먼저 todosMachine 파일을 만듭니다.
import { createMachine, assign } from "xstate";
export const todosMachine = createMachine({
// 여기서 우리는 머신의 초기 상태를 정의합니다.
context: {
todos: []
},
// 여기에서 상태 전환을 트리거할 이벤트를 정의합니다.
on: {
ADD_TODO: {
actions: assign({
todos: (context, event) => [...context.todos, event.todo]
})
}
}
});
send('ADD_TODO', { todo: 'Learn XState' });
이제 컴포넌트에서 사용해 봅시다.
import styled from "styled-components";
import { todosMachine } from "./todosMachine";
import { useMachine } from "@xstate/react";
import { useState } from "react";
export default function Index() {
const [newTodo, setNewTodo] = useState("");
const [state, send] = useMachine(todosMachine);
function handleAddTodo() {
if (newTodo) {
send("ADD_TODO", {
todo: newTodo
});
setNewTodo("");
}
}
return (
<div style={{ textAlign: "center" }}>
<h1>Todo list</h1>
{/* 해당 방법으로 컨텍스트에 접근하는 것이 가능합니다! */}
{state.context.todos.map((todo, index) => (
<h2 key={index}>⭐️ {todo}</h2>
))}
<input
type="text"
placeholder="Add new Todo"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
/>
<button onClick={handleAddTodo}>Add todo</button>
</div>
);
}
과제 : ToDo 클릭 시 삭제 기능을 추가해 봅시다.
고급 레벨 : 신호등
상태머신을 만드는 방법, 페이로드를 전달하는 방법을 배웠습니다.
이제 XState 상태 머신의 상태에 대해 알아보겠습니다.
각 신호는 단방향으로 Red > Yellow > Green > Red ... 이렇게 사이클을 돌며 전환합니다.
이전의 context가 없네요?
사실 이 context는 상태 기계에서 전환에 관련되었다기 보단 오직 description과 관련된 정보들을 포함하는 객체입니다.
심화된 주제라 해당 게시물에선 다루지 않습니다.
export const trafficLightMachine = createMachine({
// 새로운 속성 initial : 초기 상태를 의미합니다.
initial: 'red',
// 새로운 속성 states : 모든 기계의 상태를 포함합니다.
states: {
// 단 하나의 NEXT 이벤트만을 사용하여 다음 상태로의 전환을 트리거 합니다.
red: {
on: {
NEXT: {
target: 'yellow',
},
},
},
yellow: {
on: {
NEXT: {
target: 'green',
},
},
},
green: {
on: {
NEXT: {
target: 'red',
},
},
},
},
});
이 상태 기계를 useMachine 훅을 통해 사용해 봅시다.
import React from "react";
import TrafficLight from "react-trafficlight";
import { useMachine } from "@xstate/react";
import trafficLightMachine from "./trafficLightMachine";
import styled from "styled-components";
const Wrapper = styled.section`
display: flex;
flex-direction: column;
align-items: center;
`;
export default function Index() {
const [state, send] = useMachine(trafficLightMachine);
return (
<Wrapper>
<div>
<TrafficLight
RedOn={state.value === "red"}
YellowOn={state.value === "yellow"}
GreenOn={state.value === "green"}
/>
</div>
<button onClick={() => send("NEXT")}>Next light</button>
</Wrapper>
);
}
보시다시피 신호등의 각 색상에 대해 버튼이 있습니다.
그리고 현재 상태가 색상과 같지 않으면 버튼을 비활성화합니다.
그리고 버튼에 onClick 이벤트를 추가합니다.
주 : 원문에 구현 소스가 없어서 적당히 외부 라이브러리를 가져다 만들었습니다.
XState의 또 하나의 강점은, UI의 상태를 StateChart로 보여줄 수 있다는 점입니다.
(무료 버전도 있고 유료 버전도 있는줄 알았는데 일단은 베타라 둘 다 무료입니다.)
아래는 정말 간단하지만, 동시성, 비동기 등이 들어가면 더욱 더 강력한 시각화가 됩니다.
상태 기계 시각화하기
https://stately.ai/viz 서비스 이용하기 : 회원가입 필요.
참고로 위 구현은 모범 사례와는 약간 거리가 있습니다만, 기본적인 사용법을 배우기에는 충분했을 것으로 생각합니다.
물론 XState가 많은 사람들이 좋다고 인정하지만, 단점으로 뽑는 것이 높은 진입 장벽인 만큼, 추가 학습이 꽤 필요합니다.
이 글에서 다루진 않았으나, 특히 이 XState의 강점은, 상태 모델의 재사용에 있다고 생각합니다.
앞으로 Model Based Testing, visualization 등의 내용을 추가로 다뤄볼 예정입니다.
'FrontEnd' 카테고리의 다른 글
리액트 패턴 : Derived State - 파생(계산된) 상태 활용하기 (0) | 2022.04.28 |
---|---|
리액트 패턴 : 타입스크립트를 활용해 as Props 사용하기 (0) | 2022.04.28 |
타입스크립트의 타입 시스템으로 틱택토(Tic Tac Toe) 구현하기 (0) | 2022.04.27 |
타입스트립트의 타입시스템으로 산수 구현하기 (0) | 2022.04.27 |
React Remix Framework로 알아보는 중첩 경로(nested routing) (0) | 2022.04.26 |