암호화 성능 최적화: 브라우저에서 빠른 암호화
대용량 파일 전송을 위한 브라우저 암호화 성능을 최적화하세요. 스트리밍 암호화, Web Workers, 청크 처리 기술.
브라우저에서 5GB 파일을 UI를 멈추지 않고 암호화하려면 특정 기술 조합이 필요합니다. Web Crypto API를 통한 하드웨어 가속 AES-256-GCM(AES-NI로 초당 1~2GB), 메모리를 제한하는 1~4MB 청크 처리, 메인 스레드를 유지하는 Web Workers, FileReader.readAsArrayBuffer() 대신 File.stream() 방식의 스트리밍 읽기, 그리고 청크를 병렬 암호화할 수 있는 논스 관리가 그것입니다. 순수 JavaScript 암호화 라이브러리는 Web Crypto보다 10~20배 느리므로, 브라우저가 기본 제공하지 않는 알고리즘에만 예약해야 합니다.
최적화 전에 기준 측정하기
최적화 전에 먼저 측정이 필요합니다. Chrome 120에서 2024년 MacBook Air M2로 AES-256-GCM을 사용해 1GB 버퍼를 암호화하면 약 1.7GB/s가 나옵니다. 중급형 Android 폰(Pixel 7)에서는 약 600MB/s, 소프트웨어 전용 AES를 사용하는 2015년형 Intel 노트북에서는 약 250MB/s입니다.
이 수치가 말하는 것은 분명합니다. Web Crypto AES-GCM은 대부분의 암호화 파일 전송 흐름에서 병목이 아닙니다. 실제 병목은 파일 읽기, ArrayBuffer 간 JavaScript 마샬링, 또는 네트워크 업로드입니다. 이쪽을 먼저 최적화하세요.
앱 내 실행할 벤치마크 예시:
const blob = new Uint8Array(1024 * 1024 * 100); // 100 MB
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 }, true, ["encrypt"]
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const start = performance.now();
await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, blob);
console.log(`${(100 / (performance.now() - start) * 1000).toFixed(0)} MB/s`);
JavaScript 라이브러리 대신 Web Crypto 사용하기
AES-GCM, AES-CBC, PBKDF2, HMAC, RSA, ECDH, ECDSA, SHA-256/384/512의 경우, 브라우저의 Web Crypto API는 가능한 곳에서 하드웨어 가속을 사용합니다. x86의 AES-NI, 모바일의 ARMv8 암호화 확장이 그 예입니다. @noble/ciphers 또는 순수 JS crypto-js 같은 JavaScript 라이브러리는 이러한 명령어에 접근할 수 없어 인터프리터에서만 실행됩니다.
1MB 입력에 대한 AES-256-GCM 속도 비교:
- Web Crypto (하드웨어 가속): 1~2GB/s
- libsodium.js WASM: 400~800MB/s
- @noble/ciphers 순수 JS: 100~200MB/s
- crypto-js 순수 JS: 30~80MB/s
5GB 파일의 경우 Web Crypto와 순수 JS의 차이는 약 3초 대 50초입니다. 사용자가 확실히 체감하는 수준입니다. Web Crypto가 지원하는 알고리즘은 항상 Web Crypto를 우선 사용하고, libsodium.js나 argon2-browser 같은 WASM 라이브러리는 ChaCha20-Poly1305나 Argon2id처럼 브라우저가 기본 제공하지 않는 알고리즘에만 사용하세요.
대용량 파일을 위한 청크 처리
500MB 이상의 파일은 대부분의 기기에서 단일 ArrayBuffer에 편하게 담기지 않습니다. 1~4MB 조각으로 나누어 각각 암호화하세요.
const CHUNK_SIZE = 4 * 1024 * 1024; // 4 MB
async function encryptLargeFile(file, key) {
const chunks = [];
let chunkIndex = 0;
for (let offset = 0; offset < file.size; offset += CHUNK_SIZE) {
const chunk = await file.slice(offset, offset + CHUNK_SIZE).arrayBuffer();
const iv = new Uint8Array(12);
new DataView(iv.buffer).setBigUint64(4, BigInt(chunkIndex++));
const ct = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, key, chunk
);
chunks.push({ iv, ct: new Uint8Array(ct) });
}
return chunks;
}
4MB 청크를 선택하는 이유가 있습니다. 더 작은 청크(예: 64KB)는 Web Crypto API 호출 오버헤드가 작은 입력에서 지배적이 됩니다. 더 큰 청크(예: 64MB)는 L2/L3 캐시에 잘 맞지 않아 메모리 압박이 심해집니다. 1~4MB가 데스크톱과 모바일 모두에서 최적의 범위입니다.
Web Workers로 UI 응답성 유지하기
메인 스레드에서의 암호화는 렌더링과 입력을 차단합니다. 약 500ms 이상의 작업은 Web Worker로 오프로드하세요.
// worker.js
self.onmessage = async (e) => {
const { chunk, key, iv } = e.data;
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, key, chunk
);
self.postMessage(ciphertext, [ciphertext]);
};
// 메인 스레드
const worker = new Worker('/worker.js');
worker.postMessage({ chunk, key, iv }, [chunk]);
worker.onmessage = (e) => { /* 암호문 처리 */ };
postMessage의 두 번째 인수로 ArrayBuffer를 전달하면 복사 없이 소유권이 이전됩니다(제로 카피). 전달 없이 클론하면 4MB 청크당 약 20ms의 오버헤드가 추가됩니다. Web Crypto API는 Workers에서도 사용 가능하므로 실제 암호화는 메인 스레드와 동일한 성능으로 실행됩니다.
병렬 청크 암호화
논스가 충돌하지 않는 한 청크별 AES-GCM 암호화는 독립적입니다. 청크 인덱스에서 논스를 결정론적으로 도출하면 여러 Workers에서 병렬로 청크를 암호화할 수 있습니다. 일반 하드웨어에서 약 4개의 Workers 이후에는 수익이 감소합니다. Web Crypto가 너무 빠르기 때문에 병목이 파일 읽기와 스레드 간 메시지 전달로 이동합니다. 복잡성을 추가하기 전에 반드시 벤치마크하세요. 오버헤드로 인해 단일 Worker 순차 암호화가 멀티 Worker 병렬 처리만큼 빠를 수 있습니다.
ReadableStream을 통한 스트리밍
정말 큰 파일(20GB 이상)의 경우 청크조차 한꺼번에 메모리에 로드하지 않도록 File.stream()을 사용하세요. 이렇게 하면 메모리 사용량이 한 번에 하나의 청크로 제한됩니다. 브라우저가 디스크에서 읽고, 암호화하고, 업로드 스트림에 쓰고, 다음 청크를 읽는 순서로 처리합니다. 메모리 사용량은 파일 크기에 관계없이 10MB 미만을 유지합니다.
업로드 동시성
암호화는 업로드와 직렬이 아닌 병렬로 실행됩니다. 청크 N이 업로드되는 동안 청크 N+1이 암호화됩니다. 파이프라인을 사용하되 최대 동시 업로드를 4~8개로 제한하면 브라우저 제한(Chrome/Firefox에서 오리진당 6개 연결)을 초과하지 않으면서 파이프를 효율적으로 채울 수 있습니다.
누락된 알고리즘을 위한 WebAssembly
Argon2id 키 도출이나 ChaCha20-Poly1305의 경우 Web Crypto는 기본 지원이 없습니다. WASM 라이브러리가 이 격차를 메웁니다. libsodium.js는 Argon2id, XChaCha20-Poly1305, crypto_secretstream을 제공하고, argon2-browser는 Argon2만 제공하지만 번들 크기가 더 작습니다. WASM 버전은 대부분의 암호화 작업에서 네이티브 속도의 60~80%를 달성합니다. 초기 페이지 렌더링을 차단하지 않도록 WASM을 동적으로 로드하세요.
진행 상황 보고
대용량 암호화에는 진행 상황 피드백이 반드시 필요합니다. 그렇지 않으면 사용자가 앱이 멈췄다고 생각합니다. 암호화된 바이트를 계산하고 진행 이벤트를 전달하세요. UI 업데이트는 requestAnimationFrame이나 타임스탬프 확인으로 약 10Hz로 조절하세요. 더 잦은 업데이트는 사람이 인지할 수 없는 리페인트에 처리 사이클을 낭비합니다.
메모리 상한과 가비지 컬렉션 압박
각 ArrayBuffer는 참조가 없어질 때까지 메모리를 점유합니다. 4MB 청크 20개를 암호화해 보유하면 약 80MB의 메모리가 고정됩니다. 메모리 제한이 엄격한 모바일 브라우저(iOS Safari는 탭당 200~400MB 상한)에서는 이것이 심각한 문제가 됩니다. 참조를 즉시 해제하고 업로더에 스트리밍하면서 각 청크를 버리세요.
실제 성능 목표치
브라우저 탭에서 1GB 암호화 파일 전송의 현실적인 목표: 암호화 시간 1~3초(하드웨어 가속), 300Mbps 연결에서 업로드 시간 30초, 전체 약 35초(대부분 네트워크 바운드), 적절한 스트리밍으로 피크 메모리 50MB 미만, 메인 스레드가 50ms 이상 차단되지 않아 UI 응답성 유지. HexaTransfer의 10GB 상한은 Web Crypto와 청크 스트리밍이 전체 경로를 효율적으로 유지하기 때문에 브라우저 내에서 충분히 달성 가능합니다.
hexatransfer.com에서 사용해 보세요 — 무료, 계정 불필요, 최대 10GB.
엔드투엔드 암호화로 대용량 파일을 안전하게 전송
엔드투엔드 암호화로 최대 10GB의 파일을 무료로 전송하세요. 계정이 필요하지 않습니다. 업로드 전에 브라우저에서 파일이 암호화되어 다른 사람은 읽을 수 없습니다.
파일 보내기