목록으로
DEV

S3 대용량 파일 다운로드 받기 (multipart download)

S3 대용량 파일을 Chunk로 쪼개서 다운로드하는 방법을 알아봅시다

FE19분 읽기
S3 대용량 파일 다운로드 받기 (multipart download)

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로 나누어 대용량 파일을 다운로드 받을 수 있습니다.

이러한 방법으로 구현 시 다음과 같은 장점이 있습니다.

  1. 네트워크 오류로 중단 시 각 범위 별로 재시작 가능 (실패한 범위만 재시작)
  2. 각 범위를 이용하여 진행 상황을 추적 가능 (Progress로 진행율 표현 가능)

참조

https://docs.aws.amazon.com/ko_kr/sdk-for-javascript/v3/developer-guide/javascript_s3_code_examples.html

이런 글은 어때요?