Appearance
프론트엔드 아키텍처
apps/web React SPA의 구조와 핵심 패턴입니다.
기술 선택
| 영역 | 선택 | 이유 |
|---|---|---|
| 빌드 | Vite 6 | 빠른 HMR, ESM 네이티브 |
| 라우팅 | TanStack Router | 타입 안전 파일 기반 라우팅 |
| 스타일 | Tailwind CSS 4 | 유틸리티 퍼스트, @tailwindcss/vite |
| 캔버스 | Fabric.js 7 | 객체 기반 2D 캔버스, Textbox 등 |
| 상태 | Zustand | 경량, slice 합성, persist/devtools |
라우트 구조
src/routes/
├── __root.tsx # 루트 + AuthRedirectHandler
├── index.tsx # / (홈 — 로그인·워크스페이스 진입)
├── login/
│ └── route.tsx # /login (Google OAuth, redirect 쿼리 지원)
└── canvas/
├── route.tsx # /canvas 레이아웃 (에디터에서만 SidebarProvider)
├── index.tsx # /canvas/ — 워크스페이스 목록·생성
└── $workspaceId.tsx # /canvas/:id — 캔버스 에디터TanStack Router Vite 플러그인이 routeTree.gen.ts를 자동 생성합니다.
API 연동
| 경로 | API 호출 |
|---|---|
/login | GET {VITE_API_URL}/auth/google (OAuth 리다이렉트) |
/ | GET /users/me, POST /auth/logout |
/canvas/ | GET /workspaces, POST /workspaces |
/canvas/:id | GET /workspaces/:id, GET/PUT /workspaces/:id/nodes |
모든 API 요청은 credentials: 'include'로 JWT 쿠키를 전송합니다.
인증·접근 제어
/canvas,/canvas/:id진입 시beforeLoad에서GET /users/me로 인증 확인 → 미인증 시/login?redirect=.../canvas/:id는GET /workspaces/:id로 멤버십 확인 → 403/404 시/canvas?error=...로 리다이렉트- OAuth 복귀 후
AuthRedirectHandler가sessionStorage의 redirect URL로 복귀
Canvas 컴포넌트
components/canvas/index.tsx는 Fabric Canvas의 React 래퍼입니다:
tsx
export function Canvas({ className }: CanvasProps) {
const containerRef = useRef<HTMLDivElement>(null);
const domRef = useRef<HTMLCanvasElement>(null);
const [canvas, setCanvas] = useState<fabric.Canvas | null>(null);
useFabricCanvas(domRef, { onCanvas: setCanvas, containerRef });
useGlobalShortcuts();
useCanvasCamera(canvas);
useCanvasViewportWheel({ canvasContainerRef: containerRef, canvas });
useCanvasHydration(canvas);
useDrawingTools(canvas);
useCanvasSelection(canvas);
useCanvasNodes(canvas);
return (
<div ref={containerRef} className={`relative touch-none overscroll-none ${className ?? 'h-full w-full'}`}>
<canvas ref={domRef} />
<NodeLabelsOverlay canvas={canvas} />
</div>
);
}각 hook은 단일 책임을 가집니다:
| Hook | 책임 |
|---|---|
useFabricCanvas | Canvas 인스턴스 생성/해제, ResizeObserver로 canvasSize 동기화 |
useGlobalShortcuts | 전역 단축키 → COMMANDS 실행 |
useCanvasCamera | zoom, pan 상태 → viewportTransform |
useCanvasViewportWheel | 휠 이벤트 → 줌/팬 |
useCanvasHydration | 서버 로드 완료 후 store → Fabric 객체 복원 |
useDrawingTools | 도구별 노드 배치 (placement) |
useCanvasSelection | Fabric selection ↔ selectedIds, 삭제 요청 처리 |
useCanvasNodes | nodes 변경 ↔ Fabric sync (이동·리사이즈·편집·삭제) |
/canvas/:id 페이지는 추가로 useWorkspaceDocument(workspaceId)를 호출해 노드를 서버에서 로드하고 변경 시 debounce(500ms) 저장합니다.
Features 모듈
features/
├── auth/
│ ├── api.ts # fetchMe, ApiError
│ └── AuthRedirectHandler.tsx # OAuth 후 redirect 복귀
├── workspace/
│ ├── api.ts # workspaces / nodes API 클라이언트
│ ├── components/
│ │ └── WorkspaceHub.tsx # 목록·생성 UI
│ └── hooks/
│ └── useWorkspaceDocument.ts # 로드 + debounce 저장
├── canvas/
│ ├── hooks/
│ ├── utils/
│ ├── labels/
│ └── drawing/
└── shortcuts/
└── hooks/UI 컴포넌트
components/ui/— shadcn/ui (Button, Sidebar, Input, Badge, Sheet, DropdownMenu 등)components/canvas/zoomLevel.tsx,position.tsx— 좌상단 줌·팬 좌표 HUDcomponents/propertiesSidebar/— 우측 속성 패널 (현재 placeholder 메뉴만)components/themeProvider.tsx— 다크/라이트 테마
/canvas/:id 페이지는 ModeToggle, ZoomLevel, Position, Canvas, "워크스페이스" 복귀 버튼을 조합합니다.
전역 단축키
Canvas 컴포넌트에서 useGlobalShortcuts()를 호출합니다. 이 hook이 COMMANDS 배열을 순회하며 keydown 이벤트를 처리하고, keyCombo.ts로 mod+z 등 플랫폼별 키 매핑을 처리합니다.
개발 도구
- TanStack Router Devtools — 라우트 디버깅
- Zustand devtools — Redux DevTools 연동
window.devCanvas— DEV 모드 콘솔 디버깅 API (global-injection.ts)store.app— Zustand storecanvas— 현재 Fabric CanvasgetSelectedNodeIds(),getNodeById(id),getNodes(),getNodeOrder()