[PonsWarp Grid] 방을 먼저 열고, direct와 grid를 가르다
파일을 고르는 순간 방이 열린다. 전송은 direct(직접)와 grid(그리드)로 갈린다. 사용자는 경로를 고르지 않는다. 제품이 고른다. 이 편은 room-first 메시 전송과 direct/grid 분기, 그리고 브라우저 대용량 export가 Blob에서 터진 사건을 함께 읽는다. 공통 주제는 하나다 — 사용자에게 복잡성을 넘기지 않으려면 제품 안쪽에서 얼마나 많은 판단을 대신해줘야 하는가.
이 세 커밋 묶음(room-first, 분기, export 스트리밍)을 한 편에 묶은 이유도 그렇다. 각각은 다른 층의 이야기지만, 흐름으로 읽으면 사용자의 한 번의 클릭이 지나가는 길이다. 파일을 고르면(방이 열리고) 경로가 정해지고(분기) 조각이 흘러서(그리드) 디스크에 저장된다(export). 어느 한 층이 사용자에게 결정을 밀어붙이면 흐름이 끊긴다. 세 층을 한 편에 놓고 보면 어디서도 끊기지 않았음을 확인하는 것이 이 편의 목적이다.
room-first 메시
| 커밋 | 내용 |
|---|---|
a18c5e9 | room-first mesh transfer with direct/grid branching |
930ff6a | 파일/폴더 선택 즉시 전송 룸 오픈 |
e7f4082 | share code를 in-memory 브라우저 레지스트리로 해석 |
UX 원칙: "링크를 만들어 나중에 보내라"가 아니라 파일을 고르는 순간 방이 열리고, share code가 세션 링크로 바로 이어진다. 대시 없는 share code(65758a1)는 복사·붙여넣기 실수를 줄이기 위한 세부 수정이다.
room-first가 왜 이 제품에서 그렇게 중요한지를 보려면 전송 제품의 전통적 흐름을 떠올려보면 된다. 파일을 고르고 → 설정하고 → 링크를 만들고 → 링크를 공유하고 → 상대가 열 때까지 기다린다. 각 단계는 사용자에게 결정을 요구하고, 결정은 이탈 지점이 된다. room-first는 이 흐름을 반으로 자른다. 파일을 고르는 행위 자체가 세션 생성이고, 남은 것은 코드를 건네는 것뿐이다. share code가 세션 링크로 바로 이어지는 것도 같은 논리다 — 링크 만들기와 세션 열기를 별개 절차로 두지 않는다.
"나중에 보내기"와 "지금 보내기"의 차이는 UX만의 차이가 아니다. 나중에 보내기가 가능하려면 링크가 영속적이어야 하고, 링크가 영속적이려면 세션 상태가 저장소에 있어야 하고, 저장소가 있으면 만료 정책·보안·운영이 따라온다. room-first는 이 사슬 전체를 자른다. 세션은 방이 열린 순간부터 방이 닫힐 때까지만 존재하고, 존재 기간이 짧으니 in-memory 레지스트리가 성립한다. UX 원칙 하나가 저장소 설계를 끝까지 끌고 가는 사례다.
구현 쪽에서 주목할 것은 e7f4082의 "in-memory 브라우저 레지스트리"다. share code를 해석하는 테이블이 메모리에 산다는 것은, coordinator 재시작 시 코드가 모두 무효가 된다는 뜻이다. 이는 단점처럼 보이지만 장기 세션을 전제하지 않는 제품 설계와 맞는다 — 전송 세션은 분 단위의 존재고, 영속 코드가 필요한 서비스가 아니면 영속 저장소의 복잡성을 안 들이는 게 맞다. 단순함은 기본값으로 유지하고, 요구가 생기면 그때 저장소를 붙인다. 대시 없는 share code(65758a1) 같은 세부 수정도 같은 방향이다 — 사용자가 코드를 다루는 마찰을 줄이는 것.
direct와 grid의 분기
| 경로 | 조건 | 수송체 |
|---|---|---|
| direct | 소수 피어, 낮은 지연 | WebRTC 직접 |
| grid | 다수 피어, 대용량 | piece mesh (scheduler) |
분기는 "어느 경로가 맞나"를 사용자에게 묻지 않는다. 참여자 수와 파일 크기로 제품이 결정한다.
이 분기가 존재하는 이유부터 보자. direct는 조각 스케줄링 오버헤드 없이 가장 빠른 경로다. 소수의 피어가 작은 파일을 주고받는 상황에서 그리드 메커니즘(Have fanout, rarest-first, coordinator 조율)은 순수 오버헤드다. 반대로 다수 피어가 대용량을 받는 상황에서 direct는 피어당 대역폭의 상한에 묶인다 — 조각을 나눠 받는 그리드가 유일한 답이다. 두 경로 모두 "정답"이고, 잘못된 것은 상황에 맞지 않는 선택이다. 경로 자체에 우열이 없다는 것이 이 표를 읽을 때 첫 번째로 깨달아야 할 점이다.
그래서 설계의 초점은 경로 자체가 아니라 분기 조건에 있다. 참여자 수와 파일 크기 — 이 두 변수는 전송 시작 시점에 이미 알려진 값들이라서 분기 판단이 결정적(deterministic)으로 이루어진다. 사용자에게 모달로 물을 수도 있었겠지만, 그러면 room-first가 줄여준 결정 하나를 다시 되돌려주는 꼴이다. 경로 선택은 전문 지식이 필요한 결정이고, 전문 지식이 필요한 결정일수록 제품이 대신해야 한다.
분기 조건의 결정론성이 갖는 부수적 이득도 있다. 같은 조건이면 항상 같은 경로를 택하니, 전송이 느릴 때 "어느 경로를 탔는지"가 재현 가능한 질문이 된다. 확률적 분기였다면 같은 상황이 경로마다 다르게 동작해 디버깅이 두 배가 된다. 사용자에게 보이지 않는 분기일수록 내부적으로는 결정적이어야 한다 — 이건 UX와 운영의 교집합 원칙이다.
분기의 경계에 있는 애매한 경우도 있을 것이다. 피어가 적지만 파일이 큰 경우, 피어가 많지만 파일이 작은 경우. 전자는 direct가 빠를 수도 있지만 끊길 때의 재개 비용을 생각하면 grid가 안전하고, 후자는 grid로 가도 스케줄러의 오버헤드가 눈에 띄지 않는다. 애매한 지대에서는 보수적으로 grid 쪽으로 기우는 것이 안전한 기본값이다 — 그리드는 direct의 상위 집합처럼 동작할 수 있으니(조각이 소수 피어에만 있어도 분배는 일어난다) 잘못 grid로 간 것의 비용이 잘못 direct로 간 것의 비용보다 작다.
브라우저 export가 Blob에서 터진 날
| 커밋 | 내용 |
|---|---|
f33c8fa | Blob assembly 임계값 너머로 대용량 파일 export 스트리밍 |
eb12e56 | 대용량 export용 saveShowFilePicker 확장자 |
719b3bc | 전송 완료 시 브라우저 다운로드 자동 시작 |
브라우저는 파일 전체를 Blob으로 조립하면 메모리 한계에 부딪힌다. 이건 버그가 아니라 브라우저의 설계다 — 탭의 메모리 상한 안에서 파일 전체를 메모리에 들고 있을 수는 없다. 문제는 전송 파이프라인이 조각을 잘 흘려받아도, 마지막에 "조립"에서 통째로 메모리를 요구하면 그간의 노력이 무의미해진다는 것이다. 조각 분산, 스케줄링, backpressure — 다 잘 돌다가 조립에서 메모리를 초과해 탭이 죽는다. 사용자 눈에는 "전송하다 죽었다"지만 기술적으로는 "수신이 다 끝난 뒤 저장에서 죽었다"다. 실패 지점이 사용자의 귀납(전송이 문제)과 사실(저장이 문제) 사이에 어긋나는 또 하나의 사례로, zip 편의 단일 스트림 버그와 같은 부류다.
f33c8fa가 "Blob 임계값 너머"로 스트리밍 저장 경로를 만든 것은 파이프라인의 마지막 고리를 메모리 없이 잇는 작업이다. 스트림으로 받은 것을 스트림으로 저장한다 — zip 편의 원칙이 수신 쪽 export에도 그대로 적용된 형태다. 작게 고려된 것 하나: "임계값"이라는 표현이 커밋 제목에 있다는 것. 크기가 임계값 아래면 기존 Blob 경로를 쓰고 넘으면 스트리밍 경로를 쓴다는 뜻이다. 작은 파일에 File System Access API의 권한 요청을 강요하는 것보다 Blob으로 즉시 다운로드하는 게 빠르고 간편하기 때문에, 두 경로를 다 두고 크기로 고르는 하이브리드다.
eb12e56의 확장자 처리와 719b3bc의 자동 다운로드 시작은 사소해 보이지만 수신 경험의 완결이다. 스트리밍 저장이 되어도 파일 탐색기에 알맞은 확장자로 저장되지 않으면 사용자는 "받았는데 깨졌다"고 느끼고, 완료 후 수동 클릭이 필요하면 "끝났는지 모르겠다"고 느낀다. 전송 제품의 신뢰감은 이런 마지막 수미상관에서 결정된다.
데모의 잔재를 치우며
이 편의 마무리 커밋들(291d0a9, e95bafa)은 데모 하드코드 제거다. 데모에서 프로덕션으로 가는 길의 잔재 청소다. 하드코드 제거가 별도 커밋으로 분리된 것은 zip 편의 원칙 3(버그 수정과 기능 추가 분리)과 같은 맥락의, 청소를 청소로 남기는 규율이다.
하드코드 제거를 별도의 작업으로 다루는 것의 가치도 기록해두자. 데모 하드코드는 데모에서는 무해하다 — 오히려 데모를 빠르게 만들어준다. 문제는 그 코드가 프로덕션으로 "따라 들어가는" 것이다. 데모 전용 값이 프로덕션 코드에 남아 있으면, 그 값이 언젠가 실제 트래픽과 만나는 순간 원인을 알기 어려운 오동작이 된다. 데모에서 프로덕션으로 넘어가는 전환점마다 청소 커밋을 남기는 것은 "데모 코드와 프로덕션 코드의 경계"를 커밋 히스토리에 명시적으로 새기는 행위다.
이 편이 그리드 시리즈를 닫는다. S3의 세 편을 통틀어 확인한 것은, 브라우저라는 제약 환경에서도 데스크톱과 같은 원칙(플레인 분리, CLI 규율, 스트리밍, 결정적 분기)이 그대로 통한다는 것 — 통하는 방식이 다를 뿐이다. 다음 편은 시리즈 전체를 수평으로 내려다보는 RC1 게이트 — 무엇이 되어야 출시인가 — 다.