PonsWarp 초반의 전송 문제는 단순히 WebRTC가 연결되느냐 아니냐에서 끝나지 않았습니다.
연결은 됐는데 속도가 들쭉날쭉했고, 진행률은 움직이다 멈칫했고, 빠른 네트워크에서도 브라우저 내부 큐가 리듬을 잃으면 사용자는 앱이 굳었다고 느꼈습니다. 2025년 11월 21일의 6e635f3는 그 불편한 감각을 정면으로 건드린 커밋입니다.
⚡ Pull 방식은 왜 답답했는가
초기 전송 구조는 이해하기 쉬웠습니다. 메인 스레드가 WebRTC DataChannel의 상태를 보고, worker에게 다음 청크를 달라고 요청하고, worker가 파일을 읽어 패킷을 만들어 돌려보냅니다. 받는 쪽도 비슷하게 순서대로 받아 처리합니다.
이 구조의 장점은 안전함입니다. 한 번에 너무 많이 읽지 않고, 요청이 들어올 때만 파일을 잘라 보내기 때문입니다. 하지만 사용자가 느끼는 제품 경험에서는 곧 한계가 보입니다. 요청이 늦으면 전송이 굶고, 큐가 비면 링크가 잠깐 놀고, 네트워크가 빠른 환경에서도 애플리케이션 레벨의 왕복이 전송 리듬을 잘라 먹습니다.
6e635f3 이전의 sender worker는 pull 요청을 스케줄링하고, 중복 읽기를 막고, 배치 하나를 처리한 뒤 다시 요청을 기다리는 쪽에 가까웠습니다. 이것은 대용량 파일을 다루는 초반 안전장치로는 자연스럽지만, 제품 입장에서는 “연결은 됐는데 왜 속도가 계속 출렁이는가”라는 질문을 피하기 어렵습니다.
🔄 Push 방식은 전송의 주도권을 바꿨다
Push 방식의 변화는 단순합니다. worker가 “요청받으면 하나 만든다”에서 “보낼 수 있는 창이 남아 있으면 계속 만든다”로 이동합니다. 6e635f3의 worker diff에는 sendLoop, pendingChunks, windowSize, network-congestion 같은 단어가 등장합니다. 이 이름들만 봐도 전송 모델이 달라졌다는 것을 알 수 있습니다.
기존 Pull 모델에서는 메인 스레드가 worker를 깨우는 쪽에 가까웠습니다. Push 모델에서는 worker가 자기 안에 전송 루프를 갖고, 현재 outstanding chunk 수가 congestion window보다 작으면 다음 청크를 읽어 보냅니다. 이때 메인 스레드는 단순 통로가 아니라, WebRTC 버퍼가 차거나 send가 실패했을 때 혼잡 신호를 되돌려 주는 피드백 지점이 됩니다.
📊 AIMD는 브라우저 안에 작은 교통 규칙을 세웠다
AIMD는 Additive Increase, Multiplicative Decrease의 약자입니다. 여유가 있을 때는 조금씩 늘리고, 혼잡이 감지되면 크게 줄이는 방식입니다. 네트워크 제어에서 오래된 아이디어지만, PonsWarp에서는 WebRTC DataChannel과 브라우저 메모리라는 현실적인 제약 안에 들어왔습니다.
6e635f3의 worker 구현은 TCP Reno 스타일에 가깝게 window를 다룹니다. ACK를 받으면 RTT를 갱신하고, slow start 상태에서는 window를 빠르게 키우며, 혼잡 회피 상태에서는 완만하게 늘립니다. 반대로 network-congestion 메시지를 받거나 timeout이 나면 threshold를 window의 절반으로 낮추고, window를 그 threshold로 줄입니다.
이후 현재 코드에서는 제어가 src/services/networkAdaptiveController.ts로 더 정리되어 있습니다. cwnd는 128KB에서 시작하고, WebRTC stats의 currentRoundTripTime으로 RTT를 샘플링합니다. RTT 비율이 2.0을 넘거나 현재 bufferedAmount가 cwnd보다 커지면 혼잡으로 보고 window를 줄입니다.
🧩 시스템 구조는 점점 네 층으로 갈라졌다
이 시기의 PonsWarp는 이미 단순한 React 앱 하나로 설명하기 어려워지고 있었습니다. 위에는 React UI와 상태 표시가 있고, 그 아래에는 sender worker와 receiver worker가 있습니다. 전송 중간에는 simple-peer 기반 WebRTC DataChannel이 있고, 더 아래에는 패킷 인코딩, CRC, 나중의 암호화와 ZIP64, 재정렬 버퍼 같은 코어 로직이 붙습니다.
Push와 AIMD는 이 중 브라우저 전송 엔진 층의 문제였습니다. 하지만 영향은 UI까지 올라옵니다. 전송이 끊기지 않고 흐르면 progress bar와 속도 표시가 덜 불안해집니다. 반대로 window가 너무 커지면 receiver 저장 계층이 따라오지 못하고, 나중에 backpressure와 watermark 제어가 필요해집니다.
sender를 쉬지 않게 만드는 것, receiver를 터지지 않게 만드는 것, 네트워크가 느려졌을 때 멈추는 것처럼 보이지 않게 하는 것. 셋은 서로 분리된 기능이 아니었습니다.
⚖️ 그때의 선택은 맞았지만 충분하지는 않았다
지금 돌아보면 Push 방식 도입은 필요한 방향이었습니다. Pull만으로는 빠른 네트워크에서 sender가 지나치게 소극적이었고, 사용자는 앱이 네트워크를 제대로 쓰지 못한다고 느꼈을 가능성이 큽니다. 대용량 파일 전송 제품에서 이런 체감은 치명적입니다.
하지만 Push는 위험한 도구이기도 합니다. window 계산이 틀리면 브라우저 DataChannel 큐가 부풀고, receiver 저장 큐가 밀리고, 결국 메모리와 GC가 제품 경험을 망가뜨립니다. 실제로 현재 constants.ts에는 나중의 판단이 남아 있습니다. MAX_BUFFERED_AMOUNT는 4MB로 보수화되어 있고, BATCH_SIZE_MAX도 1로 묶여 있습니다.
🧠 실제 사용자에게 바뀐 것은 숫자보다 신뢰감이었다
사용자는 AIMD라는 단어를 보지 않습니다. congestion window도 보지 않습니다. 사용자가 보는 것은 전송이 시작된 뒤의 리듬입니다. 진행률이 자주 멈추는지, 속도 표시가 말이 되는지, 큰 파일을 보낼 때 브라우저가 불안하게 흔들리는지, 상대가 받는 동안 UI가 솔직하게 상태를 보여 주는지입니다.
Push 방식은 빠른 구간에서 전송이 더 살아 있게 만들었습니다. AIMD는 그 빠름이 무모함으로 바뀌지 않도록 제동 장치를 붙였습니다. 그리고 이후 backpressure, save-complete handshake, partial recovery로 이어진 흐름은 모두 같은 제품 감각을 향합니다. “빠른 척”보다 “정말 보낼 수 있는 만큼 보내고, 정말 끝났을 때 끝났다고 말하기”입니다.
PonsWarp commit 6e635f3 — Push 방식 도입과 AIMD 혼잡 제어 구현의 시작점PonsWarp/workers/file-sender.worker.v1.ts at 6e635f3 — sendLoop, pendingChunks, handleAck, handleNetworkCongestion 구조PonsWarp/services/webRTCService.ts at 6e635f3 — WebRTC bufferedAmount가 커졌을 때 worker에 network-congestion을 되돌려 주는 경로PonsWarp/src/services/networkAdaptiveController.ts:38-215 — 현재 코드의 RTT 기반 cwnd 조절과 batch size 계산PonsWarp/src/services/swarmManager.ts:756-817 — WebRTC stats 샘플링과 worker adaptive config 갱신PonsWarp/src/utils/constants.ts:18-29 — 안정성 우선으로 보수화된 DataChannel queue와 partition 정책'개발 회고' 카테고리의 다른 글
| [PonsWarp] - 초반 UI보다 먼저 해결해야 했던 것들 (0) | 2026.06.24 |
|---|---|
| [PonsWarp] - 파일 손상은 UX 문제가 아니라 신뢰 문제였다 (0) | 2026.06.24 |
| [PonsWarp] - TURN 없이는 모바일이 안 붙는다는 현실 (0) | 2026.06.24 |
| [PonsWarp] - 전송 완료라고 뜬다고 정말 끝난 게 아니었다 (0) | 2026.06.24 |
| [PonsWarp] - 메모리 폭발을 처음 마주했을 때 (0) | 2026.06.24 |




