<PonsLink/>
작업
글
주제
시리즈
카테고리
연락
⌘K 검색
About
코드와 제품으로

코드와 제품으로

연결되는 세계

연결되는 세계

PonsLink는 만남 뒤 요청을 세션으로 잇고, PonsWarp는 서버 보관 없이 파일을 직접 전송합니다. 이 블로그는 기능 소개보다 설계 판단과 운영 증거를 남깁니다.

연락처

[email protected]github.com/DeclanJeon

운영

콘텐츠는 WordPress headless CMS, 공개 UI는 Next.js입니다.

초안 미리보기
© 2026 PonsLink Blog|Next.js · WordPress REST · Framer Motion
Local-first · Backup-before-deploy

[Blueprint W1] PonsWarp는 왜 서버에 파일을 안 올리는가 — WebRTC DataChannel + Rust/WASM E2E 구조 해부

Declan·2026년 8월 28일

  • Blueprint
  • P2P/분산기술
  • 개발 회고

PonsWarp는 10GB 파일을 서버에 올리지 않는다. WebRTC DataChannel, Rust/WASM 7단계 파이프라인, directFileWriter, PARTITION_ACK/RESUME으로 끊김을 복구하는 구조를 설계도로 해부한다.

# [Blueprint W1] PonsWarp는 왜 서버에 파일을 안 올리는가 — WebRTC DataChannel + Rust/WASM E2E 구조 해부 > Series: `blueprint-ponswarp` (1/5) · Category: `P2P/분산기술` + `개발 회고` · Slug: `blueprint-ponswarp-01-webrtc-datachannel-wasm` > Source: `blueprints/specs/P01_ponswarp_family.md` §01 + `blueprints/01-ponswarp.png` + `ponswarp/ponswarp-signaling-rs` + `ponswarp/pons-core-wasm` — ## 0. TL;DR PonsWarp(`warp.ponslink.com`)는 10GB 파일을 서버에 업로드하지 않는다. 브라우저끼리 직접 연결(WebRTC DataChannel)하고, 서버(Rust Axum)는 시그널링(offer/answer/ICE) + TURN 자격증명만 담당한다. 파일 청킹·재정렬·압축·암호화·무결성 검증은 `pons-core-wasm`(Rust→WASM) 코어가, 디스크 저장은 `directFileWriter`(StreamSaver/File System Access API)가 맡는다. 서버는 바이트를 보지 않는다. 끊기면 `PARTITION_ACK/RESUME_REQUEST`로 이어서 보낸다. 서버에 올리는 유일한 경로는 `Cloud Drop`(R2 presigned) 뿐이다.
PonsWarp 설계도 01-ponswarp
*그림: PonsWarp 원본 설계도. 좌/우 브라우저(Sender/Receiver), 중앙 Rust Axum 시그널링, 굵은 DataChannel 화살표, 점선 PARTITION_ACK/RESUME, 별도 Cloud Drop→R2 흐름.* — ## 1. 문제 정의 — 왜 서버 업로드가 실패하는가 대용량 P2P가 풀어야 할 제약은 3개다. 1. **서버 비용/프라이버시:** 10GB를 서버에 올리면 스토리지·대역폭·법적 책임이 폭증한다. E2E 암호화도 서버가 평문을 보면 의미가 없다. 2. **브라우저 메모리 한계:** 4GB 파일을 `ArrayBuffer`에 들고 있으면 탭이 죽는다. 스트리밍 저장이 필수다. 3. **끊김:** 모바일 백그라운드, NAT 변경, 네트워크 전환에서 연결은 반드시 끊긴다. 재개(resume) 없이 3GB에서 실패하면 처음부터 다시 보내는 UX는 제품이 아니다. PonsWarp의 답은 **”서버는 연결만 도와주고, 바이트는 브라우저끼리, 복구는 오프셋으로”** 다. — ## 2. 설계도 한눈에 — 3가지 화살표 설계도를 3색으로 읽으면 구조가 끝난다. * **얇은 실선 (시그널링):** `Sender/Receiver React SPA → signaling-factory → adapter → WS /ws(roomCode) → Rust Axum` . `offer/answer/ICE/TURN cred` 교환만 한다. 방(룸코드) 상태머신은 서버가 들고 있다. * **굵은 실선 (데이터):** `Browser A ←WebRTC DataChannel→ Browser B` . 이 채널로만 파일 바이트가 흐른다. 서버 릴레이 없음(coturn TURN 경유만, 이 경우에도 바이트는 TURN을 경유하지만 서버 앱이 보지 않는다). * **점선 역방향 (컨트롤):** `Receiver → Sender: PARTITION_ACK(offset, runId), RESUME_REQUEST` . 수신자가 “여기까지 썼다”를 확인한다. 역방향이라 끊김 감지에 유용하다. * **별도 흐름 (Cloud Drop):** `Front → /api → R2 presigned PUT → QR/Drop Code 픽업` . 상대가 오프라인일 때만 서버(R2)에 임시 보관(24h)한다. 기본값은 P2P. 브라우저 내부 스택: `SenderView/ReceiverView → swarmManager/webRTCService → Web Worker(file-sender/receiver.worker) → pons-core-wasm → directFileWriter → 디스크` . 메인 스레드는 UI만, 무거운 일은 Worker+WASM 격리. Rust 서버 내부: `ponswarp-signaling-rs/handlers/ , protocol/ , mesh*.rs(저장소·보안·남용방어), billing.rs(LemonSqueezy/PayPal/Stripe), auth.rs(Google OAuth), admin.rs, database.rs(Postgres 8 migrations)` . AppState 공유, Docker Compose `3500/5502`. — ## 3. 아키텍처 해부 — 레이어별 역할 ### 3.1 시그널링 팩토리/어댑터 (전략 패턴) `swarmManager`가 직접 `WebSocket`을 만지지 않는다. `signaling-factory`가 `adapter`를 생성한다. 테스트에서는 `MockAdapter`, 프로덕션에서는 `AxumWsAdapter`로 교체. 재연결·백오프·룸코드 검증이 어댑터 안에 캡슐화된다. 이게 없으면 UI가 소켓 상태에 오염된다. ### 3.2 Web Worker 분리 `file-sender.worker.ts` / `file-receiver.worker.ts`가 `postMessage`로 메인 스레드와 통신. 500MB 파일을 읽으면서 UI가 60fps를 유지하는 이유다. Worker 내부에서 `pons-core-wasm`을 로드한다. ### 3.3 pons-core-wasm — 바이트가 통과하는 7단계 파이프라인 Rust로 작성, `wasm-pack`으로 빌드된 코어. JS가 하면 느리거나 GC가 터지는 일을 맡는다. “` [File] → 청킹 → 재정렬버퍼 → LZ4 압축 → AES-GCM E2E 암호화 → Reed-Solomon FEC → Merkle트리 → 스트리밍 SHA-256 → [DataChannel] “` * **재정렬 버퍼 + zero-copy 풀:** DataChannel은 순서가 뒤죽박죽 올 수 있다. WASM이 `offset` 기반 링버퍼에 재배열하고, `ArrayBuffer` 복사를 피하는 풀을 쓴다. * **LZ4:** 텍스트/CSV는 2~3배 압축. 이미 압축된 영상/zip은 패스. * **AES-GCM E2E:** 키는 시그널링 서버를 안 거치고 `roomCode` 유도 또는 별도 교환. 서버는 복호화 불가. * **Reed-Solomon FEC:** 패킷 손실 시 재전송 없이 복구. 10개 중 2개 손실까지 복구하는 파라미터. * **Merkle + 스트리밍 SHA-256:** 1GB 파일도 메모리에 안 올리고 청크별 해시 트리로 무결성 검증. 마지막조각에서 루트 해시 비교. ### 3.4 directFileWriter — 메모리를 디스크로 `StreamSaver.js` + `File System Access API` 폴백. 청크가 도착하면 즉시 디스크에 `WritableStream.write()` 한다. 10GB 파일도 힙 100MB 이하로 전송 가능. 실패하면 partial-write로 남고, resume 시점에 이어쓴다. ### 3.5 Cloud Drop (R2 presigned) `POST /api/drop → R2 presigned PUT URL 발급 → 브라우저가 R2에 직접 PUT → Drop Code/QR 생성` . 서버는 URL만 발급하고 바이트를 프록시하지 않는다. 만료(24h) 크론이 R2 객체를 삭제. — ## 4. 로직/알고리즘 딥다이브 — 3개만 깊게 ### 4.1 RTT 기반 혼잡제어 + 백프레셔 (transferFlowControl) TCP처럼 `RTT`를 관측해 `chunkSize`와 `sendRate`를 조절한다. * **관측:** DataChannel `bufferedAmount` + `RTT` 왕복시간. * **제어:** RTT ↑ 또는 `bufferedAmount` 임계치 초과 → `chunkSize` 축소, `setTimeout` 간격 증가. RTT 안정 → 크기 증가. * **백프레셔:** `bufferedAmount > highWaterMark`면 `await`로 송신 일시정지. 수신자 `PARTITION_ACK`가 와야 재개. 브라우저가 OOM으로 죽는 걸 막는 최후의 방벽. > Rejected: 고정 64KB 청크 + 무제한 `send()` → 고대역폭에서 빠르지만 저대역폭/모바일에서 패킷 손실 폭증, 탭 크래시. ### 4.2 PARTITION_ACK / RESUME_REQUEST 상태머신 “` Sender: [SEND partition N offset=X] ──→ Receiver Receiver: [write to disk offset=X len=Y] ──PARTITION_ACK(offset=X+Y, runId=R)──→ Sender … (연결 끊김) … Receiver (재접속): [RESUME_REQUEST runId=R lastOffset=X+Y] ──→ Sender Sender: [offset=X+Y부터 재전송, 중복 버림] “` * `runId`로 재전송 세션 구분. 이전 세션의 늦게 도착한 ACK는 버린다. * 수신자는 `partial-write`를 파일 끝에 남긴다. `offset`은 디스크에 실제로 쓴 바이트 기준. 메모리 기준이 아니라서 전원 꺼짐에도 안전. * `DOWNLOAD_COMPLETE`는 Merkle 루트 검증 후에만 보낸다. 중간에 끊기면 미완료로 남는다. ### 4.3 TURN credential TTL 발급 `POST /turn/credentials`가 `coturn`용 `username: expiry:hmac`를 발급. TTL 1시간. 시그널링 WS 연결 시 같이 내려준다. 클라가 `RTCPeerConnection(iceServers=[{urls: turn:…, credential}])`로 사용. 서버는 릴레이 바이트를 보지 않고, 자격증명만 짧게 발급해 남용을 막는다. — ## 5. 왜 이 선택인가 — Rejected 대안 | 선택 | 채택 | 버린 대안 | 버린 이유 | |—|—|—|—| | 전송 | WebRTC DataChannel P2P | WebSocket 서버 릴레이 | 서버 대역폭 비용, E2E 암호화 불가, 10GB 업로드 UX | | 코어 | Rust/WASM | 순수 JS 청킹 | JS GC/복사 오버헤드, 1GB 이상에서 프레임 드랍, 암호화 속도 3~5배 느림 | | 저장 | 스트리밍 directFileWriter | Blob 누적 후 `a[download]` | 2GB 이상에서 메모리 초과, 모바일에서 탭 킬 | | 시그널링 | adapter/factory 분리 | 컴포넌트 내 직접 `new WebSocket` | 재연결/테스트/룸코드 검증 로직이 UI에 스파게티 | | 백업 | R2 presigned Cloud Drop | 서버가 파일 받아서 S3 업로드 | 서버 메모리 2배, 타임아웃, 프라이버시 리스크 | — ## 6. 실패와 복구 — 운영에서 배운 것 * **실패 1: 시그널링 서버가 바이트를 보면 안 된다.** 초기 프로토타입에서 디버깅 편의상 서버에 로그를 남기려다 E2E 원칙 위반. `mesh*.rs`에 `남용방어·레이트리밋`만 남기고 페이로드는 절대 로깅하지 않는 규칙으로 고정. * **실패 2: 고정 청크로 저대역폭에서 죽음.** `ponswarp-lan-10mbps` 포크로 10Mbps 제약을 걸고 측정. 고정 256KB 청크는 재전송 폭증. 이후 `networkAdaptiveController` 도입(다음 편 W3에서 다룸). * **실패 3: 모바일 백그라운드 복귀 시 중복 전송.** 포그라운드 복귀 시 `RESUME_REQUEST`를 재전송 + sender가 `offset` 기준으로 중복 버리는 로직 추가. `lan-10mbps`에서 검증. * **복구 증거:** `Docker Compose 3500(front)/5502(signaling)` 헬스체크 `/health,/ready`, Postgres 마이그레이션 8종, `ADMIN_EMAILS` 화이트리스트, `LemonSqueezy/PayPal/Stripe` 빌링 웹훅 멱등 처리. — ## 7. 운영 증거 — 숫자와 명령 * **스택:** React19+Vite+Tailwind+Framer Motion+Three.js / Rust Tokio Axum / Postgres / coturn / R2 / simple-peer * **제약:** 무료 10GB/24h, 멀티파일/폴더는 ZIP64로 묶음, 유료 플랜은 빌링 웹훅으로 활성화 * **검증:** `cargo test` + `wasm-pack test` + `Playwright` E2E(다음 편 `vnext`의 `verify` 체인) * **보안:** Google OAuth, TURN TTL 1h, R2 presigned 만료, E2E AES-GCM — ## 8. 다음 편 예고 **W2: `ponswarp-bulk-plane-vnext` — 헥사고날로 찢은 전송 코어.** `transfer/ports.ts/state.ts/useCases.ts`에 `hybridBulkTransport(WebRTC+LAN TCP)`, `crypto-plane.worker` 분리, `contracts/protocol/v1/messages.json` 28종 고정, `SpeedSender/Receiver speedFrames`를 다룬다. 설계도가 어떻게 코드 계약이 되었는지 보여준다. — ### 부록: 재현 경로 * 설계도: `blueprints/01-ponswarp.png` (프롬프트: `prompts.part1.json#01-ponswarp`) * 명세: `blueprints/specs/P01_ponswarp_family.md` §01 * 코드 진입점: `ponswarp/PonsWarp/src/services/webRTCService.ts`, `ponswarp/pons-core-wasm/src/lib.rs`, `ponswarp/ponswarp-signaling-rs/src/handlers/ws.rs` * 로컬 실행: `compose.yaml up` → `http://localhost:3500` (front) + `ws://localhost:5502/ws` *이 글은 설계도 풀이 8부작 중 1편이다. 시리즈 전체: `blueprint-ponswarp`(5) + `blueprint-ponslink`(3). 다음 글에서 `vnext`의 헥사고날 리팩터링을 이어간다.*

Related

관련 운영 노트

같은 주제에서 이어서 읽기 좋은 글입니다.

  • [Blueprint L3] Pons 포트폴리오 — 의존성 0, 단일 index.html이 오래 가는 이유

    의존성 0, 단일 index.html 41KB. 디자인 토큰 SSOT와 13개 제품 쇼케이스로 만든 정적 포트폴리오가 오래 가는 이유.

  • [Blueprint L2] PonsLink Control Tower — Threads에 dry-run으로 쏘는 이유

    Python Threads 컨트롤타워. cardnews 재사용→생성 폴백, n8n 패치, dry-run 기본 게이트로 안전하게 쏘는 자동발행.

  • [Blueprint L1] PonsLink P2P Room — WebRTC 화상회의를 BFF로 감싼 이유

    React SPA와 Express BFF가 Socket.IO로 시그널링하고, 미디어는 mesh로 가는 2티어. TURN 그랜트와 presence lease로 룸을 만든 화상회의.

댓글

아직 댓글이 없습니다.

현재 이 글의 댓글은 닫혀 있습니다.