Skip to content

상태 관리

Zustand 기반 전역 상태 설계입니다.

스토어 구조

useAppStore는 6개 slice를 합성합니다:

typescript
useAppStore = create(
  persist(
    devtools((...a) => ({
      ...createCanvasSlice(...a), // tool, zoom, position
      ...createHistorySlice(...a), // undo/redo
      ...createSelectionSlice(...a), // selectedIds
      ...createEditorSlice(...a), // sidebar, labels
      ...createNodesSlice(...a), // nodes, nodeOrder
      ...createWorkspaceSlice(...a), // workspaceId, nodesLoaded, 저장 상태
    })),
    { name: 'canvas-app-v1', version: 2, partialize },
  ),
);

Slice 상세

CanvasSlice

상태타입설명
tool'move' | NodeTool현재 활성 도구
zoomnumber줌 레벨
position{ x, y }팬 오프셋
canvasSize{ width, height }캔버스 DOM 크기

HistorySlice

실행 취소/다시 실행용 스택입니다. 현재는 tool(활성 도구) 변경만 스냅샷으로 저장합니다.

  • 스냅샷 타입: { tool } (historySlice.ts)
  • pushHistory() 유틸이 있으나, 아직 어떤 액션에서도 호출되지 않아 undo/redo 커맨드는 사실상 비활성 상태입니다
  • 노드 추가·수정·삭제 히스토리는 향후 pushHistory 연동 또는 별도 스냅샷 설계가 필요합니다

SelectionSlice

상태설명
selectedIds선택된 노드 ID 배열. selectAll()['__all__'] sentinel을 사용
deleteSelectionRequest삭제 요청 카운터. useCanvasSelection이 증가를 감지해 Fabric 객체를 제거

EditorSlice

상태기본값설명
isPropertiesSidebarOpentrue속성 패널 표시
isVisibleNodeLabelstrue노드 표시 이름(라벨) 오버레이 표시

NodesSlice

상태설명
nodesRecord<id, CanvasNodeState>
nodeOrder렌더링/z-index 순서

WorkspaceSlice

상태설명
workspaceId현재 편집 중인 워크스페이스 ID
nodesLoaded서버에서 노드 문서 로드 완료 여부
isWorkspaceLoading문서 로드 중
isNodesSaving서버 저장 중
documentErrorunauthorized / forbidden / not_found

커맨드 시스템

모든 사용자 동작은 Command로 선언됩니다:

typescript
interface Command {
  id: string;
  label: string;
  shortcut?: string;
  group?: string;
  isActive?: (store) => boolean;
  isDisabled?: (store) => boolean;
  execute: (store) => void;
}

실행:

typescript
executeCommand('history.undo', store);

커맨드 그룹:

group예시
toolsmove, text
historyundo, redo
selectionsselectAll, delete
viewszoomIn, zoomOut, zoomToFit
editortogglePropertiesSidebar

Persist (localStorage)

stores/persistence/appStorage.ts:

typescript
export const PERSIST_GROUPS = {
  preferences: ['isPropertiesSidebarOpen', 'isVisibleNodeLabels'],
};
  • preferences — UI 설정만 localStorage에 저장
  • nodes, nodeOrder는 persist 대상이 아님 — 서버 Node 테이블에 저장

partializeAppState가 persist 대상만 추출합니다.

버전 관리

typescript
export const APP_STORAGE_KEY = 'canvas-app-v1';
export const APP_STORAGE_VERSION = 2;

스키마 변경 시 version을 올리고 migrate 함수를 추가하는 것을 권장합니다.

서버 동기화

features/workspace/hooks/useWorkspaceDocument.ts:

  1. 로드: GET /workspaces/:id/nodesreplaceDocument(nodes, nodeOrder)nodesLoaded = true
  2. 저장: nodes / nodeOrder 변경 시 500ms debouncePUT /workspaces/:id/nodes
  3. 초기 로드 직후 1회 저장은 skipNextSaveRef로 스킵

개별 노드 PATCH가 아니라 문서 전체 스냅샷을 PUT합니다.

Fabric ↔ Store 동기화

  • Hydration: nodesLoadeduseCanvasHydrationhydrateCanvasFromStore로 store → Fabric 일괄 복원
  • Sync: useCanvasNodes가 실시간 양방향 처리 — 캔버스 조작 시 Fabric read → setNode/updateNode, store 변경 시 Fabric write
  • Selection: useCanvasSelection이 Fabric selection ↔ selectedIds 동기화 및 삭제 처리
  • Placement: createState + createFabricObject로 store + Fabric 동시 추가
  • Sync guard: canvasSync.tssyncingFromCanvasRef / isCanvasInteracting()으로 루프·덮어쓰기 방지

노드 타입별 변환은 NodeDefinitionstateFromFabricObject / applyStateToFabricObject가 담당합니다. 메서드 역할·흐름은 노드 시스템을 참고하세요.

Devtools

Redux DevTools Extension으로 app-store 이름으로 inspect 가능합니다.

관련 문서

Canvas — 시각적 프로토타입 에디터