PonsWarp가 Rust/WASM 코어로 내려간 뒤 가장 먼저 눈에 띄는 단어 중 하나가 ZIP64입니다.
이름만 보면 압축 기능처럼 보입니다. 하지만 실제 기록을 따라가면 ZIP64는 압축률보다 파일 크기 의미에 가까운 문제였습니다. PonsWarp는 처음부터 unlimited size, 10GB, 100GB, 1TB+ 같은 강한 문장을 들고 시작했습니다.
그 약속을 브라우저에서 직접 전송하려면, 데이터가 흐르는 동안 파일 크기와 offset, central directory 위치를 거짓 없이 기록해야 했습니다. 4GB를 넘는 순간 평범한 ZIP의 32비트 가정은 더 이상 안전한 언어가 아니었습니다.
🧭 평범한 ZIP 가정은 어디에서 깨졌나
ZIP은 오래된 형식이고, 일반적인 작은 파일 묶음에서는 충분히 단순하게 느껴집니다. local file header가 있고, payload가 있고, 뒤쪽에 central directory와 end of central directory가 붙습니다. 문제는 이 구조가 끝부분의 directory를 통해 파일 목록과 크기, offset을 다시 읽는다는 점입니다.
post-19에서 이미 파일 깨짐 문제를 다룰 때, 원본 파일 크기의 합계와 실제 ZIP byte length가 다르면 진행률 계산과 저장 종료 판단이 틀어질 수 있다고 정리했습니다. 압축률이 좋으면 뒤가 null byte로 채워질 수 있고, 압축률이 나쁘면 central directory가 잘릴 수 있습니다. ZIP은 끝부분의 central directory가 살아 있어야 열리는 형식이기 때문에, 이것은 단순한 표시 오차가 아니라 파일 형식 자체를 깨뜨리는 문제였습니다.
ZIP64가 필요한 진짜 이유는 바로 여기에 있습니다. 4GB 이상 파일 지원이라는 커밋 문구는 단지 “큰 숫자도 받자”가 아닙니다. 큰 파일에서는 size와 offset이 더 이상 32비트 필드 안에 정직하게 들어가지 않습니다. 그래서 ZIP64는 32비트 필드에 marker 값을 두고, 실제 64비트 값은 extra field와 ZIP64 EOCD 쪽에 기록합니다.
⚙️ 코드에서 ZIP64는 어떻게 생겼나
pons-core-wasm/src/zip64/constants.rs는 ZIP64의 언어를 작게 보여 줍니다. VERSION_NEEDED_ZIP64는 45이고, 주석은 ZIP64가 version 4.5를 요구한다고 적습니다. ZIP64_MARKER_32는 0xFFFFFFFF, ZIP64_MARKER_16은 0xFFFF입니다.
structures.rs의 build_local_file_header()는 compressed size와 uncompressed size의 32비트 필드에 ZIP64_MARKER_32를 쓰고, ZIP64 extra field에는 uncompressed_size와 아직 모르는 compressed size 자리의 64비트 값을 둡니다. build_data_descriptor()는 CRC32와 compressed/uncompressed size를 64비트로 씁니다.
build_central_dir_header()는 크기뿐 아니라 local header offset도 ZIP64 extra field에 64비트로 넣습니다. 마지막 build_eocd64()는 entry count, central directory size, offset을 64비트로 닫습니다. 이 구조를 보면 ZIP64가 압축 알고리즘이 아니라, 큰 파일의 위치와 크기를 끝까지 잃지 않는 파일 형식의 정직성이라는 점이 분명해집니다.
📦 PonsWarp의 sender worker에서 ZIP64가 맡은 자리
현재 file-sender.worker.ts 상단 주석은 Sender Worker V5를 “Zero-Copy Packet Pool + E2E Encryption + ZIP64”로 설명합니다. 같은 묶음 안에 Rust/WASM CRC32, packet encoding, AES-256-GCM, ZIP64, zero-copy streaming, backpressure가 들어갑니다. 이것은 ZIP64가 UI 장식이 아니라 전송 코어의 일부였다는 뜻입니다.
구체적으로 worker는 Zip64Stream을 import하고, zip64Stream 상태를 둡니다. initZipStream()은 new Zip64Stream(0)으로 STORE 모드 스트림을 만듭니다. 주석도 “압축 없음 - 전송 속도 최적화”라고 적습니다. PonsWarp의 ZIP64 경로는 압축률을 얻으려고 모든 것을 DEFLATE로 밀어 넣는 흐름이 아니었습니다.
파일마다 begin_file(filePath, BigInt(file.size))가 local file header를 만들고, file.stream().getReader()가 읽은 chunk는 process_chunk(value)로 들어갑니다. 파일이 끝나면 end_file()이 data descriptor를 만들고, 전체가 끝나면 finalize()가 central directory와 EOCD64를 붙입니다.
🔄 ZIP64도 backpressure 밖에 있지 않았다
큰 파일 형식만 맞으면 충분하지 않습니다. ZIP64 stream이 빠르게 바이트를 만들어도 WebRTC와 receiver 저장 계층이 그것을 감당하지 못하면 같은 문제가 반복됩니다. 그래서 sender worker에는 ZIP queue watermark가 같이 있습니다. ZIP_QUEUE_HIGH_WATER_MARK는 32MB, ZIP_QUEUE_LOW_WATER_MARK는 8MB입니다.
initZipStream()의 내부 processFilesAsync()는 chunk를 읽어 ZIP64로 처리하다가 currentZipQueueSize가 high watermark를 넘으면 isZipPaused를 true로 두고 resume promise를 기다립니다. 파일 형식의 정직성과 브라우저 런타임의 현실이 같은 함수 안에서 만나는 지점입니다.
ZIP64는 큰 ZIP을 만드는 기능이지만, PonsWarp 안에서는 무한 queue를 허용하지 않는 전송 제어와 같이 움직여야 했습니다.
🧱 크기 의미와 진행률 의미가 갈라진다
ZIP64가 필요해진 또 다른 이유는 진행률과 완료 의미입니다. sender worker의 진행률 계산은 ZIP 모드에서 zipSourceBytesRead / totalSize를 사용합니다. 전송된 ZIP byte 수가 아니라 원본 source byte를 얼마나 읽었는지를 진행률의 기준으로 삼습니다.
이 선택은 조심스럽습니다. ZIP container에는 header, descriptor, central directory, EOCD64가 추가됩니다. 압축을 켜면 compressed size와 uncompressed size도 달라집니다. 그러면 “몇 바이트를 보냈는가”와 “사용자의 원본 파일 중 어디까지 읽었는가”가 같은 숫자가 아닙니다. ZIP64는 이 차이를 숨기는 대신, data descriptor와 central directory에 압축 전후 크기를 따로 남깁니다.
🧪 현재 코드가 보여 주는 보수적 상태
한 가지는 보수적으로 적어야 합니다. 현재 sender worker에는 initZipStream() 위에 “legacy ZIP streaming mode”이고, “Multi-file sends currently use raw manifest chunks so offset resume can stay deterministic”라는 주석이 있습니다. 즉 ZIP64 경로는 제품의 핵심 개념과 구현 흔적으로 중요하지만, 현재 multi-file send의 주 경로가 항상 ZIP streaming이라고 단정하면 안 됩니다.
이 주석은 오히려 좋은 증거입니다. ZIP64는 필요했지만, partial recovery와 deterministic offset resume도 필요했습니다. ZIP archive stream은 파일 형식의 일관성을 제공하지만, 재개 가능한 전송에서는 offset을 어디 기준으로 잡을지가 더 어려워집니다. 원본 파일 offset인지, ZIP container offset인지, 압축 후 byte offset인지가 달라질 수 있기 때문입니다.
PonsWarp가 이 지점에서 raw manifest chunks를 남겼다는 것은, 큰 파일 지원을 위해 ZIP64를 붙였더라도 모든 문제를 ZIP 하나로 해결했다고 말하지 않았다는 뜻입니다. 파일 형식, resume semantics, packet integrity, receiver writer 완료는 서로 다른 경계입니다.
🧭 지금 돌아보는 판단
ZIP64는 화려한 기능이 아닙니다. 사용자는 보통 ZIP64라는 말을 보지 못합니다. 하지만 PonsWarp 같은 제품에서는 보이지 않는 이 형식 선택이 신뢰의 바닥을 만듭니다.
4GB를 넘는 파일을 다루겠다고 말하는 순간, 평범한 ZIP의 32비트 size와 offset 가정은 제품 약속을 감당하지 못합니다. 파일 여러 개를 한 stream으로 보내고, 마지막에 central directory를 붙이고, CRC32와 compressed/uncompressed size를 기록하고, 실제 archive가 열려야 한다면 ZIP64는 선택지가 아니라 계약에 가깝습니다.
PonsWarp에서 ZIP64가 필요해진 진짜 이유는 큰 파일을 멋지게 압축하기 위해서가 아니라, 큰 파일을 끝까지 정직하게 설명하기 위해서였습니다. 대용량 전송 제품에서 파일 크기는 UI 숫자가 아니라 무결성의 일부입니다.
PonsWarp commit 1b6fb15 — 4GB 이상 파일 지원을 위한 ZIP64 스트리밍 압축 기능 추가PonsWarp commit 7b0301d — sender를 WASM ZIP64 압축으로 마이그레이션PonsWarp/src/workers/file-sender.worker.ts:5-19 — Sender Worker V5가 Zero-Copy, E2E encryption, ZIP64를 같은 코어 경계로 묶음PonsWarp/src/workers/file-sender.worker.ts:31-32 — ZIP queue high/low watermark 32MB/8MBPonsWarp/src/workers/file-sender.worker.ts:371-455 — Zip64Stream(0), begin_file, process_chunk, end_file, finalize 스트리밍 경로PonsWarp/src/workers/file-sender.worker.ts:992-995, 1039-1044 — ZIP 모드 진행률을 zipSourceBytesRead / totalSize로 계산pons-core-wasm/src/zip64/constants.rs:16-27 — ZIP64 version 4.5와 marker 값pons-core-wasm/src/zip64/structures.rs:45-210 — local header, data descriptor, central directory, EOCD64 생성pons-core-wasm/src/zip64/stream.rs:39-196 — 파일별 header/descriptor와 archive footer 생성 흐름'개발 회고' 카테고리의 다른 글
| [PonsWarp] - Merkle tree와 file signature를 왜 넣었나 (0) | 2026.06.24 |
|---|---|
| [PonsWarp] - zero-copy와 메모리 경계 문제 (0) | 2026.06.24 |
| [PonsWarp] - `pons-core-wasm`를 따로 만든 이유 (0) | 2026.06.24 |
| [PonsWarp] - JS로 버티다 Rust로 내려간 순간 (0) | 2026.06.24 |
| [PonsWarp] - 빠른 전송보다 무결성이 먼저라는 판단 (0) | 2026.06.24 |




