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

87 lines
6.1 KiB
Markdown

# 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로 고정하고, 업데이트는 테스트 통과 후에만.