PonsWarp를 처음 떠올렸을 때 가장 크게 붙잡은 문장은 단순했습니다. 브라우저끼리 바로 큰 파일을 보내면 된다는 문장이었습니다.
서버에 올렸다가 다시 내려받는 과정을 건너뛰고, 방을 만들고, 상대가 들어오면 WebRTC 데이터 채널로 파일을 흘려보내면 된다. 제품 설명으로는 꽤 깨끗했습니다. 지금 돌아보면 그 문장 안에는 몇 가지 착각이 숨어 있었습니다.
WebRTC를 쓰면 서비스가 가벼워질 것이라는 착각, 대용량 파일은 청크로 자르면 된다는 착각, 전송 완료 이벤트가 곧 사용자 완료라는 착각, 서버는 signaling만 하면 된다는 착각입니다. 각각은 완전히 틀린 말은 아니었습니다. 그래서 더 위험했습니다. 절반만 맞는 전제는 제품을 가장 늦게 망가뜨립니다.
💡 첫 번째 착각: P2P면 제품도 단순해진다
2025-11-20 ad322f3 feat : first commit에서 시작한 PonsWarp는 같은 날 c647df9 feat: WebRTC 서비스 개선 및 프로덕션 로깅 시스템 도입, 4efb394 chore : 모든 호스트 들어올 수 있도록 허용, stun 서버 추가, constatns에서 로컬호스트와 포트 제거로 이어졌습니다. 하루 뒤에는 2025-11-21 2def330 feat: TURN 서버 설정을 동적으로 가져와 모바일 WebRTC 연결 안정화가 붙었습니다.
처음에는 P2P라는 말이 제품 구조를 줄여 줄 거라고 생각했습니다. 서버 저장소가 빠지고, 업로드 대기열이 빠지고, 다운로드 만료 정책이 빠지면 제품이 훨씬 단순해질 것 같았습니다. 사용자가 파일을 고르고, 상대에게 링크나 방 코드를 보내고, 상대가 들어오면 곧바로 전송하는 흐름만 남는다고 봤습니다.
그런데 사용자 플로우가 단순하다는 말과 시스템 플로우가 단순하다는 말은 전혀 다릅니다. PonsWarp에서 사용자가 보는 흐름은 SEND, RECEIVE, 나중에는 CLOUD Drop 선택으로 정리됩니다. 하지만 그 아래에는 room 생성, URL 라우팅, signaling 연결, offer와 answer 교환, ICE candidate 전달, STUN 조회, TURN fallback, DataChannel 열림, 전송 상태 동기화가 깔립니다.
현재 src/App.tsx만 봐도 이 차이가 드러납니다. 앱은 /receive/:roomId와 /cloud/:shareId를 URL에서 읽어 각각 receiver 모드와 Cloud receiver 모드로 보냅니다. 동시에 앱이 뜨면 signaling 서버에 연결하고, 연결 실패는 사용자에게 toast로 노출합니다. 표면에서는 모드 전환처럼 보이지만, 실제로는 라우팅, 세션 상태, signaling 상태가 같은 제품 경험 안에 묶여 있습니다.
⚙️ 두 번째 착각: WebRTC 연결이 되면 전송 서비스가 된다
WebRTC 연결이 성립하면 절반은 끝났다고 생각하기 쉽습니다. 특히 작은 파일이나 데모에서는 그렇게 보입니다. 두 브라우저가 붙고, DataChannel이 열리고, 바이트가 흐르면 성공처럼 보입니다. 하지만 PonsWarp의 목표는 몇 MB짜리 데모 전송이 아니라 아주 큰 파일과 폴더를 다루는 서비스였습니다.
대용량 전송에서는 연결보다 이후가 더 무겁습니다. sender는 파일을 읽어 chunk로 나누고, worker에서 packet을 만들고, 필요하면 압축과 암호화 경계까지 생각해야 합니다. receiver는 받은 packet을 순서대로 해석하고, 검증하고, 저장 계층으로 넘겨야 합니다. 이 사이에 브라우저 메모리와 DataChannel buffer가 끼어 있습니다.
2025-11-20 638bc83 feat: 대용량 파일 다운로드 시 메모리 폭발 방지 기능 추가가 매우 빠르게 등장한 이유도 여기에 있습니다. 청크로 나누는 것만으로는 충분하지 않았습니다. 청크가 너무 빨리 쌓이면 메모리가 터지고, 너무 느리면 전송 속도가 무너집니다.
현재 src/components/SenderView.tsx는 단순 업로드 폼이 아닙니다. 파일과 폴더 입력, manifest 생성, QR 공유, 연결된 peer 목록, 준비된 peer, 완료된 peer, 대기 중 peer, 전송 속도와 남은 시간까지 추적합니다. 한 명에게 보내는 것처럼 시작했지만, 실제 제품은 sender가 여러 receiver 상태를 이해해야 했습니다.
📦 세 번째 착각: 완료 표시는 사용자가 믿어도 되는 상태다
“전송 완료”는 네트워크 이벤트가 아니라 사용자 신뢰의 계약이었습니다. receiver가 파일을 안전하게 닫기 전까지는 끝났다고 말하면 안 됐습니다.
파일 전송에서 가장 위험한 착각은 완료 표시입니다. sender 입장에서는 모든 chunk를 보냈으면 끝난 것처럼 느껴집니다. receiver 입장에서도 마지막 packet을 받으면 끝난 것처럼 보입니다. 하지만 사용자가 실제로 원하는 것은 전송 이벤트가 아니라 안전하게 저장된 파일입니다.
이 차이는 2025-11-21 21dc9ba feat: 파일 전송 완료 후 수신자 저장까지 대기하는 양방향 핸드셰이크 기능 구현에서 분명해집니다. 완료는 단일 이벤트가 아니라 sender와 receiver가 서로 다른 완료 조건을 확인해야 하는 상태였습니다.
사용자는 진행률 100%를 보면 파일을 믿습니다. 그런데 receiver의 디스크 쓰기가 아직 끝나지 않았거나, 브라우저 저장 API가 닫히지 않았거나, 마지막 검증이 끝나지 않았다면 100%는 거짓말이 됩니다. 이런 거짓말은 속도보다 더 치명적입니다.
🌐 네 번째 착각: 서버는 signaling만 하면 된다
PonsWarp를 시작할 때 서버는 가벼운 중개자로 남을 수 있다고 봤습니다. 방을 만들고, offer와 answer를 전달하고, ICE candidate를 넘기면 됩니다. 데이터는 브라우저끼리 직접 흐르니 서버는 제품의 핵심 비용에서 비켜나 있을 것 같았습니다.
하지만 4efb394의 STUN 서버 추가와 2def330의 TURN 동적 설정은 바로 그 믿음을 흔듭니다. 연결 성공률은 제품 품질입니다. 모바일 네트워크, 회사망, 엄격한 NAT 환경에서 직접 연결이 실패하면 사용자는 WebRTC의 철학을 이해해 주지 않습니다. 그냥 안 되는 서비스라고 느낍니다.
TURN은 이 문제를 줄여 주지만, 공짜가 아닙니다. 대역폭 비용이 생기고, 자격 증명 관리가 필요하고, 서버 상태와 지역 지연도 제품 문제가 됩니다. 나중에 ponswarp-signaling-rs가 따로 만들어지고 /health, /ready, cleanup, Cloud Drop, billing 같은 영역까지 넓어진 것도 같은 흐름입니다.
🧭 제품 구조는 처음 생각보다 빨리 네 갈래가 됐다
초기 제품 구조를 단순하게 그리면 sender browser, receiver browser, signaling server 세 개였습니다. 하지만 실제 구조는 곧 네 층으로 갈라졌습니다.
| 층 | 구성 | 깨진 착각 |
|---|---|---|
| Product and UI | React 19, Vite 7, Tailwind 4, Zustand, Sender/Receiver/Cloud views | 화면이 단순하면 제품도 단순하다는 착각 |
| Browser transfer engine | WebRTC, simple-peer, workers, chunk/batch, backpressure | 연결되면 전송 서비스가 된다는 착각 |
| Storage and recovery | StreamSaver, File System Access API, DirectFileWriter, resume | 받으면 저장도 끝났다는 착각 |
| Core and backend | pons-core-wasm, Rust signaling, TURN, readiness, Cloud Drop | 서버는 가벼운 signaling이면 된다는 착각 |
착각은 이 네 층을 처음부터 네 층으로 보지 못했다는 데 있습니다. 사용자에게는 하나의 버튼과 하나의 링크처럼 보여야 하지만, 만드는 쪽에서는 네 층의 실패 조건을 분리해서 봐야 했습니다.
🔍 운영 blind spot은 늦게 온 문제가 아니었다
운영 문제는 제품화 단계에서나 등장한다고 생각했습니다. 하지만 PonsWarp는 첫날부터 프로덕션 로깅이라는 단어를 커밋에 올렸습니다. 그 말은 이미 운영 문제가 시작됐다는 뜻입니다. 연결이 왜 실패했는지, 어느 단계에서 멈췄는지, 사용자는 어떤 상태를 보고 있었는지 모르면 P2P 서비스는 디버깅 자체가 어렵습니다.
특히 WebRTC는 실패가 조용한 편입니다. 사용자는 방에 들어왔는데 연결이 안 됩니다. 또는 연결됐는데 속도가 이상합니다. 또는 모바일에서 잠깐 앱을 내렸다가 돌아오면 흐름이 죽습니다. 이런 문제를 제품이 설명하지 못하면 기술적으로는 정상이어도 서비스로는 실패합니다.
그래서 관찰 가능성, readiness, cleanup, GA4 같은 것들이 나중에 붙은 건 부가 기능이 아니었습니다. 제품이 실제로 쓰이는지, 어디서 멈추는지, 어떤 경로가 실패하는지 알아야 다음 결정을 할 수 있었기 때문입니다.
지금 보면 PonsWarp의 시작이 틀렸다고 말하고 싶지는 않습니다. 브라우저끼리 바로 보내는 경험은 여전히 매력적이고, direct transfer는 제품의 강한 중심축입니다. 다만 시작할 때는 그 경험을 가능하게 하는 비용을 너무 작게 봤습니다.
P2P는 제품을 단순하게 만들어 주는 마법이 아니었습니다. 오히려 서버가 숨겨 주던 복잡도를 브라우저, worker, storage, signaling, 운영 지표 쪽으로 다시 배치하는 선택이었습니다. 그 재배치를 제품 설계로 받아들이기 전까지 PonsWarp는 계속 같은 벽에 부딪혔습니다.
PonsWarp/README.md:26-95 — direct transfer와 Cloud Drop, 기술 스택, storage/core/signaling 구조PonsWarp/src/App.tsx:40-57 — receive/cloud URL 라우팅과 mode 전환PonsWarp/src/App.tsx:77-97 — 앱 시작 시 signaling 서버 연결과 실패 노출PonsWarp/src/components/SenderView.tsx:56-105 — sender 상태, peer 상태, 진행률, 대기열 관리PonsWarp/src/services/signaling-factory.ts:43-76 — Node/Rust signaling 경계와 server URL 선택'개발 회고' 카테고리의 다른 글
| [PonsWarp] - 첫 커밋에서 세운 약속 (0) | 2026.06.24 |
|---|---|
| [PonsWarp] - wormhole-file-gate는 무엇을 시험한 프로젝트였나 (1) | 2026.06.24 |
| [PonsWarp] - `KronDelivery`와 `filetransfer`에서 이미 드러난 집착 (0) | 2026.06.24 |
| [PonsWarp] - 왜 나는 계속 파일 전송 프로젝트를 만들었나 (0) | 2026.06.24 |
| [PonsWarp] - 브라우저 앱인데 왜 코어가 Rust인가 (0) | 2026.06.24 |




