개발 회고

[PonsWarp] - 전송 완료라고 뜬다고 정말 끝난 게 아니었다

AC 2026. 6. 24. 00:21

PonsWarp 초기에 진짜 문제는 마지막 청크를 보내는 일이 아니라, 그 파일이 받는 쪽에서 정말 저장됐다고 말할 수 있는 상태를 만드는 일이었습니다.

보내는 쪽 진행률이 100%에 닿았다고 해서 전송이 끝난 것은 아니었습니다. 받는 쪽에서는 아직 복호화와 검증과 저장이 남아 있었고, 이 간극을 무시하면 제품은 금방 거짓말을 하게 됩니다.

PonsWarp direct transfer overview
보내는 쪽에서 끝난 것처럼 보여도, 받는 쪽 로컬 저장은 별도의 마지막 절차였습니다.

💡 전송이 끝났다는 말은 저장이 끝났다는 말이 아니었습니다

💡 PonsWarp에서 완료는 네트워크 이벤트 하나로 끝나는 문장이 아니었습니다. 받는 쪽 브라우저가 파일을 실제로 붙잡은 뒤에야 비로소 쓸 수 있는 문장이었습니다.

초기 PonsWarp의 약속은 분명했습니다. 브라우저끼리 직접 파일을 보내고, 서버 저장을 거치지 않으면서도 큰 파일을 버티는 쪽이었습니다. 2025년 11월 20일 루트 커밋 이후의 흐름을 보면 WebRTC, TURN/STUN, 대용량 전송, UI 정리 같은 문제들이 거의 동시에 올라옵니다.

하지만 사용자가 보는 전송 상태와 시스템이 실제로 끝났다고 인정할 수 있는 상태는 달랐습니다. sender 쪽에서는 마지막 패킷을 보낸 순간이 중요해 보이지만, receiver 쪽에서는 그 뒤에 검증과 저장이 남아 있었습니다. 특히 브라우저에서는 이 뒤 단계가 생각보다 길고 민감했습니다.

📐 시스템 구조로 보면 왜 다른 사건이 되는가

sender worker가 데이터를 보내는 것과 receiver 쪽 writer가 데이터를 저장하는 것은 같은 층의 사건이 아니었습니다. 그래서 PonsWarp는 완료 상태도 둘로 나눠서 다뤄야 했습니다.

지금 코드 구조를 보면 이 차이가 더 선명합니다. src/workers/file-sender.worker.ts는 청크 생성, 패킷 인코딩, 암호화, ZIP64, 백프레셔(backpressure) 같은 sender 쪽 책임을 한꺼번에 집니다. 반면 src/workers/file-receiver.worker.ts는 패킷을 받고, 필요한 경우 WASM 기반 검증과 복호화를 거친 뒤, 그 다음에야 메인 스레드로 write-chunk를 넘깁니다.

그리고 실제 저장은 또 다른 계층인 src/services/directFileWriter.ts가 맡습니다. 이 파일은 단순 저장 유틸이 아니라 StreamSaver, File System Access API, Blob fallback, OPFS fallback을 브라우저와 파일 크기에 맞춰 갈라 쓰고, 32MB와 16MB 워터마크를 기준으로 pause/resume까지 걸어 둡니다. 네트워크가 끝났다고 해서 저장도 끝난 것이 아니라는 사실이 여기서 드러납니다.

PonsWarp completion sequence
sender 기준 완료와 receiver 기준 완료가 같은 순간이 아니었기 때문에 save-complete ack가 필요했습니다.

⚙️ 2025년 11월 21일, 완료 상태 정의가 바뀌었다

`파일 전송 완료 후 수신자 저장까지 대기하는 양방향 핸드셰이크 기능 구현`이라는 커밋 문장은, PonsWarp가 완료의 기준선을 네트워크에서 저장으로 옮기기 시작한 순간을 아주 잘 보여줍니다.

2025년 11월 21일의 21dc9ba 커밋은 sender가 모든 청크를 보낸 뒤 바로 빠져나가지 않고 receiver의 저장 완료 신호를 기다리게 만들었습니다. 수신자는 저장 완료 뒤 DOWNLOAD_COMPLETE를 되돌려 보내고, sender 화면에는 REMOTE_PROCESSING 상태가 추가됩니다.

이건 상태값 하나 추가한 일이 아니었습니다. 제품이 무엇을 완료라고 부를 것인가를 바꾼 결정이었습니다. 빠르게 성공한 것처럼 보이는 감각보다, 지금은 아직 상대가 저장 중이라는 사실을 더 먼저 보여주기로 한 셈입니다.

🐛 핸드셰이크 하나로 끝난 일은 아니었습니다

바로 다음 날의 db175bf8aff234를 보면 파일 손상 문제가 이어집니다. 둘 다 WebRTC 수신부 버퍼 처리 오류와 패킷 범위 문제를 다루고 있습니다. byteOffset, ArrayBuffer 범위 복제, 패킷 크기 정확히 일치 검증 같은 표현이 등장합니다.

즉 save-complete handshake를 넣었다고 곧바로 안전해진 것이 아니었습니다. 오히려 완료 상태를 더 엄격하게 잡고 나니, 그 완료를 진짜라고 부르기 위해 수신부 버퍼와 무결성 문제를 더 깊이 파야 했습니다. 이 시점부터 PonsWarp는 빠르게 끝났다고 보이는 제품보다, 정말 끝난 상태를 입증할 수 있는 제품 쪽으로 기울기 시작합니다.

PonsWarp receiver pipeline
receiver 쪽에서는 packet 수신 이후에도 검증, 재정렬, 저장, flush가 남아 있었습니다.

🔄 이 판단은 partial file을 그냥 남기지 않는 쪽으로 이어졌다

이 흐름이 잠깐 반짝하고 사라진 것도 아닙니다. 2026년 5월 12일의 b185563Make P2P transfers recover instead of saving partial files라고 말합니다. 부분 파일을 그냥 남기는 것보다 recover 가능한 계약으로 가겠다는 판단입니다.

초기의 저장 완료 handshake와 나중의 recovery 정책은 떨어져 있지 않았습니다. 둘 다 같은 질문을 붙잡고 있습니다. 지금 이 파일을 정말 성공이라고 불러도 되는가. PonsWarp는 그 질문 앞에서 생각보다 보수적인 쪽을 택했습니다. 속도보다 신뢰를 먼저 잡겠다는 선택입니다.

💡 이 시점부터 PonsWarp의 완료 상태는 단순 progress 숫자가 아니라, 무결성과 저장 성공까지 포함한 제품 계약으로 바뀌었다고 보는 편이 맞습니다.

📚 지금 돌아보면 더 중요했던 것은 속도가 아니라 정직함이었다

그때는 chunk 크기, 배치 쓰기, WebRTC 버퍼, 멀티 채널, ZIP64 같은 것들이 더 눈에 띄었을 겁니다. 실제로 그런 문제들도 중요했습니다. 하지만 제품의 말투를 정한 것은 다른 쪽이었습니다. 정말 끝나지 않았는데 끝났다고 말하지 않는 것이었습니다.

PonsWarp가 나중에 메모리 압력, backpressure, reordering buffer, partial recovery까지 깊게 들어가게 된 것도 우연처럼 보이지 않습니다. `완료`라는 단어를 함부로 쓰지 않겠다는 결정이 먼저 있었기 때문에, 그 단어를 지키기 위해 나머지 설계가 따라온 것으로 읽힙니다.

📚 읽은 코드
PonsWarp/src/workers/file-receiver.worker.ts:6-10 — receiver worker가 검증, 복호화, 메인 스레드 전달을 별도 책임으로 다루는 구조
PonsWarp/src/services/directFileWriter.ts:46-52 — 저장 계층이 고수위/저수위 워터마크로 흐름 제어를 거는 지점
PonsWarp/src/services/directFileWriter.ts:230-236 — 브라우저별 저장 전략 차이가 제품 신뢰와 연결되는 지점
PonsWarp/src/components/ReceiverView.tsx:181-189 — receiver 완료 상태가 실제 UI에 닿는 지점
LIST