Files
joke-app/docs/02-architecture.md
EJClaw f4d63e1544 docs: 온라인 보드게임 사이트 계획서 (공통 14편 + 게임 26종)
- 개요, 조사(BGA·한국 인기 보드게임·UI/UX), 아키텍처, 실시간 프로토콜,
  안정성·보안, 계정, 로비·방, UI/UX, 데이터 모델, 게임 엔진, 테스트,
  배포·운영, 로드맵(M0~M9), 사용자 결정 항목
- 게임별 규칙·엔진 설계·UI·테스트 체크리스트 26종

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 04:18:29 +09:00

6.1 KiB

02. 시스템 아키텍처

1. 핵심 결정

결정 선택 이유
언어 TypeScript (strict) 하나로 서버·클라이언트·게임 규칙 규칙 코드와 타입을 서버/클라이언트가 같이 씀
서버 런타임 Bun 1.3+ (Bun.serve HTTP + WebSocket) 내장 WebSocket pub/sub, 높은 처리량, 내장 SQLite·테스트 러너
HTTP 라우팅 Hono 작고 검증된 라우터, 쿠키·미들웨어
실시간 WebSocket + JSON, 서버 권위(authoritative) 모든 판정은 서버, 클라이언트는 화면만
검증 zod (공용 스키마) 들어오는 모든 메시지를 스키마로 검사
DB SQLite (WAL 모드, bun:sqlite) 단일 서버에서 가장 단순하고 안정적, 백업 쉬움
웹 React 19 + Vite + react-router + zustand 익숙하고 작음, 게임 화면은 lazy import로 분리
스타일 CSS 변수 + CSS Modules, UI 라이브러리 미사용 가벼움, 글자 크기·테마 전환이 쉬움
보드 그리기 SVG(보드·말) + DOM(카드·버튼) 선명함, 확대·접근성·클릭 판정이 쉬움. Canvas는 그림 맞히기에서만
글꼴 Pretendard(오픈 라이선스) 자체 호스팅, 한글 서브셋 가독성, 외부 CDN 의존 없음
배포 Docker Compose: app 컨테이너 + Caddy(HTTPS, 압축, 정적 파일) 단순, 자동 인증서

2. 전체 구조

브라우저 (React)
   │ HTTPS: 정적 파일, /api/* (로그인, 계정, 방 만들기/조회)
   │ WSS:  /ws (방 입장, 행동, 상태 수신, 채팅)
   ▼
Caddy ── 정적 파일(/assets 장기 캐시) ── gzip/zstd 압축
   │ reverse proxy
   ▼
Bun 서버 (단일 프로세스)
 ├─ HTTP API (Hono)        : auth, me, rooms, games 목록
 ├─ WS 게이트웨이           : 연결 인증, 메시지 검증, 속도 제한, 하트비트
 ├─ RoomManager            : 방 생성/조회/정리, 6자리 코드 발급
 │   └─ Room (방 하나)       : 자리, 준비 상태, 채팅, 타이머, GameRunner
 │        └─ GameRunner    : GameDefinition 호출(validate/apply/view), 시퀀스 번호, 저장
 ├─ Persistence            : SQLite (users, sessions, rooms, room_log, results)
 └─ games/*                : 게임별 순수 규칙 모듈 (I/O 없음)

3. 저장소 구조 (bun workspaces 모노레포)

/
├─ apps/
│  ├─ server/src/
│  │  ├─ index.ts            서버 시작, 종료 신호 처리
│  │  ├─ http/               Hono 라우트 (auth, me, rooms)
│  │  ├─ ws/                 게이트웨이, 연결 상태, 속도 제한
│  │  ├─ rooms/              RoomManager, Room, GameRunner, 타이머
│  │  ├─ auth/               디스코드 OAuth, 게스트, 세션
│  │  └─ db/                 스키마, 마이그레이션, 쿼리
│  └─ web/src/
│     ├─ main.tsx, app/      라우터, 레이아웃, 설정(글자 크기·테마)
│     ├─ pages/              홈, 게임 상세, 방(대기실/게임), 계정, 참여(/j/:code)
│     ├─ net/                WS 클라이언트(재연결, 시퀀스), API 클라이언트
│     ├─ components/         버튼, 카드, 모달, 토스트, 타이머 링, 채팅
│     └─ games/<id>/         게임별 화면 (lazy)
├─ packages/
│  ├─ shared/                프로토콜 타입, zod 스키마, 공용 상수(닉네임 규칙 등)
│  ├─ engine/                GameDefinition 타입, 시드 RNG, 카드/덱 유틸, 테스트 도구
│  └─ games/src/<id>/        게임 규칙 (index.ts, rules/*.ts, *.test.ts)
├─ deploy/                   Dockerfile, docker-compose.yml, Caddyfile
└─ docs/

규칙:

  • packages/games는 I/O, 시간(Date.now), Math.random을 쓰지 않는다(린트로 금지). 시간과 난수는 ctx로만 받는다.
  • 웹은 숨겨진 정보가 있는 게임의 규칙 코드를 import하지 않는다(서버가 준 view만 사용). 완전 정보 게임(오목·바둑·체스 등)은 둘 수 있는 곳 표시를 위해 규칙 코드를 import할 수 있다.
  • 서버와 웹은 packages/shared의 타입·스키마로만 통신한다.

4. 방 하나의 처리 흐름

  1. 클라이언트가 {t:"act", room, clientSeq, action} 전송.
  2. 게이트웨이: 세션 확인 → 메시지 크기·속도 제한 → zod 스키마 검사.
  3. Room: 보낸 사람이 그 방의 플레이어인지, 게임이 진행 중인지 확인.
  4. GameRunner: validate() → 실패면 그 사람에게만 {t:"reject", clientSeq, reason}.
  5. 성공이면 apply() → 새 상태 + 이벤트. 먼저 DB에 행동 기록을 쓰고(같은 틱 안의 동기 SQLite 쓰기), 그다음 방 시퀀스 번호를 올리고 브로드캐스트.
  6. 각 접속자에게 view(state, 그 사람)을 계산해 {t:"state", seq, view, events} 전송. 관전자는 view(state, null) 하나를 계산해 공유.
  7. 타이머 재설정(deadline()), 끝났으면 result() 저장.

방 하나의 처리는 자바스크립트 단일 스레드에서 순서대로 실행되므로 같은 방에서 경쟁 상태가 생기지 않는다. 한 행동 처리(검증+적용+저장+전송)는 1~5ms 안에 끝나야 하며, 무거운 계산(예: 마작 점수 계산)도 이 안에 들어오도록 테스트한다.

5. 왜 단일 프로세스인가

  • 목표 규모(동시 2,000명, 방 500개)는 Bun 프로세스 하나로 충분하다(방당 초당 행동 수가 매우 적음).
  • 프로세스가 하나면 방 상태를 메모리에 두고 잠금 없이 처리할 수 있어 버그가 줄고 빠르다.
  • 확장이 필요해지면: 방 코드 기준으로 여러 프로세스에 나누고(코드 → 워커 매핑), Caddy가 /ws?room= 기준으로 라우팅. 이건 로드맵 M9 이후 검토(12-roadmap.md).

6. 의존성 원칙

  • 런타임 의존성은 최소화. 추가 전에 "직접 50줄로 되는가?"를 먼저 따진다.
  • 허용 목록(초기): hono, zod, react, react-dom, react-router, zustand, qrcode(작은 QR 생성기). 체스는 chess.js 사용 여부를 games/chess.md에서 결정.
  • 버전은 lockfile로 고정하고, 업데이트는 테스트 통과 후에만.