"모두의 스토리, 우리의 스튜디오."
Onuri Studio는 URL 한 줄로 입장하는 실시간 협업 화이트보드 웹앱이다. FigJam과 유사한 무한 캔버스 위에서 여러 사용자가 동시에 도형을 그리고, 메모를 붙이고, 표를 만들 수 있다.
"방송 스튜디오" 메타포를 기반으로, 사용자는 채널(Channel) 에 입장해서 스토리(Story) 라는 화이트보드를 함께 만든다. 접속 중인 사용자에게는 "On Air" 빨간 인디케이터가 표시된다.
| 영역 | 선택 | 비용 |
|---|---|---|
| 프레임워크 | Next.js 14 App Router + TypeScript | 무료 |
| UI | TailwindCSS + shadcn/ui | 무료 |
| 캔버스 | tldraw v5 (Hobby License) | 무료 |
| 실시간 동기화 | Supabase Realtime broadcast + presence (LWW) | 무료 티어 |
| DB + 인증 | Supabase Free (500MB DB, 50K MAU) | 무료 |
| 호스팅 | Vercel Hobby | 무료 |
| AI 도구 | Claude Code (Anthropic, Opus 모델) | 유료 |
| 패키지 매니저 | pnpm | 무료 |
이 프로젝트의 특이점은 모든 설계와 구현이 Claude Code와의 대화를 통해 이루어졌다는 것이다. 초기 프로덕트 정의서(CLAUDE.md)를 프롬프트로 입력하고, 이후 Phase별로 코드를 생성하고, 발생하는 문제를 대화로 해결하며, 의사결정을 Decision Log로 누적해갔다.
개발 과정에서 21개의 의사결정(D-001 ~ D-021)이 Claude Code와의 대화를 통해 내려졌다. 그 중 프로젝트 방향에 가장 큰 영향을 준 결정들:
| 결정 | 내용 | 영향 |
|---|---|---|
| D-010 | Yjs CRDT 대신 Supabase Realtime broadcast + LWW 채택 | 학습 비용 절감, MVP 속도 확보. 50명 이상 시 마이그레이션 필요. |
| D-013 | Google SSO 조기 도입 (원래 Phase 7 계획) | 이메일 매직 링크 없이도 회원 가입 가능하게 만듦 |
| D-016 | tldraw Editor abstraction L1 | lib/editor/index.ts 단일 모듈로 tldraw 표면 추상화 — 향후 에디터 교체 가능성 확보 |
| D-017 | Realtime sync hardening + 25명 정원 | Smart autosave, Non-destructive reconnect, broadcast throttle 50ms batching |
| D-018 | Google Drive 연동 (Phase 8a + 8b) | Picker SDK + drive.file scope + Shortcut + 폴더 자동 생성 |
| D-019 | TableShape 커스텀 도형 | 셀 편집/병합/스타일 — tldraw의 closed type union 우회 필요 |
프로젝트 초기에 가장 큰 결정은 동기화 전략이었다. Yjs CRDT는 이론적으로 완벽하지만 학습 곡선이 가파르고, tldraw와의 통합에 추가 작업이 필요했다.
Claude Code와의 대화에서 나온 결론:
"문제 크기에 맞는 도구를 선택하자. 25명 동시 편집이면 LWW + Smart autosave로 충분하다."
tldraw는 강력한 캔버스 엔진이지만, v5에서 custom shape을 만들 때 문서화되지 않은 동작이 많았다.
겪은 문제들:
| 문제 | 원인 | 해결 |
|---|---|---|
| Custom shape의 closed TLShape union | TypeScript가 새로운 shape 타입을 허용하지 않음 | @ts-expect-error로 우회 |
setEditingShape silent reject | canEditShape 체크를 통과해야 동작 (Editor.mjs:1956) | canEdit=true 조건 명시적 확인 |
| 옛 snapshot의 새 필드 누락 → store 손상 | schema 검증 실패 시 instance state 사라짐 | loadSnapshot 전 pre-migration JSON 변환 |
| native dblclick suppression | tldraw가 더블클릭을 내부적으로 먹음 | onPointerDown에서 직접 카운팅 |
OverflowingToolbar 양쪽 렌더 quirk | boundary item이 두 번 렌더됨 | createPortal(document.body) + position: fixed |
Supabase의 anon 세션은 RLS에서 auth.uid()가 null이라 Postgres Changes를 수신할 수 없었다. 이 문제를 broadcast 채널 + admin client 서버 액션으로 우회했다.
Google Picker SDK에서 setAppId를 누락하면 404가 발생하는데, 이 에러 메시지는 전혀 도움이 되지 않았다. Claude Code와의 대화에서 GOOGLE_CLIENT_ID의 prefix 숫자가 Project Number라는 것을 알아내고 해결했다.
또한 drive.file scope는 sensitive scope라서 production에서 사용하려면 Google의 verification이 필요하다 — 도메인 인증, 데모 영상, 약관까지. 소규모 indie가 감당하기엔 큰 비용이어서, testing 모드 영구 운영 + 수동 등록 요청 workflow(D-021)로 전환했다.
| 서비스 | 무료 티어 한도 | 실 사용량 | 위험도 |
|---|---|---|---|
| Supabase | 500MB DB, 50K MAU | 충분 | 낮음 |
| Vercel | Hobby (100GB bandwidth) | 충분 | 낮음 |
| Google OAuth | testing 모드 100명 | D-021로 관리 | 중간 |
| tldraw | Hobby License | production에서 5초 뒤 캔버스 사라짐 | 높음 |
tldraw의 Hobby License 문제가 가장 큰 미해결 과제다. localhost에서는 정상 동작하지만, production 도메인에서는 라이선스 키 없이 5초 뒤 캔버스가 display:none 처리된다.
상업화 unit economics를 계산한 결과:
결론적으로 포트폴리오 / 개인 사용 / 기술 학습 목적으로 아카이브하기로 결정했다.
이 프로젝트는 다음과 같은 실증적 기록을 남긴다:
기능보다 단위 경제가 의사결정의 본질이다.
기술적으로 무엇이든 만들 수 있다는 자신감은 얻었지만, "만들 수 있다"와 "만들어야 한다"는 다른 질문이었다. $0 예산으로도 실사용 가능한 협업 도구를 만들 수 있지만, 그것을 사업으로 전환하려면 기술 외의 비용(라이선스, 인프라, 영업, 법적 요건)이 기술 비용을 압도한다.
라이브 데모: onuri-studio.vercel.app