[이전 블로그 글] XState와 유한상태기계

Aug 20, 2024

xstate는 유한 상태 기계 의 개념을 차용한 javascript 프레임워크이다. typescript의 타입추론 기능을 사용해서 견고하고 믿을 수 있게 규모있는 어플리케이션을 개발할 수 있게 한다.

기존의 상태 관리

useState로 대표되는 상태는 너무 많은 자율성을 제공한다. 여러개의 지역 상태가 있는 경우, 상태의 흐름을 파악하기 어려울 수 있다. 그리고 여러 개발자를 거쳐가며 본래의 의도를 벗어나거나 잠재적인 버그를 발생시킬 가능성이 높다.

그래서 규모있는 어플리케이션을 만들거나, 여러명의 개발자가 있는 경우 redux와 같은 라이브러리를 사용하곤 한다. reducer 와 slice 등으로 관심사를 분리할 수 있고, 정해진 방향으로 의도를 가지고 작성한 reducer 로만 상태를 분리한다.

새로 개발하는 프로젝트의 경우 2020년 경 부터 atomic state 가 주목받기 시작했던 것 같다. 이제는 어느정도 성숙한 개념이라고 생각한다. 이제는 커뮤니티의 품으로 간, meta 의 recoil 을 소개하는 유명한 영상이다.

빠르게 개발을 할때는 더할나위 없이 좋다고 생각한다. 이쪽 분야에서 제일 많이 쓰이는 라이브러리는 jotai로 알고 있는데, atomic state 의 철학에 기반한 많은 기능들을 first-party로 지원한다. 하지만 마찬가지로 atomic state 는 사용자에게 너무나 많은 자유도를 제공해서, 잘 쓰면 좋지만, 여러 개발자가 협업하는 경우 맥락을 파악하고 최선의, 일관적인 코드를 짜기는 어렵다는 생각이 든다.

atomic state 를 제공하는 라이브러리가 피곤하고, react 에서 쓸 때 보일러플레이트스러운 코드를 작성해야한다고 생각하는 사람들이 많았던 것 같다. 그래서 사람들이 많은 찾은 것이 zustand 이다. zustand는 아예 react만을 상정하고 만들었다는 생각이 들 정도로 react 친화적인 인터페이스를 제공한다. 어쩌면 atomic state 보다 더한 자율성을 개발자에게 제공한다고 생각한다. 그러면서도 react에 완전히 의존적이지는 않아서, react 외부의 리소스와 통신할때도 편리하다. 그리고 코어 팬층이 많아서, 서드파티 플러그인도 꽤 활성화되어있다. 상태 관리 라이브러리보다, 전역상태를 관리하는 훅 작성이 더 메인인 것 같다. 하지만 딱 이정도가 필요한 프로젝트가 많은가? 라고 생각하면 잘 모르겠다. 소규모에서는 react의 context로 간단하게 사용할 수 있고, 그렇다고 대규모 프로젝트에서는 퍼스트파티 지원이 부족하다고 느껴진다.

언제 무엇을 쓰는가?

위에서는 어쩌다보니 useState 와 redux, jotai, zustand 등의 상태 관리 툴들과 비교하는 것처럼 써놨지만, 그렇지는 않다. 오히려 같이 쓰는 것을 추천한다. 서로 성격이 다르기 때문이다.

useState 는 지역 상태에 최적화 되어있다. useState로 선언한 상태를 컴포넌트 단계에서 prop으로 수 단계 이상 내리기는 부담스럽다. 가장 알맞은 사용처는 사용자의 인풋이라고 생각한다. 또는 ui 컴포넌트 내부에서 사용할 수 있다.

jotai 와 zustand 등은 어플리케이션 전역에서 가지고 있고, 서버측에서 자주 바뀌지 않는 상태를 저장하기에 알맞다고 생각한다. 예를 들면 유저의 정보, 로그인 여부, 권한, 브라우징 히스토리 등이 있겠다. atomic 은 provider 등을 사용해서 사용처를 제한하면 더 유연하게 사용할 수 있겠다.

redux를 쓰는 프로젝트에서 정확한 질서 없이 어느 부분은 모두 전역 상태로 빼고, 어느 부분은 코어한 부분중 일부만 전역상태를 사용하는 사례를 본 적이 있는데, 이렇게 질서 없이 프로그램을 짜면 잠재적인 버그를 많이 만들게 된다. 사실 질서를 만들고 모든 사람이 같게 일관적으로 코드를 작성하기는 어렵다. 그래서 전역상태는 최소한으로 가지고 있는게 차라리 상태의 흐름을 파악하고, 디버깅하기에 더 직관적이라고 생각한다. 그리고 논외이지만 서버에서 가져온 일반적인 데이터는 react query 나 swr을 사용하자.

기존의 무슨 문제를 해결해야 하는가?

단지 흥미를 위해서 라이브러리를 도입하는거는 개인 프로젝트에서는 괜찮지만, 실무에서는 합리적인 기준을 통해서 선택해야 할 것이다. 가장 중요한 것은 상태 및 데이터의 흐름이다. 기존의 상태 관리 라이브러리들은 비교적 엄격한 reducer를 가진 redux조차도 unidirectional 흐름을 가지고 있다. 이 말은, 개발자의 의도와 달리 어느 방향으로든 갈 수 있다는것을 의미한다. xstate 는 이를 완전히 방지한다. 어떻게 할까? 그 전에, 상태 기계가 무엇인지 알아본다.

상태기계

단적으로 나타내는 사진이다. status:'pending'. 명시적으로 지금이 무슨 상태인지를 나타낸다. enum을 사용해서 여러가지 상태를 나타낼 수도 있다. 이런 방식은 확장 가능하며, 상태간에 위계를 가질 수 있다. 그리고 직관적으로 분기를 가능하게 한다. 나는 이런 접근방식을 좋아해서, 가능하다면 이런 식으로 상태를 선언하곤 했다. xstate는 그런 철학을 확대하여 시나리오 단위로 사용가능한 라이브러리이다.

XState 는 어떻게 할까?

바로 state, event, context를 분리함으로서 가능하다. 하나의 machine 이 공유하는 것은 context 밖에 없다. event handler 등은 모두 state 에 따라서 분기된다. 보편적으로 상태를 의미하는 state 보다 mode 에 가까운 것 같다. 오히려 기존의 state 와는 context에 대응된다. 이를 기준으로 ui 를 그리게 된다. state 마다 다음 state 를 정의할 수 있고, 각 state 에서 context를 사용하고 event를 핸들링하게 된다. 이러면 이를 플로우 차트로 그릴 수 있게 된다. 이에 집중하여 각 이벤트, state 등에 이름을 달 수도 있다. 비동기, observable도 당연히 지원한다. 강력한 개발자 도구에서는 ui 에서 플로우차트를 그리거나 볼 수 있고, 각 이벤트를 시뮬레이션할 수 있다.

플로우차트를 보면 코드를 한줄한줄 읽지 않고도 어떻게 동작할지 쉽게 파악할 수 있다. 코드를 작성하는 경험도 좋다. typescript 지원을 위한 코드 작성이 약간 번거롭지만, 이를 바탕으로 강력한 타입추론기능을 제공한다.

위 machine은 장바구니 화면을 위한 machine으로, 비동기 상품 가져오기, 구매할 상품 선택, 비동기 수량 선택 및 검증, 각 요청의 에러 메시지 저장, 실패시 재시도, 구매 요청 기능을 제공한다. 장바구니를 위주로 여러 상태가 얽혀있어, 기존의 상태관리도구를 사용했다면 작성한 사람마다 아주 다른 모양의 코드가 나왔을 것이다. 하지만 xstate로 작성한 사람은 대부분 비슷한 모양이 나올 것이다.

더 살펴볼 것들은?

  • derived state (computed state 등으로도 불린다.) 는 단방향 상태의 흐름에서 ui 를 그리거나 로직을 수행할때 아주 중요한 지점이다. zustand를 사용해보았을 때 내부적으로 이를 위한 기능을 제공하지 않아, 이를 감싸는 훅 등을 작성해야 했다. derived state 를 라이브러리에서 제공하는 경우, 렌더링 최적화를 기대할 수 있고, (의존하는 상태가 업데이트 될 때만 재계산) 상태의 선언부와 가까운게 더 합리적으로 보인다. recoil이 처음 나왔을때 강조하던 부분이기도 하다. xstate 에서는 first party로 이를 위한 기능은 제공하지 않는것으로 보인다. 다만 model 과 view 를 분리한다는 점에서, 약간의 캐싱 및 성능 을 포기하더라도, 렌더링 함수 (컴포넌트) 부에서 상수로 선언하는것이 나름 합리적으로 보인다. 필요한 경우, useMemo 등을 사용할 수 있겠다.
  • xstate를 사용한 장바구니 화면 예시