S3 대용량 파일 다운로드 받기 (multipart download)
S3 대용량 파일을 Chunk로 쪼개서 다운로드하는 방법을 알아봅시다

S3를 이용해서 파일을 업로드 / 다운로드 하다보면 대용량 파일(수백 MB~수 GB)을 다룰 때가 있습니다. 이 때 대용량 파일을 한 번에 다루게 된다면 메모리 문제나 타임아웃이 발생할 수 있습니다.
이를 해결하기 위해서 AWS에서 업로드는 멀티파트 업로드 API를 제공하지만 다운로드는 별도의 API가 없습니다.
하지만 GetObjectCommand에서 Range 옵션을 이용하여 잘게 chunk로 나누어 받은 다음 다시 합치는 방법으로 이를 해결 할 수 있습니다.
사전 조건
S3의 CORS를 허용해야합니다.
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"PUT",
"POST",
"DELETE"
],
"AllowedOrigins": [
"your-domain"
],
"ExposeHeaders": [
"Access-Control-Allow-Origin",
"Content-Range",
]
}
]ExposeHeaders에 Content-Range를 추가하여 해당 정보를 받을 수 있게 설정해야합니다.
구현
// Range Option을 이용하여 byte를 나누어 S3 객체를 가져온다
export const getObjectRange = async ({
start,
end,
}: {
start: number;
end: number;
}) => {
const client = new S3Client({
region: "your region",
});
const command = new GetObjectCommand({
Bucket: `your bucket`,
Key: `your S3 Key`,
Range: `bytes=${start}-${end}`,
});
return await client.send(command);
};
/**
* ex) Content-Range: bytes 0-1048575/127279841
* Content-Range를 start, end, contentLength를 구함
*/
export const getRangeAndLength = (contentRange: string) => {
const [range, length] = contentRange.split("/");
const [start, end] = range.split("-");
return {
start: Number.parseInt(start),
end: Number.parseInt(end),
length: Number.parseInt(length),
};
};
// 완료 판단 함수
export const isComplete = ({ end, length }: { end: number; length: number }) =>
end === length - 1;
// chunk로 나누어 다운로드
export const downloadInChunks = async ({ filename }: { filename: string }) => {
const oneMB = 1024 * 1024; // 1MB
// 첫 번째 청크를 가져와서 총 크기를 확인
const firstChunk = await getObjectRange({
filename,
start: 0,
end: oneMB - 1,
});
const rangeInfo = getRangeAndLength(firstChunk.ContentRange ?? "");
const totalSize = rangeInfo?.length;
// 모든 청크를 저장할 배열
const chunks = [];
chunks.push(await firstChunk.Body?.transformToByteArray());
// 나머지 청크 다운로드
while (!isComplete(rangeInfo)) {
const nextRange = {
start: rangeInfo.end + 1,
end: Math.min(rangeInfo.end + oneMB, totalSize - 1),
};
const { ContentRange, Body } = await getObjectRange({
filename,
...nextRange,
});
const chunk = await Body?.transformToByteArray();
chunks.push(chunk);
rangeInfo.end = getRangeAndLength(ContentRange ?? "").end;
}
return chunks as Array<Uint8Array<ArrayBufferLike>>;
};
// 모든 청크를 하나의 Blob으로 결합
const blob = new Blob(chunks);
// 다운로드 링크 생성 및 클릭
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "FILENAME";
document.body.appendChild(a);
a.click();이와 같이 각 chunk로 나누어 대용량 파일을 다운로드 받을 수 있습니다.
이러한 방법으로 구현 시 다음과 같은 장점이 있습니다.
- 네트워크 오류로 중단 시 각 범위 별로 재시작 가능 (실패한 범위만 재시작)
- 각 범위를 이용하여 진행 상황을 추적 가능 (Progress로 진행율 표현 가능)
