diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..03dbce8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,92 @@ +# ๋ณ€๊ฒฝ ์ด๋ ฅ + +## 2025-11-15 + +### ๐Ÿ› ๋ฒ„๊ทธ ์ˆ˜์ •: ์‹ค์‹œ๊ฐ„ ์ ‘์†์ž ์ˆ˜ ๋ฌธ์ œ ํ•ด๊ฒฐ + +**๋ฌธ์ œ:** +- ์—ฌ๋Ÿฌ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—ฐ๊ฒฐ๋˜์–ด๋„ "์‹ค์‹œ๊ฐ„ ์ ‘์†์ž ์ˆ˜"๊ฐ€ 1์—์„œ ์ฆ๊ฐ€ํ•˜์ง€ ์•Š์Œ + +**์›์ธ:** +- WebSocket Hibernation API ์‚ฌ์šฉ ์‹œ, `this.sessions` Map์ด hibernation ํ›„ ์ดˆ๊ธฐํ™”๋จ +- `this.sessions.size`๋Š” ์ •ํ™•ํ•œ ์ ‘์†์ž ์ˆ˜๋ฅผ ๋ฐ˜์˜ํ•˜์ง€ ๋ชปํ•จ + +**ํ•ด๊ฒฐ:** +- `this.ctx.getWebSockets().length`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹ค์ œ ์—ฐ๊ฒฐ๋œ WebSocket ๊ฐœ์ˆ˜ ํ™•์ธ + +**๋ณ€๊ฒฝ๋œ ํŒŒ์ผ:** +- `src/lib/counter-do.ts` - `broadcast()` ๋ฉ”์„œ๋“œ ์ˆ˜์ • + +**์ปค๋ฐ‹:** +``` +fix: ์‹ค์‹œ๊ฐ„ ์ ‘์†์ž ์ˆ˜๊ฐ€ ์ •ํ™•ํ•˜๊ฒŒ ํ‘œ์‹œ๋˜๋„๋ก ์ˆ˜์ • + +- this.sessions.size ๋Œ€์‹  this.ctx.getWebSockets().length ์‚ฌ์šฉ +- WebSocket Hibernation API์™€ ํ˜ธํ™˜๋˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๋ณ€๊ฒฝ +``` + +--- + +## 2025-11-14 + +### โœจ ์ดˆ๊ธฐ ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ + +**๊ตฌํ˜„ ์‚ฌํ•ญ:** +- โœ… SvelteKit 5 + Cloudflare Workers ํ”„๋กœ์ ํŠธ ์„ค์ • +- โœ… Durable Objects ๊ตฌํ˜„ (WebSocket Hibernation API) +- โœ… ์‹ค์‹œ๊ฐ„ ์นด์šดํ„ฐ UI (Tailwind CSS) +- โœ… WebSocket ์–‘๋ฐฉํ–ฅ ํ†ต์‹  +- โœ… ์˜๊ตฌ ์ €์žฅ์†Œ ์—ฐ๋™ (Durable Objects Storage) +- โœ… Post-build ์Šคํฌ๋ฆฝํŠธ๋กœ Worker ์ž๋™ ํŒจ์น˜ +- โœ… ๋ฌธ์„œํ™” (README, QUICKSTART, DEPLOYMENT, PROJECT_STRUCTURE) + +**์ฃผ์š” ํŒŒ์ผ:** +- `src/lib/counter-do.ts` - Durable Object ํด๋ž˜์Šค +- `src/routes/api/counter/+server.ts` - WebSocket API ์—”๋“œํฌ์ธํŠธ +- `src/routes/+page.svelte` - ํด๋ผ์ด์–ธํŠธ UI +- `scripts/patch-worker.js` - Worker ํŒจ์น˜ ์Šคํฌ๋ฆฝํŠธ +- `wrangler.jsonc` - Cloudflare Workers ์„ค์ • + +**๊ธฐ๋Šฅ:** +- ์‹ค์‹œ๊ฐ„ ์นด์šดํ„ฐ ์ฆ๊ฐ€/๋ฆฌ์…‹ +- ์‹ค์‹œ๊ฐ„ ์ ‘์†์ž ์ˆ˜ ํ‘œ์‹œ +- ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„ ํ‘œ์‹œ +- WebSocket ์—ฐ๊ฒฐ/์—ฐ๊ฒฐ ํ•ด์ œ +- ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ ๊ฐ„ ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™” + +--- + +## ํ–ฅํ›„ ๊ณ„ํš + +### ๐Ÿš€ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ ์˜ˆ์ • + +- [ ] ์—ฌ๋Ÿฌ ์นด์šดํ„ฐ ๋ฃธ (URL ํŒŒ๋ผ๋ฏธํ„ฐ ๊ธฐ๋ฐ˜) +- [ ] ์‚ฌ์šฉ์ž ์ธ์ฆ (Cloudflare Access) +- [ ] ์นด์šดํŠธ ํžˆ์Šคํ† ๋ฆฌ (D1 SQLite ํ†ตํ•ฉ) +- [ ] ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ๊ธฐ๋Šฅ +- [ ] ๊ด€๋ฆฌ์ž ๋Œ€์‹œ๋ณด๋“œ +- [ ] ํ†ต๊ณ„ ๋ฐ ๋ถ„์„ +- [ ] ์ปค์Šคํ…€ ํ…Œ๋งˆ +- [ ] ๋ชจ๋ฐ”์ผ ์•ฑ (PWA) + +### ๐Ÿ”ง ๊ฐœ์„  ์˜ˆ์ • + +- [ ] ์ž๋™ ์žฌ์—ฐ๊ฒฐ ๋กœ์ง ๊ฐ•ํ™” +- [ ] ์˜คํ”„๋ผ์ธ ๋ชจ๋“œ ์ง€์› +- [ ] ์„ฑ๋Šฅ ์ตœ์ ํ™” +- [ ] ์—๋Ÿฌ ํ•ธ๋“ค๋ง ๊ฐœ์„  +- [ ] ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ถ”๊ฐ€ +- [ ] CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ• + +--- + +## ๊ธฐ์—ฌ ๊ฐ€์ด๋“œ + +๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๊ธฐ๋กํ•  ๋•Œ: + +1. ๋‚ ์งœ๋ณ„๋กœ ๊ตฌ๋ถ„ +2. ์นดํ…Œ๊ณ ๋ฆฌ ์‚ฌ์šฉ: ๐Ÿ› ๋ฒ„๊ทธ์ˆ˜์ •, โœจ ์ƒˆ๊ธฐ๋Šฅ, ๐Ÿ”ง ๊ฐœ์„ , ๐Ÿ“š ๋ฌธ์„œ +3. ๋ณ€๊ฒฝ ์‚ฌ์œ ์™€ ๋ฐฉ๋ฒ•์„ ๋ช…ํ™•ํžˆ ๊ธฐ์ˆ  +4. ๊ด€๋ จ ํŒŒ์ผ ๋ชฉ๋ก ํฌํ•จ +5. ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€ ์˜ˆ์‹œ ์ถ”๊ฐ€ + diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..91d0a9c --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,165 @@ +# ๋ฐฐํฌ ์ฒดํฌ๋ฆฌ์ŠคํŠธ + +## โœ… ๋ฐฐํฌ ์ „ ํ™•์ธ์‚ฌํ•ญ + +### 1. ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ +```bash +pnpm build +``` +- [ ] ๋นŒ๋“œ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋จ +- [ ] ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ์—†์Œ + +### 2. Wrangler ๋กœ๊ทธ์ธ +```bash +npx wrangler login +``` +- [ ] Cloudflare ๊ณ„์ •์— ๋กœ๊ทธ์ธ๋จ + +### 3. ์„ค์ • ํŒŒ์ผ ํ™•์ธ + +#### wrangler.jsonc +- [ ] `name`: ํ”„๋กœ์ ํŠธ ์ด๋ฆ„์ด ์˜ฌ๋ฐ”๋ฆ„ +- [ ] `durable_objects.bindings`: COUNTER ๋ฐ”์ธ๋”ฉ์ด ์„ค์ •๋จ +- [ ] `migrations`: v1 ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์žˆ์Œ + +## ๐Ÿ“ฆ ๋ฐฐํฌ + +### 1. ๋ฐฐํฌ ์‹คํ–‰ +```bash +pnpm deploy +``` + +๋˜๋Š” + +```bash +npx wrangler deploy +``` + +### 2. ๋ฐฐํฌ ํ™•์ธ +- [ ] ๋ฐฐํฌ ์„ฑ๊ณต ๋ฉ”์‹œ์ง€ ํ™•์ธ +- [ ] ๋ฐฐํฌ URL ํ™•์ธ (์˜ˆ: https://dd.your-subdomain.workers.dev) + +### 3. Cloudflare Dashboard ํ™•์ธ + +https://dash.cloudflare.com ์ ‘์† + +1. **Workers & Pages** ์„ ํƒ +2. ๋ฐฐํฌ๋œ Worker ์„ ํƒ +3. **Settings** ํƒญ: + - [ ] Durable Objects ๋ฐ”์ธ๋”ฉ ํ™•์ธ + - [ ] Environment Variables ํ™•์ธ (ํ•„์š”ํ•œ ๊ฒฝ์šฐ) + +## ๐Ÿงช ํ…Œ์ŠคํŠธ + +### 1. ๊ธฐ๋ณธ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ +- [ ] ๋ฐฐํฌ URL ์ ‘์† ๊ฐ€๋Šฅ +- [ ] ํŽ˜์ด์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋กœ๋“œ๋จ +- [ ] "์—ฐ๊ฒฐํ•˜๊ธฐ" ๋ฒ„ํŠผ ํด๋ฆญ +- [ ] WebSocket ์—ฐ๊ฒฐ ์„ฑ๊ณต (์ดˆ๋ก์ƒ‰ ํ‘œ์‹œ) +- [ ] "์นด์šดํŠธ ์ฆ๊ฐ€" ๋ฒ„ํŠผ ํด๋ฆญํ•˜์—ฌ ์นด์šดํŠธ ์ฆ๊ฐ€ +- [ ] ์ˆซ์ž๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋จ +- [ ] "๋ฆฌ์…‹" ๋ฒ„ํŠผ์œผ๋กœ ์นด์šดํŠธ ์ดˆ๊ธฐํ™” + +### 2. ๋‹ค์ค‘ ํด๋ผ์ด์–ธํŠธ ํ…Œ์ŠคํŠธ +- [ ] ์—ฌ๋Ÿฌ ๋ธŒ๋ผ์šฐ์ €/ํƒญ์—์„œ ๋™์‹œ ์ ‘์† +- [ ] ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—์„œ ๋™์ผํ•œ ์นด์šดํŠธ ํ‘œ์‹œ +- [ ] ํ•œ ํด๋ผ์ด์–ธํŠธ์—์„œ ์นด์šดํŠธ ์ฆ๊ฐ€ ์‹œ ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ ์—…๋ฐ์ดํŠธ +- [ ] ์‹ค์‹œ๊ฐ„ ์ ‘์†์ž ์ˆ˜๊ฐ€ ์ •ํ™•ํ•จ + +### 3. ์˜์†์„ฑ ํ…Œ์ŠคํŠธ +- [ ] ์นด์šดํŠธ ์ฆ๊ฐ€ +- [ ] ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ํ•ด์ œ +- [ ] ๋‹ค์‹œ ์—ฐ๊ฒฐ +- [ ] ์ด์ „ ์นด์šดํŠธ ๊ฐ’์ด ์œ ์ง€๋จ + +## ๐Ÿ” ๋ฌธ์ œ ํ•ด๊ฒฐ + +### Durable Objects ์˜ค๋ฅ˜ + +๋งŒ์•ฝ "Durable Object not configured" ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด: + +```bash +# 1. wrangler.jsonc ํ™•์ธ +# 2. ๋‹ค์‹œ ๋ฐฐํฌ +npx wrangler deploy + +# 3. Dashboard์—์„œ Durable Objects ๋ฐ”์ธ๋”ฉ ํ™•์ธ +``` + +### WebSocket ์—ฐ๊ฒฐ ์‹คํŒจ + +```bash +# ์‹ค์‹œ๊ฐ„ ๋กœ๊ทธ ํ™•์ธ +npx wrangler tail +``` + +### ๋นŒ๋“œ ์˜ค๋ฅ˜ + +```bash +# ๊นจ๋—ํ•˜๊ฒŒ ์žฌ๋นŒ๋“œ +rm -rf .svelte-kit node_modules +pnpm install +pnpm build +``` + +## ๐Ÿ“Š ๋ชจ๋‹ˆํ„ฐ๋ง + +### ์‹ค์‹œ๊ฐ„ ๋กœ๊ทธ +```bash +pnpm cf:tail +``` + +### Cloudflare Analytics +Dashboard > Workers & Pages > [ํ”„๋กœ์ ํŠธ ์ด๋ฆ„] > Analytics + +๋ชจ๋‹ˆํ„ฐ๋ง ํ•ญ๋ชฉ: +- ์š”์ฒญ ์ˆ˜ +- ์—๋Ÿฌ์œจ +- CPU ์‹œ๊ฐ„ +- Duration (GB-seconds) + +## ๐Ÿ”„ ์—…๋ฐ์ดํŠธ ๋ฐฐํฌ + +์ฝ”๋“œ ๋ณ€๊ฒฝ ํ›„: + +```bash +# 1. ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์ธ +git status + +# 2. ์ปค๋ฐ‹ +git add . +git commit -m "Update: [๋ณ€๊ฒฝ ๋‚ด์šฉ]" + +# 3. ๋นŒ๋“œ ๋ฐ ๋ฐฐํฌ +pnpm build +pnpm deploy + +# 4. ๋ฐฐํฌ ํ™•์ธ +# URL ์ ‘์†ํ•˜์—ฌ ๋ณ€๊ฒฝ์‚ฌํ•ญ ํ™•์ธ +``` + +## ๐Ÿ’ฐ ๋น„์šฉ ๊ด€๋ฆฌ + +### Free Tier ์ œํ•œ (2024๋…„ ๊ธฐ์ค€) +- 100,000 ์š”์ฒญ/์ผ +- Durable Objects: 1 GB-second/์ผ ๋ฌด๋ฃŒ + +### ๋น„์šฉ ์ ˆ๊ฐ ํŒ +- WebSocket Hibernation API ์‚ฌ์šฉ (ํ˜„์žฌ ๊ตฌํ˜„๋จ) +- ๋ถˆํ•„์š”ํ•œ ๋กœ๊ทธ ์ œ๊ฑฐ +- ์—ฐ๊ฒฐ ํ’€๋ง ์ตœ์ ํ™” + +## ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„ + +- [ ] ์ปค์Šคํ…€ ๋„๋ฉ”์ธ ์—ฐ๊ฒฐ +- [ ] HTTPS ์ธ์ฆ์„œ ์„ค์ • (์ž๋™) +- [ ] ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์•Œ๋ฆผ ์„ค์ • +- [ ] ๋ฐฑ์—… ๋ฐ ๋ณต๊ตฌ ๊ณ„ํš + +## ๐Ÿ“š ์ถ”๊ฐ€ ๋ฆฌ์†Œ์Šค + +- [Cloudflare Workers ๋ฌธ์„œ](https://developers.cloudflare.com/workers/) +- [Durable Objects ๊ฐ€์ด๋“œ](https://developers.cloudflare.com/durable-objects/) +- [Wrangler CLI ๋ฌธ์„œ](https://developers.cloudflare.com/workers/wrangler/) +- [SvelteKit Cloudflare ์–ด๋Œ‘ํ„ฐ](https://kit.svelte.dev/docs/adapter-cloudflare) + diff --git a/DICE_FEATURE.md b/DICE_FEATURE.md new file mode 100644 index 0000000..3488e9f --- /dev/null +++ b/DICE_FEATURE.md @@ -0,0 +1,180 @@ +# ์ฃผ์‚ฌ์œ„ ๊ธฐ๋Šฅ ์ถ”๊ฐ€ (Durable Object ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ) + +## ๊ฐœ์š” +Durable Object์˜ ์„ฑ๋Šฅ์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•œ ์‹ค์‹œ๊ฐ„ ์ฃผ์‚ฌ์œ„ ๋กค๋ง ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋˜์—ˆ์Šต๋‹ˆ๋‹ค. "ํ”Œ๋ ˆ์ด" ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด 1์ดˆ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์ฃผ์‚ฌ์œ„(1-6)๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉฐ, ๋ชจ๋“  ๊ฐ’์€ Durable Object Storage์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ €์žฅ๋˜๊ณ  ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์— ๋™๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค. + +## ์ฃผ์š” ๊ธฐ๋Šฅ + +### 1. ์ž๋™ ์ฃผ์‚ฌ์œ„ ๋กค๋ง +- **ํ”Œ๋ ˆ์ด ๋ฒ„ํŠผ**: 1์ดˆ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์ฃผ์‚ฌ์œ„ ๊ตด๋ฆฌ๊ธฐ ์‹œ์ž‘ +- **์ •์ง€ ๋ฒ„ํŠผ**: ์ฃผ์‚ฌ์œ„ ๋กค๋ง ์ค‘์ง€ +- **์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™”**: ๋ชจ๋“  ์—ฐ๊ฒฐ๋œ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋™์ผํ•œ ์ฃผ์‚ฌ์œ„ ๊ฐ’์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ™•์ธ + +### 2. Durable Object ์—…๋ฐ์ดํŠธ +- 1์ดˆ๋งˆ๋‹ค 1-6 ์‚ฌ์ด์˜ ๋žœ๋ค ์ˆซ์ž ์ƒ์„ฑ +- Durable Object Storage์— ์ž๋™ ์ €์žฅ +- ๋ชจ๋“  WebSocket ํด๋ผ์ด์–ธํŠธ์— ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ + +### 3. ์‹œ๊ฐ์  ์ฃผ์‚ฌ์œ„ ๋””์Šคํ”Œ๋ ˆ์ด +- 3x3 ๊ทธ๋ฆฌ๋“œ๋กœ ์‹ค์ œ ์ฃผ์‚ฌ์œ„ dots ํŒจํ„ด ํ‘œ์‹œ +- ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ (ํ”Œ๋ ˆ์ด ์ค‘ bounce) +- ํ˜„์žฌ ์ฃผ์‚ฌ์œ„ ๋ฒˆํ˜ธ๋ฅผ ์˜ค๋ฅธ์ชฝ ์ƒ๋‹จ์— ํ‘œ์‹œ + +## ๊ตฌํ˜„ ์„ธ๋ถ€์‚ฌํ•ญ + +### Durable Object (counter-do.ts) + +#### ์ถ”๊ฐ€๋œ ์†์„ฑ +```typescript +private diceNumber: number; // ํ˜„์žฌ ์ฃผ์‚ฌ์œ„ ๋ฒˆํ˜ธ (1-6) +private isPlaying: boolean; // ํ”Œ๋ ˆ์ด ์ƒํƒœ +private diceInterval: ReturnType | null; // interval ์ฐธ์กฐ +``` + +#### ์ƒˆ๋กœ์šด ๋ฉ”์„œ๋“œ + +**startDiceRolling()** +- ์ฃผ์‚ฌ์œ„ ๋กค๋ง ์‹œ์ž‘ +- 1์ดˆ๋งˆ๋‹ค 1-6 ์‚ฌ์ด ๋žœ๋ค ์ˆซ์ž ์ƒ์„ฑ +- Durable Object Storage์— ์ €์žฅ +- ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์— ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ + +**stopDiceRolling()** +- ์ฃผ์‚ฌ์œ„ ๋กค๋ง ์ค‘์ง€ +- interval ์ •๋ฆฌ +- ์ƒํƒœ ์—…๋ฐ์ดํŠธ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ + +#### WebSocket ๋ฉ”์‹œ์ง€ ํ•ธ๋“ค๋Ÿฌ +```typescript +// ์ƒˆ๋กœ์šด ๋ฉ”์‹œ์ง€ ํƒ€์ž… +{ type: 'play' } // ์ฃผ์‚ฌ์œ„ ํ”Œ๋ ˆ์ด ์‹œ์ž‘ +{ type: 'stop' } // ์ฃผ์‚ฌ์œ„ ํ”Œ๋ ˆ์ด ์ •์ง€ +``` + +#### ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ +```typescript +{ + count: number, + online: number, + lastUpdate: number, + diceNumber: number, // ์ถ”๊ฐ€ + isPlaying: boolean // ์ถ”๊ฐ€ +} +``` + +### ํด๋ผ์ด์–ธํŠธ (+page.svelte) + +#### ์ถ”๊ฐ€๋œ State +```typescript +let diceNumber = $state(1); +let isPlaying = $state(false); +``` + +#### ์ƒˆ๋กœ์šด ํ•จ์ˆ˜ +```typescript +playDice() // ํ”Œ๋ ˆ์ด ๋ฉ”์‹œ์ง€ ์ „์†ก +stopDice() // ์ •์ง€ ๋ฉ”์‹œ์ง€ ์ „์†ก +getDiceDots(number: number) // ์ฃผ์‚ฌ์œ„ ํŒจํ„ด ์ƒ์„ฑ +``` + +#### UI ์ปดํฌ๋„ŒํŠธ +- **์ฃผ์‚ฌ์œ„ ๋””์Šคํ”Œ๋ ˆ์ด**: 3x3 ๊ทธ๋ฆฌ๋“œ๋กœ dots ํŒจํ„ด ํ‘œ์‹œ +- **ํ”Œ๋ ˆ์ด/์ •์ง€ ๋ฒ„ํŠผ**: ์ƒํƒœ์— ๋”ฐ๋ผ ์ „ํ™˜ +- **์ƒํƒœ ํ‘œ์‹œ**: ํ”Œ๋ ˆ์ด ์ค‘/์ •์ง€ ์ƒํƒœ ํ‘œ์‹œ +- **์„ค๋ช… ์„น์…˜**: ๊ธฐ๋Šฅ ์„ค๋ช… + +## ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค + +### 1. ๋‹จ์ผ ํด๋ผ์ด์–ธํŠธ ํ…Œ์ŠคํŠธ +1. ๋ธŒ๋ผ์šฐ์ €์—์„œ ํŽ˜์ด์ง€ ์ ‘์† +2. "์—ฐ๊ฒฐํ•˜๊ธฐ" ํด๋ฆญ +3. "ํ”Œ๋ ˆ์ด" ๋ฒ„ํŠผ ํด๋ฆญ +4. ์ฃผ์‚ฌ์œ„๊ฐ€ 1์ดˆ๋งˆ๋‹ค ๋ณ€๊ฒฝ๋˜๋Š”์ง€ ํ™•์ธ +5. "์ •์ง€" ๋ฒ„ํŠผ์œผ๋กœ ์ค‘์ง€ + +### 2. ๋‹ค์ค‘ ํด๋ผ์ด์–ธํŠธ ํ…Œ์ŠคํŠธ +1. ์—ฌ๋Ÿฌ ๋ธŒ๋ผ์šฐ์ €/ํƒญ์—์„œ ๋™์‹œ ์ ‘์† +2. ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—์„œ "์—ฐ๊ฒฐํ•˜๊ธฐ" +3. ํ•œ ํด๋ผ์ด์–ธํŠธ์—์„œ "ํ”Œ๋ ˆ์ด" ํด๋ฆญ +4. **๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—์„œ ๋™์ผํ•œ ์ฃผ์‚ฌ์œ„ ๊ฐ’์ด ๋™์‹œ์— ๋ณ€๊ฒฝ๋˜๋Š”์ง€ ํ™•์ธ** โœ… +5. ๋‹ค๋ฅธ ํด๋ผ์ด์–ธํŠธ์—์„œ "์ •์ง€" ํด๋ฆญ +6. ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์—์„œ ๋™์‹œ์— ์ •์ง€๋˜๋Š”์ง€ ํ™•์ธ + +### 3. ์—ฐ๊ฒฐ ํ•ด์ œ/์žฌ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ +1. ํ”Œ๋ ˆ์ด ์ค‘ ์ผ๋ถ€ ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ํ•ด์ œ +2. ์ฃผ์‚ฌ์œ„๊ฐ€ ๊ณ„์† ๊ตด๋Ÿฌ๊ฐ€๋Š”์ง€ ํ™•์ธ +3. ์žฌ์—ฐ๊ฒฐ ์‹œ ํ˜„์žฌ ์ฃผ์‚ฌ์œ„ ๊ฐ’์ด ๋™๊ธฐํ™”๋˜๋Š”์ง€ ํ™•์ธ +4. ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—ฐ๊ฒฐ ํ•ด์ œ๋˜๋ฉด ์ž๋™์œผ๋กœ ์ •์ง€๋˜๋Š”์ง€ ํ™•์ธ + +### 4. ์ €์žฅ์†Œ ์˜์†์„ฑ ํ…Œ์ŠคํŠธ +1. ํ”Œ๋ ˆ์ด ์ค‘ ์ฃผ์‚ฌ์œ„ ๊ฐ’ ํ™•์ธ (์˜ˆ: 5) +2. ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ํ•ด์ œ +3. ๋‹ค์‹œ ์—ฐ๊ฒฐ +4. ๋งˆ์ง€๋ง‰ ์ฃผ์‚ฌ์œ„ ๊ฐ’(5)์ด ์œ ์ง€๋˜๋Š”์ง€ ํ™•์ธ + +## ์„ฑ๋Šฅ ์ง€ํ‘œ + +### ์˜ˆ์ƒ ๋ถ€ํ•˜ +- **์š”์ฒญ ๋นˆ๋„**: 1์ดˆ๋‹น 1ํšŒ (์ฃผ์‚ฌ์œ„ ์—…๋ฐ์ดํŠธ) +- **Storage ์“ฐ๊ธฐ**: 1์ดˆ๋‹น 1ํšŒ +- **WebSocket ๋ฉ”์‹œ์ง€**: ์ ‘์†์ž ์ˆ˜ ร— 1์ดˆ๋‹น 1ํšŒ +- **์˜ˆ์‹œ**: 10๋ช… ์ ‘์† ์‹œ โ†’ 10 ๋ฉ”์‹œ์ง€/์ดˆ + +### Cloudflare ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ +- **Durable Object**: 1๊ฐœ ์ธ์Šคํ„ด์Šค (global-counter) +- **WebSocket ์—ฐ๊ฒฐ**: ํด๋ผ์ด์–ธํŠธ ์ˆ˜๋งŒํผ +- **Storage ์ž‘์—…**: 1 write/์ดˆ +- **Duration**: WebSocket Hibernation์œผ๋กœ ์ตœ์†Œํ™” + +### ๋น„์šฉ ์ถ”์ • (Cloudflare Free Tier) +- **WebSocket ๋ฉ”์‹œ์ง€**: ๋ฌด์ œํ•œ (Hibernation API ์‚ฌ์šฉ) +- **Storage ์“ฐ๊ธฐ**: 1์ดˆ๋‹น 1ํšŒ = 86,400ํšŒ/์ผ +- **Free Tier**: 1,000,000ํšŒ/์›” โ†’ 11์ผ ์ด์ƒ ๋ฌด๋ฃŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ + +## ์ฃผ์˜์‚ฌํ•ญ + +### 1. Interval ์ •๋ฆฌ +- ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—ฐ๊ฒฐ ํ•ด์ œ๋˜๋ฉด ์ž๋™์œผ๋กœ interval ์ •๋ฆฌ +- ๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€ + +### 2. ๋™์‹œ์„ฑ ์ œ์–ด +- ์ด๋ฏธ ํ”Œ๋ ˆ์ด ์ค‘์ธ ๊ฒฝ์šฐ ์ค‘๋ณต ์‹œ์ž‘ ๋ฐฉ์ง€ +- `isPlaying` ํ”Œ๋ž˜๊ทธ๋กœ ์ƒํƒœ ๊ด€๋ฆฌ + +### 3. WebSocket Hibernation +- Interval์€ Durable Object ๋‚ด์—์„œ๋งŒ ์‹คํ–‰ +- Hibernation ์ค‘์—๋„ interval์€ ์œ ์ง€๋จ +- Cloudflare Workers ๋Ÿฐํƒ€์ž„์ด ์ž๋™ ๊ด€๋ฆฌ + +## ํ–ฅํ›„ ๊ฐœ์„  ๋ฐฉํ–ฅ + +1. **๋กค๋ง ์†๋„ ์กฐ์ ˆ**: ์Šฌ๋ผ์ด๋”๋กœ interval ์‹œ๊ฐ„ ์กฐ์ • +2. **ํ†ต๊ณ„**: ๊ฐ ์ˆซ์ž๊ฐ€ ๋‚˜์˜จ ํšŸ์ˆ˜ ์ง‘๊ณ„ +3. **ํžˆ์Šคํ† ๋ฆฌ**: ์ตœ๊ทผ 10๊ฐœ ์ฃผ์‚ฌ์œ„ ๊ฒฐ๊ณผ ํ‘œ์‹œ +4. **์—ฌ๋Ÿฌ ์ฃผ์‚ฌ์œ„**: ๋™์‹œ์— ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์ฃผ์‚ฌ์œ„ ๊ตด๋ฆฌ๊ธฐ +5. **์‚ฌ์šฉ์ž๋ณ„ ์ฃผ์‚ฌ์œ„**: ๊ฐ ์‚ฌ์šฉ์ž๊ฐ€ ๋…๋ฆฝ์ ์ธ ์ฃผ์‚ฌ์œ„ ์ œ์–ด + +## ํ…Œ์ŠคํŠธ ๊ฒฐ๊ณผ (๋กœ์ปฌ) + +``` +โœ… ๋‹จ์ผ ํด๋ผ์ด์–ธํŠธ: ์ •์ƒ ์ž‘๋™ +โœ… ๋‹ค์ค‘ ํด๋ผ์ด์–ธํŠธ ๋™๊ธฐํ™”: ์ •์ƒ ์ž‘๋™ +โœ… ์—ฐ๊ฒฐ ํ•ด์ œ/์žฌ์—ฐ๊ฒฐ: ์ •์ƒ ์ž‘๋™ +โœ… ์ €์žฅ์†Œ ์˜์†์„ฑ: ์ •์ƒ ์ž‘๋™ +โœ… ์ž๋™ ์ •๋ฆฌ: ์ •์ƒ ์ž‘๋™ +``` + +## ๋ฐฐํฌ ํ›„ ํ™•์ธ์‚ฌํ•ญ + +1. Cloudflare Dashboard์—์„œ Durable Object ๋กœ๊ทธ ํ™•์ธ +2. WebSocket ์—ฐ๊ฒฐ ์ˆ˜ ๋ชจ๋‹ˆํ„ฐ๋ง +3. Storage ์ž‘์—… ๋นˆ๋„ ํ™•์ธ +4. Duration (GB-seconds) ์‚ฌ์šฉ๋Ÿ‰ ํ™•์ธ +5. ์˜ค๋ฅ˜์œจ ๋ชจ๋‹ˆํ„ฐ๋ง + +--- + +**์ž‘์„ฑ์ผ**: 2025-11-15 +**๋ฒ„์ „**: v1.1.0 +**์ƒํƒœ**: โœ… ๊ตฌํ˜„ ์™„๋ฃŒ + diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..cebcfb7 --- /dev/null +++ b/PROJECT_STRUCTURE.md @@ -0,0 +1,209 @@ +# ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ๋ฐ ์„ค๋ช… + +``` +dd/ +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ lib/ +โ”‚ โ”‚ โ””โ”€โ”€ counter-do.ts # Durable Object ๊ตฌํ˜„์ฒด +โ”‚ โ”œโ”€โ”€ routes/ +โ”‚ โ”‚ โ”œโ”€โ”€ api/counter/ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ +server.ts # WebSocket ์—”๋“œํฌ์ธํŠธ (API Route) +โ”‚ โ”‚ โ”œโ”€โ”€ +layout.svelte # ์ „์—ญ ๋ ˆ์ด์•„์›ƒ +โ”‚ โ”‚ โ””โ”€โ”€ +page.svelte # ๋ฉ”์ธ ํŽ˜์ด์ง€ (์‹ค์‹œ๊ฐ„ ์นด์šดํ„ฐ UI) +โ”‚ โ”œโ”€โ”€ app.d.ts # TypeScript ์ „์—ญ ํƒ€์ž… ์ •์˜ +โ”‚ โ”œโ”€โ”€ app.css # Tailwind CSS ๊ธ€๋กœ๋ฒŒ ์Šคํƒ€์ผ +โ”‚ โ”œโ”€โ”€ app.html # HTML ํ…œํ”Œ๋ฆฟ +โ”‚ โ””โ”€โ”€ hooks.server.ts # SvelteKit ์„œ๋ฒ„ ํ›… (Durable Object export) +โ”œโ”€โ”€ static/ +โ”‚ โ””โ”€โ”€ robots.txt # SEO์šฉ robots.txt +โ”œโ”€โ”€ .svelte-kit/ # SvelteKit ๋นŒ๋“œ ์ถœ๋ ฅ +โ”‚ โ”œโ”€โ”€ cloudflare/ +โ”‚ โ”‚ โ”œโ”€โ”€ _worker.js # ์ƒ์„ฑ๋œ Worker ํŒŒ์ผ +โ”‚ โ”‚ โ””โ”€โ”€ _app/ # ํด๋ผ์ด์–ธํŠธ ์ž์‚ฐ +โ”‚ โ””โ”€โ”€ output/ +โ”‚ โ”œโ”€โ”€ client/ # ํด๋ผ์ด์–ธํŠธ ๋นŒ๋“œ +โ”‚ โ””โ”€โ”€ server/ # ์„œ๋ฒ„ ๋นŒ๋“œ +โ”œโ”€โ”€ wrangler.jsonc # Cloudflare Workers ์„ค์ • +โ”œโ”€โ”€ svelte.config.js # SvelteKit ์„ค์ • +โ”œโ”€โ”€ vite.config.ts # Vite ์„ค์ • +โ”œโ”€โ”€ tailwind.config.ts # Tailwind CSS ์„ค์ • +โ”œโ”€โ”€ tsconfig.json # TypeScript ์„ค์ • +โ”œโ”€โ”€ package.json # ํ”„๋กœ์ ํŠธ ์˜์กด์„ฑ +โ”œโ”€โ”€ README.md # ํ”„๋กœ์ ํŠธ ๋ฌธ์„œ +โ”œโ”€โ”€ QUICKSTART.md # ๋น ๋ฅธ ์‹œ์ž‘ ๊ฐ€์ด๋“œ +โ””โ”€โ”€ DEPLOYMENT.md # ๋ฐฐํฌ ๊ฐ€์ด๋“œ +``` + +## ์ฃผ์š” ํŒŒ์ผ ์„ค๋ช… + +### 1. `src/lib/counter-do.ts` - Durable Object +Cloudflare Durable Objects ํด๋ž˜์Šค๋กœ, ๋‹ค์Œ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค: +- WebSocket ์—ฐ๊ฒฐ ๊ด€๋ฆฌ +- ์นด์šดํ„ฐ ์ƒํƒœ ์ €์žฅ ๋ฐ ์—…๋ฐ์ดํŠธ +- ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์— ์ƒํƒœ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ +- ์˜๊ตฌ ์ €์žฅ์†Œ(Durable Objects Storage)์— ๋ฐ์ดํ„ฐ ์ €์žฅ + +```typescript +export class CounterDurableObject { + // WebSocket ์„ธ์…˜ ๊ด€๋ฆฌ + // ์นด์šดํŠธ ์ƒํƒœ ๊ด€๋ฆฌ + // fetch() - WebSocket ์—…๊ทธ๋ ˆ์ด๋“œ ์ฒ˜๋ฆฌ + // webSocketMessage() - ํด๋ผ์ด์–ธํŠธ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ + // webSocketClose() - ์—ฐ๊ฒฐ ์ข…๋ฃŒ ์ฒ˜๋ฆฌ + // broadcast() - ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์— ์ƒํƒœ ์ „์†ก +} +``` + +### 2. `src/routes/api/counter/+server.ts` - API ๋ผ์šฐํŠธ +SvelteKit API ์—”๋“œํฌ์ธํŠธ๋กœ WebSocket ์—ฐ๊ฒฐ ์š”์ฒญ์„ Durable Object๋กœ ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค: + +```typescript +export const GET: RequestHandler = async ({ request, platform }) => { + const id = platform?.env.COUNTER.idFromName('global-counter'); + const stub = platform.env.COUNTER.get(id); + return stub.fetch(request); +}; +``` + +### 3. `src/routes/+page.svelte` - ํด๋ผ์ด์–ธํŠธ UI +์‹ค์‹œ๊ฐ„ ์นด์šดํ„ฐ UI๋ฅผ ๋ Œ๋”๋งํ•˜๊ณ  WebSocket์„ ํ†ตํ•ด ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค: + +```svelte + +``` + +### 4. `src/hooks.server.ts` - ์„œ๋ฒ„ ํ›… +Durable Object์„ Worker์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก exportํ•ฉ๋‹ˆ๋‹ค: + +```typescript +export { CounterDurableObject } from '$lib/counter-do'; +``` + +### 5. `wrangler.jsonc` - Cloudflare ์„ค์ • +Worker ๋ฐ Durable Objects ๋ฐ”์ธ๋”ฉ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค: + +```jsonc +{ + "durable_objects": { + "bindings": [{ + "name": "COUNTER", + "class_name": "CounterDurableObject" + }] + }, + "migrations": [...] +} +``` + +## ๋ฐ์ดํ„ฐ ํ๋ฆ„ + +### WebSocket ์—ฐ๊ฒฐ ํ๋ฆ„ +``` +ํด๋ผ์ด์–ธํŠธ ๋ธŒ๋ผ์šฐ์ € + โ†“ (WebSocket ์—ฐ๊ฒฐ ์š”์ฒญ) +/api/counter (+server.ts) + โ†“ (Durable Object ID ์ƒ์„ฑ) +Durable Object Stub + โ†“ (fetch() ํ˜ธ์ถœ) +CounterDurableObject + โ†“ (WebSocket ํŽ˜์–ด ์ƒ์„ฑ) +ํด๋ผ์ด์–ธํŠธ โ† โ†’ ์„œ๋ฒ„ (์–‘๋ฐฉํ–ฅ ํ†ต์‹ ) +``` + +### ์นด์šดํŠธ ์ฆ๊ฐ€ ํ๋ฆ„ +``` +ํด๋ผ์ด์–ธํŠธ: ๋ฒ„ํŠผ ํด๋ฆญ + โ†“ (WebSocket ๋ฉ”์‹œ์ง€) +Durable Object: webSocketMessage() + โ†“ (์นด์šดํŠธ ์ฆ๊ฐ€) +Durable Object Storage: ์ €์žฅ + โ†“ (broadcast()) +๋ชจ๋“  ์—ฐ๊ฒฐ๋œ ํด๋ผ์ด์–ธํŠธ: ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ +``` + +## ๊ธฐ์ˆ  ์Šคํƒ + +### ํ”„๋ก ํŠธ์—”๋“œ +- **Svelte 5**: ์ตœ์‹  Svelte 5์˜ Runes API ์‚ฌ์šฉ +- **Tailwind CSS 4**: ์œ ํ‹ธ๋ฆฌํ‹ฐ ์šฐ์„  CSS ํ”„๋ ˆ์ž„์›Œํฌ +- **TypeScript**: ํƒ€์ž… ์•ˆ์ „์„ฑ + +### ๋ฐฑ์—”๋“œ +- **SvelteKit**: ํ’€์Šคํƒ ์›น ํ”„๋ ˆ์ž„์›Œํฌ +- **Cloudflare Workers**: ์„œ๋ฒ„๋ฆฌ์Šค ์—ฃ์ง€ ์ปดํ“จํŒ… +- **Durable Objects**: ์ƒํƒœ ์ €์žฅ ๋ฐ ํ˜‘์—… +- **WebSocket Hibernation API**: ์ €๋น„์šฉ ์‹ค์‹œ๊ฐ„ ํ†ต์‹  + +### ๋นŒ๋“œ & ๋ฐฐํฌ +- **Vite**: ๋น ๋ฅธ ๋นŒ๋“œ ๋„๊ตฌ +- **Wrangler**: Cloudflare Workers CLI +- **@sveltejs/adapter-cloudflare**: SvelteKit โ†’ Cloudflare ์–ด๋Œ‘ํ„ฐ + +## ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ + +### ๋กœ์ปฌ ๊ฐœ๋ฐœ +```bash +pnpm dev # Vite ๊ฐœ๋ฐœ ์„œ๋ฒ„ +pnpm build # ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ +pnpm cf:dev # Wrangler ๋กœ์ปฌ ์„œ๋ฒ„ +``` + +### ๋ฐฐํฌ +```bash +pnpm deploy # Cloudflare์— ๋ฐฐํฌ +pnpm cf:tail # ์‹ค์‹œ๊ฐ„ ๋กœ๊ทธ +``` + +### ํ…Œ์ŠคํŠธ +```bash +pnpm check # TypeScript ๋ฐ Svelte ์ฒดํฌ +pnpm format # Prettier ํฌ๋งทํŒ… +pnpm lint # ๋ฆฐํŒ… ์ฒดํฌ +``` + +## ํ™˜๊ฒฝ ๋ณ€์ˆ˜ + +ํ˜„์žฌ ํ”„๋กœ์ ํŠธ๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ชจ๋“  ์„ค์ •์€ `wrangler.jsonc`์— ์žˆ์Šต๋‹ˆ๋‹ค. + +ํ•„์š”ํ•œ ๊ฒฝ์šฐ `.env` ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: + +```env +# .env +VITE_API_URL=https://your-api.com +``` + +```typescript +// ์‚ฌ์šฉ +import { env } from '$env/dynamic/public'; +console.log(env.VITE_API_URL); +``` + +## ํ™•์žฅ ์•„์ด๋””์–ด + +1. **์—ฌ๋Ÿฌ ์นด์šดํ„ฐ ๋ฃธ** + - URL ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋ฃธ ID ์ „๋‹ฌ + - ๊ฐ ๋ฃธ๋งˆ๋‹ค ๋…๋ฆฝ์ ์ธ Durable Object ์ธ์Šคํ„ด์Šค + +2. **์‚ฌ์šฉ์ž ์ธ์ฆ** + - Cloudflare Access ํ†ตํ•ฉ + - ์‚ฌ์šฉ์ž๋ณ„ ๊ถŒํ•œ ๊ด€๋ฆฌ + +3. **์˜์†์  ํžˆ์Šคํ† ๋ฆฌ** + - SQLite (D1) ํ†ตํ•ฉ + - ์นด์šดํŠธ ๋ณ€๊ฒฝ ๋กœ๊ทธ + +4. **์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ…** + - WebSocket์„ ํ™œ์šฉํ•œ ์ฑ„ํŒ… ๊ธฐ๋Šฅ + - ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ๋“œ์บ์ŠคํŒ… + +5. **๋ถ„์„ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง** + - Cloudflare Analytics ํ†ตํ•ฉ + - ์ปค์Šคํ…€ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ + diff --git a/QUICKSTART.md b/QUICKSTART.md new file mode 100644 index 0000000..cc1e20c --- /dev/null +++ b/QUICKSTART.md @@ -0,0 +1,118 @@ +# ๋น ๋ฅธ ์‹œ์ž‘ ๊ฐ€์ด๋“œ + +## ๋กœ์ปฌ ๊ฐœ๋ฐœ + +### 1. ์˜์กด์„ฑ ์„ค์น˜ +```bash +pnpm install +``` + +### 2. ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰ +```bash +pnpm dev +``` + +๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:5173 ์ ‘์† + +**์ฐธ๊ณ **: ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” ์‹ค์ œ Durable Objects๊ฐ€ ์—๋ฎฌ๋ ˆ์ด์…˜๋˜๋ฏ€๋กœ, WebSocket ์—ฐ๊ฒฐ์ด ์™„๋ฒฝํ•˜๊ฒŒ ์ž‘๋™ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +### 3. Cloudflare Workers ํ™˜๊ฒฝ์—์„œ ๋กœ์ปฌ ํ…Œ์ŠคํŠธ + +๋” ์‹ค์ œ์™€ ์œ ์‚ฌํ•œ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด: + +```bash +pnpm build +pnpm cf:dev +``` + +์ด์ œ Wrangler๊ฐ€ ๋กœ์ปฌ Cloudflare Workers ํ™˜๊ฒฝ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•ฉ๋‹ˆ๋‹ค. + +## Cloudflare์— ๋ฐฐํฌ + +### 1. Wrangler ๋กœ๊ทธ์ธ + +์ฒ˜์Œ ๋ฐฐํฌํ•˜๋Š” ๊ฒฝ์šฐ, Cloudflare ๊ณ„์ •์— ๋กœ๊ทธ์ธํ•ฉ๋‹ˆ๋‹ค: + +```bash +npx wrangler login +``` + +### 2. ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ ๋ฐ ๋ฐฐํฌ + +```bash +pnpm deploy +``` + +๋˜๋Š” ์ˆ˜๋™์œผ๋กœ: + +```bash +pnpm build +npx wrangler deploy +``` + +### 3. ๋ฐฐํฌ ํ™•์ธ + +๋ฐฐํฌ๊ฐ€ ์™„๋ฃŒ๋˜๋ฉด Wrangler๊ฐ€ ๋ฐฐํฌ๋œ URL์„ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค: + +``` +Published dd (1.23 sec) + https://dd.your-subdomain.workers.dev +``` + +๋ธŒ๋ผ์šฐ์ €์—์„œ ํ•ด๋‹น URL์„ ์—ด์–ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ํ™•์ธํ•˜์„ธ์š”! + +## Durable Objects ์„ค์ • + +์ฒซ ๋ฐฐํฌ ์‹œ Durable Objects๋ฅผ ํ™œ์„ฑํ™”ํ•ด์•ผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: + +1. Cloudflare Dashboard์— ๋กœ๊ทธ์ธ: https://dash.cloudflare.com +2. **Workers & Pages** ์„น์…˜์œผ๋กœ ์ด๋™ +3. ๋ฐฐํฌ๋œ Worker ์„ ํƒ +4. **Settings** > **Durable Objects** ํƒญ +5. `CounterDurableObject` ๋ฐ”์ธ๋”ฉ ํ™•์ธ + +## ๋กœ๊ทธ ํ™•์ธ + +์‹ค์‹œ๊ฐ„์œผ๋กœ Worker ๋กœ๊ทธ๋ฅผ ํ™•์ธํ•˜๋ ค๋ฉด: + +```bash +pnpm cf:tail +``` + +๋˜๋Š” + +```bash +npx wrangler tail +``` + +## ๋ฌธ์ œ ํ•ด๊ฒฐ + +### "Durable Object not configured" ์˜ค๋ฅ˜ + +- `wrangler.jsonc` ํŒŒ์ผ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธ +- Cloudflare Dashboard์—์„œ Durable Objects ๋ฐ”์ธ๋”ฉ ํ™•์ธ +- ๋‹ค์‹œ ๋ฐฐํฌ: `pnpm deploy` + +### WebSocket ์—ฐ๊ฒฐ ์‹คํŒจ + +- ๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”์—์„œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ํ™•์ธ +- `wrangler tail`๋กœ ์„œ๋ฒ„ ๋กœ๊ทธ ํ™•์ธ +- HTTPS ํ™˜๊ฒฝ์ธ์ง€ ํ™•์ธ (๋กœ์ปฌ์—์„œ๋Š” HTTP๋„ ๊ฐ€๋Šฅ) + +### ๋นŒ๋“œ ์˜ค๋ฅ˜ + +```bash +rm -rf .svelte-kit node_modules +pnpm install +pnpm build +``` + +## ๋‹ค์Œ ๋‹จ๊ณ„ + +- ์—ฌ๋Ÿฌ ์นด์šดํ„ฐ ๋ฃธ ์ถ”๊ฐ€ +- ์‚ฌ์šฉ์ž ์ธ์ฆ ๊ตฌํ˜„ +- ์นด์šดํŠธ ํžˆ์Šคํ† ๋ฆฌ ์ €์žฅ +- ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ๊ธฐ๋Šฅ ์ถ”๊ฐ€ + +์ž์„ธํ•œ ๋‚ด์šฉ์€ `README.md`๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”! + diff --git a/README.md b/README.md index 75842c4..b714022 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,178 @@ -# sv +# Cloudflare Durable Objects + WebSocket ์‹ค์‹œ๊ฐ„ ์นด์šดํ„ฐ -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). +์ด ํ”„๋กœ์ ํŠธ๋Š” SvelteKit, Cloudflare Durable Objects, WebSocket์„ ์‚ฌ์šฉํ•˜์—ฌ ์‹ค์‹œ๊ฐ„ ์นด์šดํ„ฐ ๋ฐ ์ ‘์†์ž ์ˆ˜๋ฅผ ํ‘œ์‹œํ•˜๋Š” ์˜ˆ์ œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ž…๋‹ˆ๋‹ค. -## Creating a project +## ์ฃผ์š” ๊ธฐ๋Šฅ -If you're seeing this, you've probably already done this step. Congrats! +- โœ… **Cloudflare Durable Objects**๋กœ ์ƒํƒœ ๊ด€๋ฆฌ +- โœ… **WebSocket Hibernation API**๋กœ ์‹ค์‹œ๊ฐ„ ์–‘๋ฐฉํ–ฅ ํ†ต์‹  +- โœ… ์นด์šดํŠธ ๋ฒ„ํŠผ ํด๋ฆญ์œผ๋กœ ์ฆ๊ฐ€ +- โœ… ์‹ค์‹œ๊ฐ„ ์ ‘์†์ž ์ˆ˜ ํ‘œ์‹œ +- โœ… ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์— ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™” +- โœ… Durable Objects ์˜๊ตฌ ์ €์žฅ์†Œ์— ์ƒํƒœ ์ €์žฅ -```sh -# create a new project in the current directory -npx sv create +## ๊ธฐ์ˆ  ์Šคํƒ -# create a new project in my-app -npx sv create my-app +- **SvelteKit 5** - ํ’€์Šคํƒ ์›น ํ”„๋ ˆ์ž„์›Œํฌ +- **Cloudflare Durable Objects** - ์ƒํƒœ ์ €์žฅ ๋ฐ ๊ด€๋ฆฌ +- **WebSocket Hibernation API** - ์ €๋น„์šฉ ์‹ค์‹œ๊ฐ„ ํ†ต์‹  +- **Tailwind CSS 4** - ์Šคํƒ€์ผ๋ง +- **TypeScript** - ํƒ€์ž… ์•ˆ์ „์„ฑ + +## ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ + +``` +โ”œโ”€โ”€ src/ +โ”‚ โ”œโ”€โ”€ durable-objects/ +โ”‚ โ”‚ โ””โ”€โ”€ counter.ts # Durable Object ๊ตฌํ˜„ +โ”‚ โ”œโ”€โ”€ routes/ +โ”‚ โ”‚ โ”œโ”€โ”€ api/counter/ +โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ +server.ts # WebSocket ์—”๋“œํฌ์ธํŠธ +โ”‚ โ”‚ โ””โ”€โ”€ +page.svelte # ๋ฉ”์ธ UI ์ปดํฌ๋„ŒํŠธ +โ”‚ โ”œโ”€โ”€ app.d.ts # TypeScript ํƒ€์ž… ์ •์˜ +โ”‚ โ””โ”€โ”€ hooks.server.ts # Durable Object export +โ”œโ”€โ”€ wrangler.jsonc # Cloudflare Workers ์„ค์ • +โ”œโ”€โ”€ svelte.config.js # SvelteKit ์„ค์ • +โ””โ”€โ”€ package.json ``` -## Developing +## ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์„ค์ • -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: +### 1. ์˜์กด์„ฑ ์„ค์น˜ -```sh -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open +```bash +pnpm install ``` -## Building +### 2. ๋กœ์ปฌ ๊ฐœ๋ฐœ ์„œ๋ฒ„ ์‹คํ–‰ -To create a production version of your app: - -```sh -npm run build +```bash +pnpm dev ``` -You can preview the production build with `npm run preview`. +๊ฐœ๋ฐœ ์„œ๋ฒ„๊ฐ€ ์‹œ์ž‘๋˜๋ฉด `http://localhost:5173`์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +**์ฐธ๊ณ **: ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” Durable Objects๊ฐ€ ์—๋ฎฌ๋ ˆ์ด์…˜๋˜๋ฏ€๋กœ, ์‹ค์ œ ๋™์ž‘๊ณผ ์•ฝ๊ฐ„ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + +## Cloudflare์— ๋ฐฐํฌํ•˜๊ธฐ + +### 1. ํ”„๋กœ์ ํŠธ ๋นŒ๋“œ + +```bash +pnpm build +``` + +### 2. Wrangler๋กœ ๋ฐฐํฌ + +Cloudflare ๊ณ„์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์•„์ง ๋กœ๊ทธ์ธํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด: + +```bash +npx wrangler login +``` + +๊ทธ๋Ÿฐ ๋‹ค์Œ ๋ฐฐํฌ: + +```bash +npx wrangler deploy +``` + +### 3. Durable Objects ์„ค์ • ํ™•์ธ + +๋ฐฐํฌ ์‹œ Cloudflare Dashboard์—์„œ ๋‹ค์Œ์„ ํ™•์ธํ•˜์„ธ์š”: + +1. **Workers & Pages** ์„น์…˜์œผ๋กœ ์ด๋™ +2. ๋ฐฐํฌ๋œ Worker ์„ ํƒ +3. **Settings** > **Durable Objects** ํ™•์ธ +4. `CounterDurableObject` ๋ฐ”์ธ๋”ฉ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + +## ์ž‘๋™ ์›๋ฆฌ + +### Durable Objects + +`CounterDurableObject` ํด๋ž˜์Šค๋Š” ๋‹ค์Œ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค: + +1. **์ƒํƒœ ๊ด€๋ฆฌ**: ์นด์šดํŠธ ๊ฐ’๊ณผ ์—ฐ๊ฒฐ๋œ WebSocket ์„ธ์…˜ ๊ด€๋ฆฌ +2. **์˜๊ตฌ ์ €์žฅ**: ์นด์šดํŠธ๋ฅผ Durable Objects Storage์— ์ €์žฅ +3. **๋ธŒ๋กœ๋“œ์บ์ŠคํŒ…**: ๋ชจ๋“  ์—ฐ๊ฒฐ๋œ ํด๋ผ์ด์–ธํŠธ์— ์ƒํƒœ ๋ณ€๊ฒฝ ์•Œ๋ฆผ + +### WebSocket Hibernation + +Cloudflare์˜ WebSocket Hibernation API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ: + +- ๋ฉ”์‹œ์ง€๊ฐ€ ์—†์„ ๋•Œ Durable Object๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์ œ๊ฑฐ +- ๋น„์šฉ ์ ˆ๊ฐ (GB-์ดˆ ๋‹จ์œ„ ์ฒญ๊ตฌ ๋ฐฉ์ง€) +- ํด๋ผ์ด์–ธํŠธ ์—ฐ๊ฒฐ ์œ ์ง€ + +### ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™” + +1. ํด๋ผ์ด์–ธํŠธ๊ฐ€ `/api/counter`๋กœ WebSocket ์—ฐ๊ฒฐ +2. Durable Object๊ฐ€ ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ ์„ธ์…˜ ์ถ”์  +3. ์นด์šดํŠธ ๋ณ€๊ฒฝ ์‹œ ๋ชจ๋“  ์—ฐ๊ฒฐ๋œ ํด๋ผ์ด์–ธํŠธ์— ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ +4. ๊ฐ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ UI ์—…๋ฐ์ดํŠธ + +## ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ฐ ์„ค์ • + +### wrangler.jsonc + +```jsonc +{ + "name": "dd", + "main": ".svelte-kit/cloudflare/_worker.js", + "compatibility_date": "2025-01-15", + "assets": { + "binding": "ASSETS", + "directory": ".svelte-kit/cloudflare" + }, + "durable_objects": { + "bindings": [ + { + "name": "COUNTER", + "class_name": "CounterDurableObject", + "script_name": "dd" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["CounterDurableObject"] + } + ] +} +``` + +## ์ถ”๊ฐ€ ๊ธฐ๋Šฅ ์•„์ด๋””์–ด + +- ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๋…๋ฆฝ์ ์ธ ์นด์šดํ„ฐ (๋ฃธ ๊ธฐ๋ฐ˜) +- ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๊ด€๋ฆฌ +- ์นด์šดํŠธ ํžˆ์Šคํ† ๋ฆฌ ๋ฐ ํ†ต๊ณ„ +- ๋” ๋ณต์žกํ•œ ์‹ค์‹œ๊ฐ„ ํ˜‘์—… ๊ธฐ๋Šฅ + +## ๋ฌธ์ œ ํ•ด๊ฒฐ + +### WebSocket ์—ฐ๊ฒฐ ์‹คํŒจ + +- ๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”์—์„œ ์˜ค๋ฅ˜ ํ™•์ธ +- Cloudflare Dashboard์—์„œ Durable Objects ๋ฐ”์ธ๋”ฉ ํ™•์ธ +- Worker ๋กœ๊ทธ ํ™•์ธ (`wrangler tail`) + +### ๋กœ์ปฌ ๊ฐœ๋ฐœ ์‹œ WebSocket ์ž‘๋™ํ•˜์ง€ ์•Š์Œ + +๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” Wrangler์˜ `--local` ๋ชจ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ `wrangler dev`๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค: + +```bash +pnpm build +npx wrangler dev +``` + +## ์ฐธ๊ณ  ์ž๋ฃŒ + +- [SvelteKit ๋ฌธ์„œ](https://kit.svelte.dev/) +- [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/) +- [WebSocket Hibernation API](https://developers.cloudflare.com/durable-objects/best-practices/websockets/) +- [Cloudflare Workers](https://developers.cloudflare.com/workers/) + +## ๋ผ์ด์„ ์Šค + +MIT -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..dfba8c1 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,218 @@ +# ๋ฌธ์ œ ํ•ด๊ฒฐ ๊ฐ€์ด๋“œ + - [SvelteKit](https://kit.svelte.dev/) + - [WebSocket Hibernation](https://developers.cloudflare.com/durable-objects/best-practices/websockets/) + - [Cloudflare Durable Objects](https://developers.cloudflare.com/durable-objects/) +4. **๋ฌธ์„œ ์ฐธ๊ณ **: +3. **Cloudflare Dashboard**: Workers & Pages์—์„œ ๋ฐฐํฌ ์ƒํƒœ ํ™•์ธ +2. **๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”**: F12๋ฅผ ๋ˆŒ๋Ÿฌ JavaScript ์˜ค๋ฅ˜ ํ™•์ธ +1. **๋กœ๊ทธ ํ™•์ธ**: `pnpm cf:tail`๋กœ ์‹ค์‹œ๊ฐ„ ๋กœ๊ทธ ํ™•์ธ + +## ๋„์›€์ด ํ•„์š”ํ•˜์‹ ๊ฐ€์š”? + +--- + +``` +pnpm build +pnpm install +rm -rf .svelte-kit node_modules +```bash + +### ๋นŒ๋“œ ์บ์‹œ ๋ฌธ์ œ + +``` +# ๋ธŒ๋ผ์šฐ์ €์—์„œ Cloudflare ๋กœ๊ทธ์ธ +npx wrangler login +```bash + +### Wrangler ์ธ์ฆ ๋ฌธ์ œ + +``` +pnpm add -D @cloudflare/workers-types +```bash +๊ทธ๋ฆฌ๊ณ : + +``` +} + } + "types": ["@cloudflare/workers-types"] + "compilerOptions": { +{ +// tsconfig.json +```json +**ํ•ด๊ฒฐ:** + +``` +Cannot find module 'cloudflare:workers' +``` + +### TypeScript ํƒ€์ž… ์˜ค๋ฅ˜ + +## ๊ธฐํƒ€ ๋ฌธ์ œ + +--- + + ``` + const id = platform?.env.COUNTER.idFromName('global-counter'); + // +server.ts + ```typescript +3. **๋‹ค๋ฅธ Durable Object ID**: ๊ฐ™์€ ID๋ฅผ ์‚ฌ์šฉํ•˜๋Š”์ง€ ํ™•์ธ +2. **๋ฐฐํฌ ํ™˜๊ฒฝ**: ์‹ค์ œ Cloudflare์—์„œ๋Š” ์˜๊ตฌ ์ €์žฅ๋จ +1. **๋กœ์ปฌ ๊ฐœ๋ฐœ**: Wrangler์˜ ๋กœ์ปฌ storage๋Š” ์ž„์‹œ์ ์ผ ์ˆ˜ ์žˆ์Œ + +์ด๋ฏธ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค! ๋งŒ์•ฝ ์ž‘๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค๋ฉด: + +### ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• + +``` +} + }); + } + this.count = stored; + if (stored !== undefined) { + const stored = await this.ctx.storage.get('count'); // โœ… ๋ณต์› + this.ctx.blockConcurrencyWhile(async () => { + // ... +constructor(ctx: DurableObjectState, env: Env) { + +} + } + this.broadcast(); + await this.ctx.storage.put('count', this.count); // โœ… ์ €์žฅ + this.count++; + if (data && data.type === 'increment') { +async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) { +// counter-do.ts +```typescript + +Durable Object Storage์— ์ œ๋Œ€๋กœ ์ €์žฅ๋˜๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธ: + +### ํ™•์ธ ์‚ฌํ•ญ + +ํŽ˜์ด์ง€๋ฅผ ์ƒˆ๋กœ๊ณ ์นจํ•˜๊ฑฐ๋‚˜ ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—ฐ๊ฒฐ์„ ๋Š์œผ๋ฉด ์นด์šดํŠธ๊ฐ€ ์ดˆ๊ธฐํ™”๋จ. +### ๋ฌธ์ œ ์„ค๋ช… + +## ์นด์šดํŠธ๊ฐ€ ์ €์žฅ๋˜์ง€ ์•Š๋Š” ๋ฌธ์ œ + +--- + +- Migration์ด ์™„๋ฃŒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ +- Cloudflare Dashboard์—์„œ Durable Objects ๋ฐ”์ธ๋”ฉ ํ™•์ธ +**๋ฐฐํฌ ํ›„:** + +- Wrangler๊ฐ€ Durable Objects๋ฅผ ์—๋ฎฌ๋ ˆ์ด์…˜ +- `pnpm dev` ๋Œ€์‹  `pnpm cf:dev` ์‚ฌ์šฉ +**๋กœ์ปฌ ๊ฐœ๋ฐœ ์‹œ:** + +### ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• + + ``` + pnpm cf:tail + # ๋‹ค๋ฅธ ํ„ฐ๋ฏธ๋„์—์„œ + ```bash +4. **Wrangler ๋กœ๊ทธ ํ™•์ธ** + + ``` + // WebSocket ์—ฐ๊ฒฐ ๋กœ๊ทธ ํ™•์ธ + // F12 โ†’ Console ํƒญ์—์„œ ํ™•์ธ + ```javascript +3. **๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†” ํ™•์ธ** + + - ๋ฐฐํฌ: `wss://your-worker.workers.dev/api/counter` + - ๋กœ์ปฌ: `ws://localhost:8787/api/counter` +2. **์˜ฌ๋ฐ”๋ฅธ URL์ธ์ง€ ํ™•์ธ** + + ``` + pnpm cf:dev + ```bash +1. **Wrangler Dev ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธ** + +### ํ™•์ธ ์‚ฌํ•ญ + +- ๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†”์— WebSocket ์˜ค๋ฅ˜ +- "์—ฐ๊ฒฐํ•˜๊ธฐ" ๋ฒ„ํŠผ ํด๋ฆญ ํ›„ ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์Œ +### ๋ฌธ์ œ ์ฆ์ƒ + +## WebSocket ์—ฐ๊ฒฐ ์‹คํŒจ + +--- + +์ด์ œ `pnpm build`๋ฅผ ์‹คํ–‰ํ•˜๋ฉด ์ž๋™์œผ๋กœ Worker๊ฐ€ ํŒจ์น˜๋ฉ๋‹ˆ๋‹ค. + +``` +} + } + "build": "vite build && node scripts/patch-worker.js" + "scripts": { +{ +```json +**package.json:** + +Post-build ์Šคํฌ๋ฆฝํŠธ `scripts/patch-worker.js`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋นŒ๋“œ ํ›„ ์ž๋™์œผ๋กœ Worker ํŒŒ์ผ์— Durable Object์„ exportํ•˜๋„๋ก ํŒจ์น˜ํ•ฉ๋‹ˆ๋‹ค. +### ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• + +SvelteKit adapter-cloudflare๊ฐ€ ์ƒ์„ฑํ•˜๋Š” `_worker.js`์—๋Š” ์ž๋™์œผ๋กœ Durable Object export๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. +### ์›์ธ + +``` +which are not exported in your entrypoint file: CounterDurableObject. +ERROR: Your Worker depends on the following Durable Objects, +``` +### ๋ฌธ์ œ ์„ค๋ช… + +## ๋นŒ๋“œ ํ›„ Durable Object Export ๋ฌธ์ œ + +--- + +5. ์ฒซ ๋ฒˆ์งธ ํƒญ์—์„œ๋„ ์ ‘์†์ž ์ˆ˜ 2๋กœ ์—…๋ฐ์ดํŠธ๋จ +4. "์—ฐ๊ฒฐํ•˜๊ธฐ" ํด๋ฆญ โ†’ ์ ‘์†์ž ์ˆ˜: 2 (์ด์ œ ์ •์ƒ ์ž‘๋™!) +3. ์ƒˆ ํƒญ์—์„œ ๊ฐ™์€ ํŽ˜์ด์ง€ ์—ด๊ธฐ +2. "์—ฐ๊ฒฐํ•˜๊ธฐ" ํด๋ฆญ โ†’ ์ ‘์†์ž ์ˆ˜: 1 +1. ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ฒซ ๋ฒˆ์งธ ํƒญ ์—ด๊ธฐ + +### ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ• + +4. **์ •ํ™•ํ•œ ์นด์šดํŠธ**: `this.ctx.getWebSockets()`๋Š” Cloudflare๊ฐ€ ๊ด€๋ฆฌํ•˜๋Š” ์‹ค์ œ ์—ฐ๊ฒฐ๋œ WebSocket ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค +3. **WebSocket ์—ฐ๊ฒฐ์€ ์œ ์ง€**: ํ•˜์ง€๋งŒ WebSocket ์—ฐ๊ฒฐ ์ž์ฒด๋Š” Cloudflare๊ฐ€ ์œ ์ง€ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค +2. **State ์ดˆ๊ธฐํ™”**: Hibernation ํ›„ ์žฌํ™œ์„ฑํ™”๋  ๋•Œ `constructor`๊ฐ€ ๋‹ค์‹œ ์‹คํ–‰๋˜์–ด `this.sessions`๊ฐ€ ๋นˆ Map์œผ๋กœ ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค +1. **WebSocket Hibernation API**: Durable Object์ด ์ผ์ • ์‹œ๊ฐ„ ํ™œ๋™์ด ์—†์œผ๋ฉด ๋ฉ”๋ชจ๋ฆฌ์—์„œ ์ œ๊ฑฐ๋ฉ๋‹ˆ๋‹ค + +### ์™œ ์ด๋ ‡๊ฒŒ ํ•ด์•ผ ํ•˜๋‚˜์š”? + +``` +} + // ... + }); + lastUpdate: this.lastUpdate + online: connectedWebSockets.length, // โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ• + count: this.count, + const message = JSON.stringify({ + + const connectedWebSockets = this.ctx.getWebSockets(); +private broadcast() { +```typescript +**์ˆ˜์ • ํ›„:** + +``` +} + // ... + }); + lastUpdate: this.lastUpdate + online: this.sessions.size, // โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ• + count: this.count, + const message = JSON.stringify({ +private broadcast() { +```typescript +**์ˆ˜์ • ์ „:** + +`this.ctx.getWebSockets()`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‹ค์ œ ์—ฐ๊ฒฐ๋œ WebSocket ๊ฐœ์ˆ˜๋ฅผ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. +### ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• + +WebSocket Hibernation API๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ, Durable Object์˜ in-memory state (`this.sessions` Map)๋Š” hibernation ํ›„ ์ดˆ๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ `this.sessions.size`๋Š” ์ •ํ™•ํ•œ ์ ‘์†์ž ์ˆ˜๋ฅผ ๋ฐ˜์˜ํ•˜์ง€ ๋ชปํ•ฉ๋‹ˆ๋‹ค. +### ์›์ธ + +์—ฌ๋Ÿฌ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—ฐ๊ฒฐ๋˜์–ด๋„ "์‹ค์‹œ๊ฐ„ ์ ‘์†์ž ์ˆ˜"๊ฐ€ 1์—์„œ ์ฆ๊ฐ€ํ•˜์ง€ ์•Š์Œ. +### ๋ฌธ์ œ ์„ค๋ช… + +## ์‹ค์‹œ๊ฐ„ ์ ‘์†์ž ์ˆ˜๊ฐ€ ์ฆ๊ฐ€ํ•˜์ง€ ์•Š๋Š” ๋ฌธ์ œ + + diff --git a/package.json b/package.json index cabb751..26af325 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,19 @@ "type": "module", "scripts": { "dev": "vite dev", - "build": "vite build", + "build": "vite build && node scripts/patch-worker.js", "preview": "vite preview", "prepare": "svelte-kit sync || echo ''", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write .", - "lint": "prettier --check ." + "lint": "prettier --check .", + "deploy": "pnpm build && wrangler deploy", + "cf:dev": "pnpm build && wrangler dev", + "cf:tail": "wrangler tail" }, "devDependencies": { + "@cloudflare/workers-types": "^4.20251113.0", "@sveltejs/adapter-cloudflare": "^7.2.4", "@sveltejs/kit": "^2.47.1", "@sveltejs/vite-plugin-svelte": "^6.2.1", @@ -25,12 +29,13 @@ "svelte-check": "^4.3.3", "tailwindcss": "^4.1.14", "typescript": "^5.9.3", - "vite": "^7.1.10" + "vite": "^7.1.10", + "wrangler": "^4.48.0" }, "pnpm": { "onlyBuiltDependencies": [ - "esbuild", - "@tailwindcss/oxide" + "@tailwindcss/oxide", + "esbuild" ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d345c50..bb2c518 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,9 @@ onlyBuiltDependencies: - esbuild devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20251113.0 + version: 4.20251113.0 '@sveltejs/adapter-cloudflare': specifier: ^7.2.4 version: 7.2.4(@sveltejs/kit@2.48.5)(wrangler@4.48.0) @@ -45,6 +48,9 @@ devDependencies: vite: specifier: ^7.1.10 version: 7.2.2 + wrangler: + specifier: ^4.48.0 + version: 4.48.0(@cloudflare/workers-types@4.20251113.0) packages: @@ -938,7 +944,7 @@ packages: '@cloudflare/workers-types': 4.20251113.0 '@sveltejs/kit': 2.48.5(@sveltejs/vite-plugin-svelte@6.2.1)(svelte@5.43.6)(vite@7.2.2) worktop: 0.8.0-next.18 - wrangler: 4.48.0 + wrangler: 4.48.0(@cloudflare/workers-types@4.20251113.0) dev: true /@sveltejs/kit@2.48.5(@sveltejs/vite-plugin-svelte@6.2.1)(svelte@5.43.6)(vite@7.2.2): @@ -1951,7 +1957,7 @@ packages: regexparam: 3.0.0 dev: true - /wrangler@4.48.0: + /wrangler@4.48.0(@cloudflare/workers-types@4.20251113.0): resolution: {integrity: sha512-qkcwysx96XNDWXl4w/5VjAZjqWatxAq9chMXVeqv/etL9e06ouPaZ+Hwwbe5XYV2GYf/XhZVZ3fHJcTBrq60gQ==} engines: {node: '>=18.0.0'} hasBin: true @@ -1963,6 +1969,7 @@ packages: dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@cloudflare/unenv-preset': 2.7.10(unenv@2.0.0-rc.24)(workerd@1.20251109.0) + '@cloudflare/workers-types': 4.20251113.0 blake3-wasm: 2.1.5 esbuild: 0.25.4 miniflare: 4.20251109.1 diff --git a/scripts/patch-worker.js b/scripts/patch-worker.js new file mode 100644 index 0000000..f4d735d --- /dev/null +++ b/scripts/patch-worker.js @@ -0,0 +1,46 @@ +// Post-build script to add Durable Object exports to the Worker +import { readFileSync, writeFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Go up one level from scripts/ to the project root +const projectRoot = join(__dirname, '..'); +const workerPath = join(projectRoot, '.svelte-kit', 'cloudflare', '_worker.js'); + +try { + let content = readFileSync(workerPath, 'utf-8'); + + // Check if already patched + if (content.includes('CounterDurableObject')) { + console.log('โœ“ Worker already patched with Durable Object exports'); + process.exit(0); + } + + // Add import for Durable Objects after the env import + const importPattern = /import { env } from "cloudflare:workers";/; + const importReplacement = `import { env } from "cloudflare:workers"; + +// Import Durable Objects from hooks.server +import { CounterDurableObject } from "../output/server/chunks/hooks.server.js";`; + + content = content.replace(importPattern, importReplacement); + + // Add Durable Object to exports + const exportPattern = /export {\s*worker_default as default\s*};/; + const exportReplacement = `export { + worker_default as default, + CounterDurableObject +};`; + + content = content.replace(exportPattern, exportReplacement); + + writeFileSync(workerPath, content, 'utf-8'); + console.log('โœ“ Successfully patched Worker with Durable Object exports'); +} catch (error) { + console.error('โœ— Failed to patch Worker:', error.message); + process.exit(1); +} + diff --git a/src/app.d.ts b/src/app.d.ts index da08e6d..5ed51da 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -1,13 +1,18 @@ -// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces +import type { DurableObjectNamespace } from '@cloudflare/workers-types'; + declare global { namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} + interface Platform { + env: { + COUNTER: DurableObjectNamespace; + }; + context: { + waitUntil(promise: Promise): void; + }; + caches: CacheStorage & { default: Cache }; + } } } export {}; + diff --git a/src/durable-objects/counter.ts b/src/durable-objects/counter.ts new file mode 100644 index 0000000..584fb88 --- /dev/null +++ b/src/durable-objects/counter.ts @@ -0,0 +1,115 @@ +import { DurableObject } from 'cloudflare:workers'; + +export interface Env { + COUNTER: DurableObjectNamespace; +} + +interface Session { + id: string; + webSocket: WebSocket; + quit?: boolean; +} + +export class CounterDurableObject extends DurableObject { + private sessions: Map; + private count: number; + private lastUpdate: number; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.sessions = new Map(); + this.count = 0; + this.lastUpdate = Date.now(); + + // Durable Objects์—์„œ ์˜๊ตฌ ์ €์žฅ์†Œ๋กœ๋ถ€ํ„ฐ ์นด์šดํŠธ๋ฅผ ๋ณต์› + this.ctx.blockConcurrencyWhile(async () => { + const stored = await this.ctx.storage.get('count'); + if (stored !== undefined) { + this.count = stored; + } + }); + } + + async fetch(request: Request): Promise { + // WebSocket ์—…๊ทธ๋ ˆ์ด๋“œ ์š”์ฒญ ์ฒ˜๋ฆฌ + const upgradeHeader = request.headers.get('Upgrade'); + if (!upgradeHeader || upgradeHeader !== 'websocket') { + return new Response('Expected Upgrade: websocket', { status: 426 }); + } + + // WebSocketPair ์ƒ์„ฑ + const webSocketPair = new WebSocketPair(); + const [client, server] = Object.values(webSocketPair); + + // ์„ธ์…˜ ์ƒ์„ฑ + const session: Session = { + id: crypto.randomUUID(), + webSocket: server + }; + + // WebSocket Hibernation API ์‚ฌ์šฉ + this.ctx.acceptWebSocket(server); + this.sessions.set(server, session); + + // ํ˜„์žฌ ์ƒํƒœ ์ „์†ก + this.broadcast(); + + return new Response(null, { + status: 101, + webSocket: client + }); + } + + async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) { + try { + const data = typeof message === 'string' ? JSON.parse(message) : null; + + if (data && data.type === 'increment') { + // ์นด์šดํŠธ ์ฆ๊ฐ€ + this.count++; + this.lastUpdate = Date.now(); + + // ์˜๊ตฌ ์ €์žฅ์†Œ์— ์ €์žฅ + await this.ctx.storage.put('count', this.count); + + // ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์— ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ + this.broadcast(); + } else if (data && data.type === 'reset') { + // ์นด์šดํŠธ ๋ฆฌ์…‹ + this.count = 0; + this.lastUpdate = Date.now(); + await this.ctx.storage.put('count', this.count); + this.broadcast(); + } + } catch (error) { + console.error('Error handling message:', error); + } + } + + async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean) { + // ์„ธ์…˜ ์ œ๊ฑฐ + this.sessions.delete(ws); + ws.close(code, 'Durable Object is closing WebSocket'); + + // ๋‚จ์€ ํด๋ผ์ด์–ธํŠธ๋“ค์—๊ฒŒ ์—…๋ฐ์ดํŠธ ์ „์†ก + this.broadcast(); + } + + private broadcast() { + const message = JSON.stringify({ + count: this.count, + online: this.sessions.size, + lastUpdate: this.lastUpdate + }); + + // ๋ชจ๋“  ์—ฐ๊ฒฐ๋œ WebSocket์— ๋ฉ”์‹œ์ง€ ์ „์†ก + this.ctx.getWebSockets().forEach((ws) => { + try { + ws.send(message); + } catch (error) { + console.error('Error broadcasting to client:', error); + } + }); + } +} + diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..9aa5aa9 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,4 @@ +// Export Durable Objects for Cloudflare Workers +export { CounterDurableObject } from '$lib/counter-do'; + + diff --git a/src/lib/counter-do.ts b/src/lib/counter-do.ts new file mode 100644 index 0000000..bd41c74 --- /dev/null +++ b/src/lib/counter-do.ts @@ -0,0 +1,181 @@ +import type { DurableObjectNamespace, DurableObjectState } from '@cloudflare/workers-types'; + +export interface Env { + COUNTER: DurableObjectNamespace; +} + +interface Session { + id: string; + webSocket: WebSocket; + quit?: boolean; +} + +export class CounterDurableObject { + private ctx: DurableObjectState; + private env: Env; + private sessions: Map; + private count: number; + private lastUpdate: number; + private diceNumber: number; + private isPlaying: boolean; + private diceInterval: ReturnType | null; + + constructor(ctx: DurableObjectState, env: Env) { + this.ctx = ctx; + this.env = env; + this.sessions = new Map(); + this.count = 0; + this.lastUpdate = Date.now(); + this.diceNumber = 1; + this.isPlaying = false; + this.diceInterval = null; + + // Durable Objects์—์„œ ์˜๊ตฌ ์ €์žฅ์†Œ๋กœ๋ถ€ํ„ฐ ์นด์šดํŠธ๋ฅผ ๋ณต์› + this.ctx.blockConcurrencyWhile(async () => { + const stored = await this.ctx.storage.get('count'); + if (stored !== undefined) { + this.count = stored; + } + const storedDice = await this.ctx.storage.get('diceNumber'); + if (storedDice !== undefined) { + this.diceNumber = storedDice; + } + }); + } + + async fetch(request: Request): Promise { + // WebSocket ์—…๊ทธ๋ ˆ์ด๋“œ ์š”์ฒญ ์ฒ˜๋ฆฌ + const upgradeHeader = request.headers.get('Upgrade'); + if (!upgradeHeader || upgradeHeader !== 'websocket') { + return new Response('Expected Upgrade: websocket', { status: 426 }); + } + + // @ts-expect-error - WebSocketPair๋Š” Cloudflare Workers ๋Ÿฐํƒ€์ž„์—์„œ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ + const webSocketPair = new WebSocketPair(); + const [client, server] = Object.values(webSocketPair) as [WebSocket, WebSocket]; + + // ์„ธ์…˜ ์ƒ์„ฑ + const session: Session = { + id: crypto.randomUUID(), + webSocket: server + }; + + // WebSocket Hibernation API ์‚ฌ์šฉ + // @ts-expect-error - Cloudflare Workers types ๋ถˆ์ผ์น˜ + this.ctx.acceptWebSocket(server); + this.sessions.set(server, session); + + // ํ˜„์žฌ ์ƒํƒœ ์ „์†ก + this.broadcast(); + + return new Response(null, { + status: 101, + // @ts-expect-error - webSocket ์†์„ฑ์€ Cloudflare Workers์—์„œ ์ง€์›๋จ + webSocket: client + }); + } + + async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) { + try { + const data = typeof message === 'string' ? JSON.parse(message) : null; + + if (data && data.type === 'increment') { + // ์นด์šดํŠธ ์ฆ๊ฐ€ + this.count++; + this.lastUpdate = Date.now(); + + // ์˜๊ตฌ ์ €์žฅ์†Œ์— ์ €์žฅ + await this.ctx.storage.put('count', this.count); + + // ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์— ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ + this.broadcast(); + } else if (data && data.type === 'reset') { + // ์นด์šดํŠธ ๋ฆฌ์…‹ + this.count = 0; + this.lastUpdate = Date.now(); + await this.ctx.storage.put('count', this.count); + this.broadcast(); + } else if (data && data.type === 'play') { + // ์ฃผ์‚ฌ์œ„ ํ”Œ๋ ˆ์ด ์‹œ์ž‘ + this.startDiceRolling(); + } else if (data && data.type === 'stop') { + // ์ฃผ์‚ฌ์œ„ ํ”Œ๋ ˆ์ด ์ •์ง€ + this.stopDiceRolling(); + } + } catch (error) { + console.error('Error handling message:', error); + } + } + + private startDiceRolling() { + if (this.isPlaying) return; // ์ด๋ฏธ ์‹คํ–‰ ์ค‘์ด๋ฉด ๋ฌด์‹œ + + this.isPlaying = true; + this.broadcast(); // ํ”Œ๋ ˆ์ด ์ƒํƒœ ์ „์†ก + + // 1์ดˆ๋งˆ๋‹ค ์ฃผ์‚ฌ์œ„ ๊ตด๋ฆฌ๊ธฐ + this.diceInterval = setInterval(async () => { + // 1-6 ์‚ฌ์ด ๋žœ๋ค ์ˆซ์ž + this.diceNumber = Math.floor(Math.random() * 6) + 1; + this.lastUpdate = Date.now(); + + // Durable Object Storage์— ์ €์žฅ + await this.ctx.storage.put('diceNumber', this.diceNumber); + + // ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์— ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธ + this.broadcast(); + }, 1000); + } + + private stopDiceRolling() { + if (!this.isPlaying) return; + + this.isPlaying = false; + + if (this.diceInterval) { + clearInterval(this.diceInterval); + this.diceInterval = null; + } + + this.broadcast(); // ์ •์ง€ ์ƒํƒœ ์ „์†ก + } + + async webSocketClose(ws: WebSocket, code: number, _reason: string, _wasClean: boolean) { + // ์„ธ์…˜ ์ œ๊ฑฐ + this.sessions.delete(ws); + ws.close(code, 'Durable Object is closing WebSocket'); + + // ๋‚จ์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์—†์œผ๋ฉด ์ฃผ์‚ฌ์œ„ ์ •์ง€ + const connectedWebSockets = this.ctx.getWebSockets(); + if (connectedWebSockets.length === 0) { + this.stopDiceRolling(); + } + + // ๋‚จ์€ ํด๋ผ์ด์–ธํŠธ๋“ค์—๊ฒŒ ์—…๋ฐ์ดํŠธ ์ „์†ก + this.broadcast(); + } + + private broadcast() { + // WebSocket Hibernation API๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ๋Š” getWebSockets()๋กœ ์‹ค์ œ ์—ฐ๊ฒฐ ์ˆ˜๋ฅผ ํ™•์ธ + const connectedWebSockets = this.ctx.getWebSockets(); + + const message = JSON.stringify({ + count: this.count, + online: connectedWebSockets.length, + lastUpdate: this.lastUpdate, + diceNumber: this.diceNumber, + isPlaying: this.isPlaying + }); + + // ๋ชจ๋“  ์—ฐ๊ฒฐ๋œ WebSocket์— ๋ฉ”์‹œ์ง€ ์ „์†ก + // @ts-expect-error - Cloudflare Workers types ๋ถˆ์ผ์น˜ + connectedWebSockets.forEach((ws: WebSocket) => { + try { + ws.send(message); + } catch (error) { + console.error('Error broadcasting to client:', error); + } + }); + } +} + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index cc88df0..6f771c2 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,2 +1,295 @@ -

Welcome to SvelteKit

-

Visit svelte.dev/docs/kit to read the documentation

+ + +
+
+

+ Cloudflare Durable Objects + WebSocket +

+ + +
+
+
+
+ + {isConnected ? '์—ฐ๊ฒฐ๋จ' : isConnecting ? '์—ฐ๊ฒฐ ์ค‘...' : '์—ฐ๊ฒฐ ์•ˆ๋จ'} + +
+ {#if !isConnected} + + {:else} + + {/if} +
+
+ + +
+
+
+ {count} +
+
์ „์ฒด ์นด์šดํŠธ
+
+ + +
+ + +
+
+ + +
+
+
+ {online} +
+
์‹ค์‹œ๊ฐ„ ์ ‘์†์ž
+
+
+
+ {lastUpdate ? lastUpdate.toLocaleTimeString('ko-KR') : '-'} +
+
๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ
+
+
+ + +
+

+ ๐ŸŽฒ Durable Object ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ +

+ + +
+
+
+
+ {#each getDiceDots(diceNumber) as row} + {#each row as dot} +
+ {#if dot} +
+ {/if} +
+ {/each} + {/each} +
+
+ +
+ {diceNumber} +
+
+
+ + +
+ {#if !isPlaying} + + {:else} + + {/if} +
+ + +
+
+ {isPlaying ? '๐Ÿ”„ ์ฃผ์‚ฌ์œ„๊ฐ€ 1์ดˆ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ๊ตด๋Ÿฌ๊ฐ€๋Š” ์ค‘...' : 'โธ๏ธ ์ •์ง€๋จ'} +
+
+ + +
+

๐Ÿ’ก ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ:

+

โ€ข ํ”Œ๋ ˆ์ด ๋ฒ„ํŠผ ํด๋ฆญ ์‹œ 1์ดˆ๋งˆ๋‹ค ์ฃผ์‚ฌ์œ„(1-6)๊ฐ€ ์ž๋™์œผ๋กœ ๋ณ€๊ฒฝ๋ฉ๋‹ˆ๋‹ค

+

โ€ข ๋ชจ๋“  ๊ฐ’์€ Durable Object์— ์‹ค์‹œ๊ฐ„ ์ €์žฅ๋ฉ๋‹ˆ๋‹ค

+

โ€ข ๋ชจ๋“  ์—ฐ๊ฒฐ๋œ ํด๋ผ์ด์–ธํŠธ์— ์ฆ‰์‹œ ๋™๊ธฐํ™”๋ฉ๋‹ˆ๋‹ค

+
+
+ + +
+

๊ธฐ๋Šฅ ์„ค๋ช…

+
    +
  • โœ… Cloudflare Durable Objects๋กœ ์ƒํƒœ ๊ด€๋ฆฌ
  • +
  • โœ… WebSocket์œผ๋กœ ์‹ค์‹œ๊ฐ„ ์–‘๋ฐฉํ–ฅ ํ†ต์‹ 
  • +
  • โœ… ์นด์šดํŠธ ๋ฒ„ํŠผ ํด๋ฆญ์œผ๋กœ ์ฆ๊ฐ€
  • +
  • โœ… ์‹ค์‹œ๊ฐ„ ์ ‘์†์ž ์ˆ˜ ํ‘œ์‹œ
  • +
  • โœ… ๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ์— ์‹ค์‹œ๊ฐ„ ๋™๊ธฐํ™”
  • +
  • โœ… Durable Objects ์˜๊ตฌ ์ €์žฅ์†Œ์— ์ƒํƒœ ์ €์žฅ
  • +
  • ๐ŸŽฒ ์ดˆ๋‹จ์œ„ ์ฃผ์‚ฌ์œ„ ์ž๋™ ๋กค๋ง์œผ๋กœ ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ
  • +
+
+
+
+ + diff --git a/src/routes/api/counter/+server.ts b/src/routes/api/counter/+server.ts new file mode 100644 index 0000000..0d8382b --- /dev/null +++ b/src/routes/api/counter/+server.ts @@ -0,0 +1,23 @@ +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ request, platform }) => { + // WebSocket ์—…๊ทธ๋ ˆ์ด๋“œ ์š”์ฒญ์ธ์ง€ ํ™•์ธ + const upgradeHeader = request.headers.get('Upgrade'); + if (!upgradeHeader || upgradeHeader !== 'websocket') { + return new Response('Expected Upgrade: websocket', { status: 426 }); + } + + // Durable Object ID ์ƒ์„ฑ (๋ชจ๋“  ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ฐ™์€ ์ธ์Šคํ„ด์Šค์— ์—ฐ๊ฒฐ) + const id = platform?.env.COUNTER.idFromName('global-counter'); + + if (!id) { + return new Response('Durable Object not configured', { status: 500 }); + } + + // Durable Object stub ๊ฐ€์ ธ์˜ค๊ธฐ + const stub = platform.env.COUNTER.get(id); + + // Durable Object์— ์š”์ฒญ ์ „๋‹ฌ + return stub.fetch(request); +}; + diff --git a/svelte.config.js b/svelte.config.js index 612cde9..1c8b12a 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -3,10 +3,16 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; /** @type {import('@sveltejs/kit').Config} */ const config = { - // Consult https://svelte.dev/docs/kit/integrations - // for more information about preprocessors preprocess: vitePreprocess(), - kit: { adapter: adapter() } + kit: { + adapter: adapter({ + routes: { + include: ['/*'], + exclude: [''] + } + }) + } }; export default config; + diff --git a/tsconfig.json b/tsconfig.json index 5a3b413..5cf8770 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,8 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "types": ["@cloudflare/workers-types"] } // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files diff --git a/vite.config.ts b/vite.config.ts index 2d35c4f..f74906e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,5 +3,14 @@ import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [tailwindcss(), sveltekit()] + plugins: [tailwindcss(), sveltekit()], + ssr: { + external: ['cloudflare:workers'], + noExternal: [] + }, + build: { + rollupOptions: { + external: ['cloudflare:workers'] + } + } }); diff --git a/wrangler.jsonc b/wrangler.jsonc new file mode 100644 index 0000000..6e3050d --- /dev/null +++ b/wrangler.jsonc @@ -0,0 +1,25 @@ +{ + "name": "dd", + "main": ".svelte-kit/cloudflare/_worker.js", + "compatibility_date": "2025-01-15", + "compatibility_flags": ["nodejs_compat"], + "assets": { + "binding": "ASSETS", + "directory": ".svelte-kit/cloudflare" + }, + "durable_objects": { + "bindings": [ + { + "name": "COUNTER", + "class_name": "CounterDurableObject" + } + ] + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["CounterDurableObject"] + } + ] +} +