듀라블오브젝트 샘플
This commit is contained in:
parent
a9e6c9333d
commit
da2918bd88
92
CHANGELOG.md
Normal file
92
CHANGELOG.md
Normal file
@ -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. 커밋 메시지 예시 추가
|
||||||
|
|
||||||
165
DEPLOYMENT.md
Normal file
165
DEPLOYMENT.md
Normal file
@ -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)
|
||||||
|
|
||||||
180
DICE_FEATURE.md
Normal file
180
DICE_FEATURE.md
Normal file
@ -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<typeof setInterval> | 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
|
||||||
|
**상태**: ✅ 구현 완료
|
||||||
|
|
||||||
209
PROJECT_STRUCTURE.md
Normal file
209
PROJECT_STRUCTURE.md
Normal file
@ -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
|
||||||
|
<script lang="ts">
|
||||||
|
let count = $state(0);
|
||||||
|
let online = $state(0);
|
||||||
|
let ws = $state<WebSocket | null>(null);
|
||||||
|
|
||||||
|
function connectWebSocket() { /* ... */ }
|
||||||
|
function incrementCount() { /* ... */ }
|
||||||
|
function resetCount() { /* ... */ }
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 통합
|
||||||
|
- 커스텀 메트릭 수집
|
||||||
|
|
||||||
118
QUICKSTART.md
Normal file
118
QUICKSTART.md
Normal file
@ -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`를 참고하세요!
|
||||||
|
|
||||||
186
README.md
186
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
|
- **SvelteKit 5** - 풀스택 웹 프레임워크
|
||||||
npx sv create my-app
|
- **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
|
```bash
|
||||||
npm run dev
|
pnpm install
|
||||||
|
|
||||||
# or start the server and open the app in a new browser tab
|
|
||||||
npm run dev -- --open
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
### 2. 로컬 개발 서버 실행
|
||||||
|
|
||||||
To create a production version of your app:
|
```bash
|
||||||
|
pnpm dev
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
|
||||||
|
|||||||
218
TROUBLESHOOTING.md
Normal file
218
TROUBLESHOOTING.md
Normal file
@ -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<number>('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에서 증가하지 않음.
|
||||||
|
### 문제 설명
|
||||||
|
|
||||||
|
## 실시간 접속자 수가 증가하지 않는 문제
|
||||||
|
|
||||||
|
|
||||||
15
package.json
15
package.json
@ -5,15 +5,19 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build && node scripts/patch-worker.js",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"prepare": "svelte-kit sync || echo ''",
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
"format": "prettier --write .",
|
"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": {
|
"devDependencies": {
|
||||||
|
"@cloudflare/workers-types": "^4.20251113.0",
|
||||||
"@sveltejs/adapter-cloudflare": "^7.2.4",
|
"@sveltejs/adapter-cloudflare": "^7.2.4",
|
||||||
"@sveltejs/kit": "^2.47.1",
|
"@sveltejs/kit": "^2.47.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||||
@ -25,12 +29,13 @@
|
|||||||
"svelte-check": "^4.3.3",
|
"svelte-check": "^4.3.3",
|
||||||
"tailwindcss": "^4.1.14",
|
"tailwindcss": "^4.1.14",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.10"
|
"vite": "^7.1.10",
|
||||||
|
"wrangler": "^4.48.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
"esbuild",
|
"@tailwindcss/oxide",
|
||||||
"@tailwindcss/oxide"
|
"esbuild"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
11
pnpm-lock.yaml
generated
11
pnpm-lock.yaml
generated
@ -9,6 +9,9 @@ onlyBuiltDependencies:
|
|||||||
- esbuild
|
- esbuild
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
|
'@cloudflare/workers-types':
|
||||||
|
specifier: ^4.20251113.0
|
||||||
|
version: 4.20251113.0
|
||||||
'@sveltejs/adapter-cloudflare':
|
'@sveltejs/adapter-cloudflare':
|
||||||
specifier: ^7.2.4
|
specifier: ^7.2.4
|
||||||
version: 7.2.4(@sveltejs/kit@2.48.5)(wrangler@4.48.0)
|
version: 7.2.4(@sveltejs/kit@2.48.5)(wrangler@4.48.0)
|
||||||
@ -45,6 +48,9 @@ devDependencies:
|
|||||||
vite:
|
vite:
|
||||||
specifier: ^7.1.10
|
specifier: ^7.1.10
|
||||||
version: 7.2.2
|
version: 7.2.2
|
||||||
|
wrangler:
|
||||||
|
specifier: ^4.48.0
|
||||||
|
version: 4.48.0(@cloudflare/workers-types@4.20251113.0)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@ -938,7 +944,7 @@ packages:
|
|||||||
'@cloudflare/workers-types': 4.20251113.0
|
'@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)
|
'@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
|
worktop: 0.8.0-next.18
|
||||||
wrangler: 4.48.0
|
wrangler: 4.48.0(@cloudflare/workers-types@4.20251113.0)
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@sveltejs/kit@2.48.5(@sveltejs/vite-plugin-svelte@6.2.1)(svelte@5.43.6)(vite@7.2.2):
|
/@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
|
regexparam: 3.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/wrangler@4.48.0:
|
/wrangler@4.48.0(@cloudflare/workers-types@4.20251113.0):
|
||||||
resolution: {integrity: sha512-qkcwysx96XNDWXl4w/5VjAZjqWatxAq9chMXVeqv/etL9e06ouPaZ+Hwwbe5XYV2GYf/XhZVZ3fHJcTBrq60gQ==}
|
resolution: {integrity: sha512-qkcwysx96XNDWXl4w/5VjAZjqWatxAq9chMXVeqv/etL9e06ouPaZ+Hwwbe5XYV2GYf/XhZVZ3fHJcTBrq60gQ==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@ -1963,6 +1969,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@cloudflare/kv-asset-handler': 0.4.0
|
'@cloudflare/kv-asset-handler': 0.4.0
|
||||||
'@cloudflare/unenv-preset': 2.7.10(unenv@2.0.0-rc.24)(workerd@1.20251109.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
|
blake3-wasm: 2.1.5
|
||||||
esbuild: 0.25.4
|
esbuild: 0.25.4
|
||||||
miniflare: 4.20251109.1
|
miniflare: 4.20251109.1
|
||||||
|
|||||||
46
scripts/patch-worker.js
Normal file
46
scripts/patch-worker.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
19
src/app.d.ts
vendored
19
src/app.d.ts
vendored
@ -1,13 +1,18 @@
|
|||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
import type { DurableObjectNamespace } from '@cloudflare/workers-types';
|
||||||
// for information about these interfaces
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
interface Platform {
|
||||||
// interface Locals {}
|
env: {
|
||||||
// interface PageData {}
|
COUNTER: DurableObjectNamespace;
|
||||||
// interface PageState {}
|
};
|
||||||
// interface Platform {}
|
context: {
|
||||||
|
waitUntil(promise: Promise<any>): void;
|
||||||
|
};
|
||||||
|
caches: CacheStorage & { default: Cache };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|
||||||
|
|||||||
115
src/durable-objects/counter.ts
Normal file
115
src/durable-objects/counter.ts
Normal file
@ -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<WebSocket, Session>;
|
||||||
|
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<number>('count');
|
||||||
|
if (stored !== undefined) {
|
||||||
|
this.count = stored;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch(request: Request): Promise<Response> {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
4
src/hooks.server.ts
Normal file
4
src/hooks.server.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Export Durable Objects for Cloudflare Workers
|
||||||
|
export { CounterDurableObject } from '$lib/counter-do';
|
||||||
|
|
||||||
|
|
||||||
181
src/lib/counter-do.ts
Normal file
181
src/lib/counter-do.ts
Normal file
@ -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<WebSocket, Session>;
|
||||||
|
private count: number;
|
||||||
|
private lastUpdate: number;
|
||||||
|
private diceNumber: number;
|
||||||
|
private isPlaying: boolean;
|
||||||
|
private diceInterval: ReturnType<typeof setInterval> | 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<number>('count');
|
||||||
|
if (stored !== undefined) {
|
||||||
|
this.count = stored;
|
||||||
|
}
|
||||||
|
const storedDice = await this.ctx.storage.get<number>('diceNumber');
|
||||||
|
if (storedDice !== undefined) {
|
||||||
|
this.diceNumber = storedDice;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch(request: Request): Promise<Response> {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -1,2 +1,295 @@
|
|||||||
<h1>Welcome to SvelteKit</h1>
|
<script lang="ts">
|
||||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
let count = $state(0);
|
||||||
|
let online = $state(0);
|
||||||
|
let lastUpdate = $state<Date | null>(null);
|
||||||
|
let isConnected = $state(false);
|
||||||
|
let isConnecting = $state(false);
|
||||||
|
let ws = $state<WebSocket | null>(null);
|
||||||
|
let diceNumber = $state(1);
|
||||||
|
let isPlaying = $state(false);
|
||||||
|
|
||||||
|
function connectWebSocket() {
|
||||||
|
isConnecting = true;
|
||||||
|
|
||||||
|
// WebSocket 프로토콜 결정 (https -> wss, http -> ws)
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const wsUrl = `${protocol}//${window.location.host}/api/counter`;
|
||||||
|
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
isConnected = true;
|
||||||
|
isConnecting = false;
|
||||||
|
console.log('WebSocket connected');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
count = data.count;
|
||||||
|
online = data.online;
|
||||||
|
lastUpdate = new Date(data.lastUpdate);
|
||||||
|
diceNumber = data.diceNumber || 1;
|
||||||
|
isPlaying = data.isPlaying || false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
isConnecting = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
isConnected = false;
|
||||||
|
isConnecting = false;
|
||||||
|
console.log('WebSocket disconnected');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectWebSocket() {
|
||||||
|
if (ws) {
|
||||||
|
ws.close();
|
||||||
|
ws = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementCount() {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'increment' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetCount() {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'reset' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playDice() {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'play' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDice() {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'stop' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 주사위 표시를 위한 dots 배열 생성
|
||||||
|
function getDiceDots(number: number): boolean[][] {
|
||||||
|
const patterns: Record<number, boolean[][]> = {
|
||||||
|
1: [
|
||||||
|
[false, false, false],
|
||||||
|
[false, true, false],
|
||||||
|
[false, false, false]
|
||||||
|
],
|
||||||
|
2: [
|
||||||
|
[true, false, false],
|
||||||
|
[false, false, false],
|
||||||
|
[false, false, true]
|
||||||
|
],
|
||||||
|
3: [
|
||||||
|
[true, false, false],
|
||||||
|
[false, true, false],
|
||||||
|
[false, false, true]
|
||||||
|
],
|
||||||
|
4: [
|
||||||
|
[true, false, true],
|
||||||
|
[false, false, false],
|
||||||
|
[true, false, true]
|
||||||
|
],
|
||||||
|
5: [
|
||||||
|
[true, false, true],
|
||||||
|
[false, true, false],
|
||||||
|
[true, false, true]
|
||||||
|
],
|
||||||
|
6: [
|
||||||
|
[true, false, true],
|
||||||
|
[true, false, true],
|
||||||
|
[true, false, true]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
return patterns[number] || patterns[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 컴포넌트 언마운트 시 WebSocket 정리
|
||||||
|
$effect(() => {
|
||||||
|
return () => {
|
||||||
|
disconnectWebSocket();
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-4">
|
||||||
|
<div class="max-w-2xl w-full bg-white rounded-2xl shadow-xl p-8">
|
||||||
|
<h1 class="text-4xl font-bold text-center mb-8 text-gray-800">
|
||||||
|
Cloudflare Durable Objects + WebSocket
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- 연결 상태 -->
|
||||||
|
<div class="mb-8 p-4 rounded-lg {isConnected ? 'bg-green-50 border-2 border-green-200' : 'bg-gray-50 border-2 border-gray-200'}">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="w-3 h-3 rounded-full {isConnected ? 'bg-green-500 animate-pulse' : 'bg-gray-400'}"></div>
|
||||||
|
<span class="font-medium text-gray-700">
|
||||||
|
{isConnected ? '연결됨' : isConnecting ? '연결 중...' : '연결 안됨'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{#if !isConnected}
|
||||||
|
<button
|
||||||
|
onclick={connectWebSocket}
|
||||||
|
disabled={isConnecting}
|
||||||
|
class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
{isConnecting ? '연결 중...' : '연결하기'}
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
onclick={disconnectWebSocket}
|
||||||
|
class="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||||||
|
>
|
||||||
|
연결 끊기
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 카운터 디스플레이 -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<div class="text-center mb-6">
|
||||||
|
<div class="text-6xl font-bold text-indigo-600 mb-2">
|
||||||
|
{count}
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-600">전체 카운트</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 버튼들 -->
|
||||||
|
<div class="flex gap-4 justify-center mb-6">
|
||||||
|
<button
|
||||||
|
onclick={incrementCount}
|
||||||
|
disabled={!isConnected}
|
||||||
|
class="px-8 py-3 bg-indigo-500 text-white rounded-lg font-semibold hover:bg-indigo-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all transform hover:scale-105"
|
||||||
|
>
|
||||||
|
카운트 증가 +1
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={resetCount}
|
||||||
|
disabled={!isConnected}
|
||||||
|
class="px-8 py-3 bg-orange-500 text-white rounded-lg font-semibold hover:bg-orange-600 disabled:opacity-50 disabled:cursor-not-allowed transition-all transform hover:scale-105"
|
||||||
|
>
|
||||||
|
리셋
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 통계 정보 -->
|
||||||
|
<div class="grid grid-cols-2 gap-4 mb-8">
|
||||||
|
<div class="bg-blue-50 p-4 rounded-lg border-2 border-blue-100">
|
||||||
|
<div class="text-3xl font-bold text-blue-600 mb-1">
|
||||||
|
{online}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600">실시간 접속자</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-purple-50 p-4 rounded-lg border-2 border-purple-100">
|
||||||
|
<div class="text-lg font-semibold text-purple-600 mb-1">
|
||||||
|
{lastUpdate ? lastUpdate.toLocaleTimeString('ko-KR') : '-'}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-600">마지막 업데이트</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 주사위 성능 테스트 -->
|
||||||
|
<div class="bg-gradient-to-br from-yellow-50 to-orange-50 p-6 rounded-xl border-2 border-yellow-200 mb-8">
|
||||||
|
<h2 class="text-2xl font-bold text-center mb-4 text-gray-800">
|
||||||
|
🎲 Durable Object 성능 테스트
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- 주사위 디스플레이 -->
|
||||||
|
<div class="flex justify-center mb-6">
|
||||||
|
<div class="relative">
|
||||||
|
<div class="w-32 h-32 bg-white rounded-2xl shadow-2xl border-4 border-gray-800 p-4
|
||||||
|
transform transition-all duration-300 {isPlaying ? 'animate-bounce' : ''}">
|
||||||
|
<div class="grid grid-cols-3 gap-2 h-full">
|
||||||
|
{#each getDiceDots(diceNumber) as row}
|
||||||
|
{#each row as dot}
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
{#if dot}
|
||||||
|
<div class="w-4 h-4 bg-gray-800 rounded-full"></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 주사위 번호 표시 -->
|
||||||
|
<div class="absolute -top-3 -right-3 w-10 h-10 bg-red-500 text-white
|
||||||
|
rounded-full flex items-center justify-center text-xl font-bold shadow-lg">
|
||||||
|
{diceNumber}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 플레이 컨트롤 -->
|
||||||
|
<div class="flex gap-4 justify-center mb-4">
|
||||||
|
{#if !isPlaying}
|
||||||
|
<button
|
||||||
|
onclick={playDice}
|
||||||
|
disabled={!isConnected}
|
||||||
|
class="px-8 py-3 bg-green-500 text-white rounded-lg font-semibold
|
||||||
|
hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
|
transition-all transform hover:scale-105 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="text-2xl">▶️</span>
|
||||||
|
<span>플레이</span>
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
onclick={stopDice}
|
||||||
|
disabled={!isConnected}
|
||||||
|
class="px-8 py-3 bg-red-500 text-white rounded-lg font-semibold
|
||||||
|
hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
|
transition-all transform hover:scale-105 flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span class="text-2xl">⏸️</span>
|
||||||
|
<span>정지</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 상태 표시 -->
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="inline-block px-4 py-2 rounded-full {isPlaying ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'}">
|
||||||
|
{isPlaying ? '🔄 주사위가 1초마다 자동으로 굴러가는 중...' : '⏸️ 정지됨'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 설명 -->
|
||||||
|
<div class="mt-4 p-3 bg-white/50 rounded-lg text-sm text-gray-600">
|
||||||
|
<p class="font-semibold mb-1">💡 성능 테스트:</p>
|
||||||
|
<p>• 플레이 버튼 클릭 시 1초마다 주사위(1-6)가 자동으로 변경됩니다</p>
|
||||||
|
<p>• 모든 값은 Durable Object에 실시간 저장됩니다</p>
|
||||||
|
<p>• 모든 연결된 클라이언트에 즉시 동기화됩니다</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 설명 -->
|
||||||
|
<div class="mt-8 p-4 bg-gray-50 rounded-lg border border-gray-200">
|
||||||
|
<h2 class="font-semibold text-gray-800 mb-2">기능 설명</h2>
|
||||||
|
<ul class="text-sm text-gray-600 space-y-1">
|
||||||
|
<li>✅ Cloudflare Durable Objects로 상태 관리</li>
|
||||||
|
<li>✅ WebSocket으로 실시간 양방향 통신</li>
|
||||||
|
<li>✅ 카운트 버튼 클릭으로 증가</li>
|
||||||
|
<li>✅ 실시간 접속자 수 표시</li>
|
||||||
|
<li>✅ 모든 클라이언트에 실시간 동기화</li>
|
||||||
|
<li>✅ Durable Objects 영구 저장소에 상태 저장</li>
|
||||||
|
<li>🎲 초단위 주사위 자동 롤링으로 성능 테스트</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
23
src/routes/api/counter/+server.ts
Normal file
23
src/routes/api/counter/+server.ts
Normal file
@ -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);
|
||||||
|
};
|
||||||
|
|
||||||
@ -3,10 +3,16 @@ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
|||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
// Consult https://svelte.dev/docs/kit/integrations
|
|
||||||
// for more information about preprocessors
|
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
kit: { adapter: adapter() }
|
kit: {
|
||||||
|
adapter: adapter({
|
||||||
|
routes: {
|
||||||
|
include: ['/*'],
|
||||||
|
exclude: ['<all>']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,8 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "bundler"
|
"moduleResolution": "bundler",
|
||||||
|
"types": ["@cloudflare/workers-types"]
|
||||||
}
|
}
|
||||||
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
|
// 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
|
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
|
||||||
|
|||||||
@ -3,5 +3,14 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [tailwindcss(), sveltekit()]
|
plugins: [tailwindcss(), sveltekit()],
|
||||||
|
ssr: {
|
||||||
|
external: ['cloudflare:workers'],
|
||||||
|
noExternal: []
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['cloudflare:workers']
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
25
wrangler.jsonc
Normal file
25
wrangler.jsonc
Normal file
@ -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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user