데우스랠리 / 2025.06 ~
데우스랠리 상태관리 아키텍처
참고 자료 · 데우스랠리 데모 영상 보기 ↗
TL;DR
데우스랠리는 사내 디자인 편집 서비스 데우스 위에서 동작하는 실시간 모션 편집 플러그인이에요.
이 작업의 핵심은 복잡한 모션 편집 상태를 "무엇으로" 관리할지, 그리고 선택한 도구를 "어떻게" 구조화할지 결정하는 것이었어요. 타임라인에서 duration, delay, motion value 같은 작은 값이 자주 바뀌고, 그 변경이 조작 패널과 프리뷰, 코드젠 입력까지 같은 기준으로 반영되어야 했어요.
데우스가 MobX observable 기반 데이터를 제공하고 있었고, 데우스랠리 역시 작은 단위의 편집 상태를 여러 UI 영역에서 함께 바라봐야 했기 때문에 MobX를 선택했어요. 이후 하나의 기준 데이터를 중심에 두고 조작 패널, 타임라인, 프리뷰, 코드젠이 각자 필요한 상태만 읽도록 나눴어요. 여러 화면이 함께 쓰는 값은 한곳에 모아, 기능이 늘어나도 수정할 위치를 빠르게 찾고 동료가 구조를 따라갈 때의 인지부하도 낮출 수 있게 설계했어요.
문제 정의 Problem
데우스랠리는 데우스 위에서 모션을 실시간으로 수정하고, 프리뷰 화면에서 수정한 모션을 즉시 확인할 수 있는 플러그인이에요. 이 기능을 구현하려면 실시간 모션 데이터 조작과, 그 조작 데이터를 기반으로 한 프리뷰 재생을 동시에 해결해야 했어요.
그래서 상태관리는 단순히 UI 값을 저장하는 문제가 아니라, 편집 데이터와 프리뷰 재생 데이터, 저장소 동기화, 코드젠 입력이 같은 기준을 바라보게 만드는 기초 설계였어요. 그중에서도 가장 중요한 질문은 복잡한 모션 상태를 "무엇으로" 관리하고, 그 선택을 "어떻게" 구조화할 것인지였어요.
접근 방식 Approach
문제를 간단히 요약하면 데우스랠리의 상태관리를 "무엇으로" 만들 것인가, 그리고 선택한 상태관리 방식을 "어떻게" 설계할 것인가였어요.
먼저 "무엇으로"에 해당하는 선택지는 Zustand와 MobX였어요. 두 라이브러리는 반응성 모델이 달랐어요. Zustand는 비교적 큰 상태 단위를 구독한 뒤 selector로 필요한 값을 좁히는 Coarse-Grained Reactivity에 가깝고, MobX는 실제로 읽힌 observable 값 단위로 변경을 추적하는 Fine-Grained Reactivity에 강점이 있었어요.
판단 기준은 익숙함이 아니라 데이터 모델과 실시간 반응성이었어요. 데이터 모델 측면에서 데우스랠리는 독립 서비스가 아니라 데우스 위에 얹히는 플러그인이었고, 데우스가 내려주는 데이터도 MobX observable 모델과 이어져 있었어요. 실시간 반응성 측면에서는 타임라인 드래그처럼 작은 값이 빠르게 바뀌고, 그 값이 조작 패널과 프리뷰에 곧바로 반영되어야 했어요. 그래서 호스트의 반응성 모델을 끊었다 다시 맞추는 비용과 Fine-Grained Reactivity를 유지했을 때의 장점을 함께 견줘봤어요.
"어떻게" 만들 것인가의 기준은 확장성과 인지부하였어요. 완전히 새로 만드는 플러그인이었고 제품 스펙도 처음부터 고정되어 있지 않아서, 이후 어떤 모션 타입과 패널, 코드젠 옵션이 추가될지 알 수 없었어요. 정형화된 모범 사례를 그대로 가져오기 어려운 영역이었던 만큼, 함께 일하는 동료가 같은 기준으로 이해할 수 있는 구조를 만드는 것이 중요했어요.
핵심 결정 Decision
"무엇으로"에 대한 결정은 MobX였어요.
데이터 모델 기준에서는 데우스의 선택을 그대로 이어받는 것이 중요했어요. 호스트 서비스인 데우스가 MobX를 사용하고 있었고, 내려주는 데이터 모델도 observable 데이터와 이어져 있었기 때문에 MobX를 쓰면 플러그인 내부에서 별도의 변환 계층을 크게 늘리지 않고 같은 반응성 모델 위에서 상태를 다룰 수 있었어요.
실시간 반응성 기준에서는 Fine-Grained Reactivity가 더 중요했어요. Zustand도 충분히 선택할 수 있는 도구였지만, 디자인 에디터와 모션 편집이라는 특성상 트리 구조의 중간 노드나 특정 motion property만 바뀌는 일이 잦았어요. 그 변경을 조작 패널과 타임라인, 프리뷰가 함께 바라봐야 했기 때문에, 실제로 읽힌 observable 값 단위로 변경을 추적하는 MobX가 이 프로젝트에 더 맞다고 판단했어요.
"어떻게"에 대한 결정은 MobX 기반 클래스형 store 구조였어요.
확장가능성 측면에서는 데우스 storage에 저장되는 값을 root 데이터로 삼고, 그 아래에 좌측 조작 패널, 하단 타임라인, 프리뷰, 코드젠처럼 책임이 다른 영역을 section store로 분리했어요. 새로운 모션 타입이나 패널 기능이 추가되더라도 어느 store에 책임을 둘지 판단할 기준을 만들기 위해서였어요.
인지부하 측면에서는 여러 섹션이 함께 사용하는 값을 root store 하위의 bridge store에서 공유하도록 설계했어요. 패널 open 상태, 프리뷰 접힘 상태, 다중 노드 선택처럼 한 영역에만 속하지 않는 값을 section store 곳곳에 흩어두면 구조를 따라가기 어려워져요. bridge store를 두면 공유 상태의 위치가 분명해지고, 동료에게 구조를 설명하거나 나누어 구현할 때도 인지부하를 낮출 수 있었어요.
상태 감시와 파생값 계산은 MobX store에 맡기고, React 컴포넌트는 store에서 필요한 computed 값만 읽도록 나눴어요. 사용자가 값을 바꾸면 Deus storage 업데이트 요청과 MobX action 기반 메모리 상태 갱신을 함께 수행했어요. 당시에는 MobX와 class store가 익숙하지 않았지만, 기술 숙련도보다 프로덕트 상황에 맞는 반응성 모델을 고르는 쪽을 더 중요하게 봤어요.
해결 결과 Result
약 1년 가까이 운영하는 동안 선택의 기준이었던 데이터 모델, 실시간 반응성, 확장가능성, 인지부하 어느 쪽에서도 큰 구조 변경이 필요하지 않았어요. 제가 확인한 범위에서는 상태 구조 때문에 생긴 데이터 정합성 이슈가 없었고, 패널과 프리뷰, 코드젠이 같은 root 데이터를 기준으로 동작한 덕분에 편집 값과 출력 값의 기준을 맞출 수 있었어요.
최초 오픈 이후 신규 모션 타입이나 패널 기능이 추가될 때도 section store와 bridge store의 책임 경계를 기준으로 수정 위치를 판단할 수 있었어요. 실시간 편집 값은 Fine-Grained Reactivity로 필요한 영역에만 반영했고, 공유 상태는 bridge store에 모아둔 덕분에 동료와 기능을 나누어 개발할 때의 인지부하와 커뮤니케이션 비용을 줄일 수 있었어요.
배운 점 Learning
초기 설계의 중요성과 적절한 기술 선택의 중요성을 배웠어요. 한 번 릴리즈된 뒤에는 상태관리 방식을 바꾸는 일이 큰 마이그레이션 부담으로 이어질 수 있기 때문에, 처음부터 데이터 모델과 반응성, 확장성, 인지부하를 함께 보고 결정하는 게 중요했어요.
예전이었다면 제게 더 익숙한 Redux나 Zustand를 먼저 떠올렸겠지만, 이번에는 기능 구현의 블로커를 먼저 정의하고 상황에 맞는 기술을 골랐어요. 물론 MobX 선택에도 trade-off가 있었어요. 로그에 Proxy 객체로 찍혀 데이터를 확인하기 번거로웠고, observable 관계가 어디서 끊겼는지 추적하기도 어려웠어요. 다음에는 이런 디버깅 비용과 저장 동기화 빈도까지 더 일찍 검토해야겠다고 느꼈어요.
이 작업을 다시 한다면 Next
다시 만든다면 Zustand도 진지하게 고려할 것 같아요. 공유할 수 있는 범위에서 핵심 로직을 자세히 설명하긴 어렵지만, 데우스랠리에는 중간에 데이터 직렬화가 필요한 지점이 있었어요. 직렬화가 계속 끼어드는 구조라면 observable state의 장점을 끝까지 누리기 어려워요. 다음에는 반응성 모델뿐 아니라 저장 동기화 빈도와 직렬화 경계, 렌더링 병목, 디버깅 도구까지 함께 놓고 상태관리 방식을 정해보고 싶어요.