JavaScript로 청크 파일 업로드 구현하기
JavaScript로 재개 가능한 청크 파일 업로드를 구현하세요. 대용량 파일을 처리하고 진행률을 추적하고 네트워크 중단에서 복구합니다.
개인정보 보호법(PIPA) 제29조는 개인정보의 전송 시 안전한 방법을 적용하도록 요구합니다. 대용량 파일의 청크 업로드는 단순한 성능 최적화가 아니라 신뢰할 수 있는 전송을 위한 기술적 기반입니다. JavaScript 청크 파일 업로드는 대용량 파일을 고정 크기 조각(보통 5~10MB)으로 분할하고, 각각을 별도의 HTTP 요청으로 업로드하며, 서버에서 재조립합니다. File.slice()로 청크를 추출하고, 청크당 AbortSignal과 함께 fetch를 사용하며, S3 멀티파트 또는 커스텀 병합기로 서버 측 조립하고, IndexedDB의 로컬 인덱스로 탭 재로드 후에도 재개할 수 있습니다.
청크가 단발성 업로드보다 나은 이유
4GB 파일을 하나의 요청으로 업로드하면 예측 가능한 이유로 실패합니다: Nginx의 기본 client_max_body_size는 1MB, Cloudflare 무료 티어는 요청당 100MB, AWS API Gateway는 10MB에서 하드 스톱, 모바일 Safari는 4GB ArrayBuffer를 메모리에 보유하면 탭을 종료합니다. 청크 업로드는 이 모든 한계를 회피합니다. 실제로 움직이는 진행 표시줄, 처음부터 다시 시작하지 않는 재시도, 일시 중지·재개 기능을 얻습니다.
청크 크기 선택
청크 크기는 처리량 대 복원력의 트레이드오프입니다. 너무 작으면(1MB 미만) TLS 핸드셰이크에 데이터보다 더 많은 시간을 씁니다. 너무 크면(100MB 이상) 연결 끊김으로 분 단위 업로드가 낭비됩니다. 대부분의 네트워크에서 최적점은 5~10MB로, S3의 5MB 멀티파트 최소값과 일치하고 일반적인 TCP 윈도우 크기와도 잘 맞습니다.
먼저 사용자 네트워크를 측정하세요:
const downlink = navigator.connection?.downlink ?? 10;
const chunkSize = downlink > 20 ? 10 * 1024 * 1024 : 5 * 1024 * 1024;
파일 슬라이싱과 해싱
File.slice()는 복사 없이 기반 디스크 바이트를 참조하는 Blob을 반환하므로, 20GB 파일 슬라이싱이 비용 없습니다:
function* sliceFile(file, chunkSize) {
for (let offset = 0; offset < file.size; offset += chunkSize) {
yield {
index: Math.floor(offset / chunkSize),
blob: file.slice(offset, offset + chunkSize),
start: offset,
end: Math.min(offset + chunkSize, file.size)
};
}
}
업로드 전에 각 청크의 SHA-256 해시를 계산하여 서버가 무결성을 검증할 수 있게 하세요. 10GB 데이터 해싱은 현대 노트북에서 약 20초를 추가합니다—불안정한 셀룰러 업링크의 무음 손상을 잡기 위해 가치 있습니다.
제어된 동시성으로 업로드
순차 업로드는 대역폭을 낭비하고, 무제한 병렬은 브라우저를 충돌시킵니다. 3~4개의 진행 중인 청크 동시성 제한이 균형을 맞춥니다:
async function uploadAll(file, sessionId) {
const queue = [...sliceFile(file, 5 * 1024 * 1024)];
const workers = Array.from({ length: 4 }, async () => {
while (queue.length) {
const chunk = queue.shift();
await uploadChunk(chunk, sessionId);
emitProgress(chunk.index);
}
});
await Promise.all(workers);
}
각 uploadChunk 호출은 블롭을 본문으로, 해시를 헤더로 하는 PUT /upload/:sessionId/:index입니다. 청크당 AbortController를 사용해 전체 배치를 중단하지 않고 개별 요청을 취소할 수 있게 하세요.
서버를 폭격하지 않는 재시도
네트워크 오류는 지수 백오프가 필요합니다. 합리적인 정책: 3회 시도, 기본 지연 500ms, 최대 50% 지터:
async function uploadChunk(chunk, sessionId, attempt = 0) {
try {
const res = await fetch(`/upload/${sessionId}/${chunk.index}`, {
method: 'PUT', body: chunk.blob, headers: { 'X-Hash': chunk.hash }
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
} catch (e) {
if (attempt >= 3) throw e;
const delay = 500 * 2 ** attempt + Math.random() * 250;
await new Promise(r => setTimeout(r, delay));
return uploadChunk(chunk, sessionId, attempt + 1);
}
}
5xx 응답은 재시도 가능, 4xx는 치명적(408과 429 제외)으로 처리하세요. 429에서는 로컬 백오프 대신 Retry-After 헤더를 준수하세요.
탭 재로드 후 재개
성공적인 청크마다 IndexedDB에 업로드 상태를 유지하세요. 사용자가 같은 파일 피커로 페이지를 다시 열면 파일의 size, lastModified, 이름을 저장된 세션과 비교하고, 일치하면 서버에 이미 받은 청크를 쿼리한 후 누락된 것만 업로드하세요. tus.io 프로토콜은 Upload-Offset 헤더로 이 패턴을 정확히 공식화하고 있으며, tus-js-client 라이브러리가 직접 구현하지 않으려는 경우 견고한 구현을 제공합니다.
서버에서 청크 조립
두 가지 진지한 옵션: S3 멀티파트 업로드(각 청크가 PartNumber가 되고 최종 CompleteMultipartUpload로 결합)와 커스텀 조립기(각 청크를 임시 파일에 쓰고 최후에 연결). S3 방식은 대규모에서 더 저렴하며 R2는 이그레스 무료 읽기를 제공합니다. 10,000 파트 한도에 주의하세요—50GB 이상의 파일에는 5MB+ 청크가 필요합니다.
사용자가 신뢰하는 진행 표시줄
진행 표시줄이 들쭉날쭉하면 고장난 것처럼 느껴집니다. 완료된 청크가 아닌 업로드된 바이트 대 전체 바이트로 진행률을 계산하고, 2초 이동 평균으로 지터를 숨기세요. XMLHttpRequest.upload.onprogress는 HTTP/3에서 항상 발화하지 않으므로 fetch와 ReadableStream에 Transform을 사용해 바이트를 계산하세요.
HexaTransfer는 이와 같은 청크 + 재개 가능 파이프라인을 내부적으로 10GB 업로드에 사용하며, PUT 전에 각 청크에 클라이언트 측 AES-256-GCM을 추가합니다.
https://hexatransfer.com — 무료, 계정 불필요, 최대 10GB.
엔드투엔드 암호화로 대용량 파일을 안전하게 전송
엔드투엔드 암호화로 최대 10GB의 파일을 무료로 전송하세요. 계정이 필요하지 않습니다. 업로드 전에 브라우저에서 파일이 암호화되어 다른 사람은 읽을 수 없습니다.
파일 보내기