Javascript(React)로 이미지 파일 용량 줄이기

25831
2020-02-25

React로 만든 화면에서 파일을 업로드하는 기능을 만들고 QA로부터 업로드에 걸리는 시간을 줄일 수 없냐는 피드백을 받았습니다. 업로드 해야하는 파일은 여러 이미지 파일들을 압축한 파일 하나인데 이미 압축된 상태이므로 더이상 효과적으로 파일 용량을 줄일 수는 없다고 생각했습니다. 하지만 업로드된 이미지가 어떻게 쓰이는지 살펴보니 파일 용량를 줄일 수 있는 실마리를 발견할 수 있었습니다. 바로 실제로 보여지는 이미지 크기였습니다. 썸네일 수준의 작고 동일한 크기로 이미지를 보여주기 때문에 업로드 전 이미지 크기를 그에 맞게 줄인다면 용량과 함께 업로드에 걸리는 시간이 줄어들 것이라 판단했습니다. 그래서 이미지 크기를 줄이는 작업을 시도해 보았고 결과적으로 업로드에 걸리는 시간을 눈에 띄게 줄일 수 있었습니다.[1][2]

example

실제로 이미지가 쓰이는 모습

이번 글에서는 React로 만든 화면에서 이미지 파일 용량을 줄이기 위해 수행한 작업들을 소개하겠습니다.

시작하기 전에

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from "react";
import "./styles.css";
import { Upload, Button } from "antd";
import "antd/dist/antd.css";
import jszip from "jszip";
export default function App() {
    return (
        <Upload
            accept=".zip"
            showUploadList={false}
            onChange={async info => {
                ...
            }}
            beforeUpload={() => false}
        >
            <Button>upload</Button>
        </Upload>
    );
}

필자가 진행하는 프로젝트의 UI 컴포넌트는 antd를 사용하고 있습니다. 파일 업로드의 기본 기능은 antd의 Upload 컴포넌트를 사용했습니다. Upload 컴포넌트는 파일을 선택할 경우 "onChange" 함수를 호출하는데 이 함수에 업로드까지의 전반의 작업들을 정의하였습니다. 앞으로 나올 예제들은 "onChange" 함수의 내용으로 요약하면 다음과 같습니다.

1
2
3
4
5
6
onChange={async info => {
  // 1. zip 파일 읽기
  // 2. 이미지 파일 크기 조정
  // 3. 새로 만들어진 이미지로 zip 파일 만들기
  // 4. 업로드
}}

전체 소스는 여기서 확인하실 수 있습니다.

ZIP 파일 읽기

jszip을 사용해 ZIP 파일을 다룰 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const unzip = new jszip();
const oldZipFile = info.file;
// 압축 파일을 읽어 안의 (아직 압축된 상태의) File들을 배열에 저장합니다.
const loadedZipFile = await unzip.loadAsync(oldZipFile);
const oldImageFiles = await new Promise(ok => {
    const filesInZipFile = [];
    loadedZipFile.forEach((_, file) => {
        filesInZipFile.push(file);
    });
    const oldImageFiles = [];
    // 배열에 저장된 File들을 하나씩 순회하면서 압축을 풀어 Blob(바이너리 데이터)으로 만들고 다시 File로 만들어 배열에 저장합니다.
    filesInZipFile.forEach(async file => {
        const oldImageblob = await file.async("blob");
        oldImageFiles.push(new File([oldImageblob], file.name));
        if (oldImageFiles.length === filesInZipFile.length) {
            ok(oldImageFiles);
        }
    });
});

압축 파일을 읽고 푸는 함수는 이름에서 알 수 있듯이 비동기로 동작합니다.

이미지 크기 조정하기

별도의 라이브러리 없이 canvasimg를 통해 이미지 크기를 조정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const newImageFiles = await new Promise(async ok => {
    const newImageFiles = [];
    // 이미지 File들을 순회하면서 img를 만들고 File을 참조하는 URL을 만들어[3] img 경로로 사용합니다.
    for (const oldImageFile of oldImageFiles) {
        const image = document.createElement("img");
        image.src = URL.createObjectURL(oldImageFile);
        // 이미지가 로드되면 canvas를 원하는 크기로 만들고 이미지를 그에 맞춰 그립니다.
        image.onload = () => {
            URL.revokeObjectURL(image.src);
            const canvas = document.createElement("canvas");
            canvas.width = 125;
            canvas.height = 125;
            const context = canvas.getContext("2d");
            context.drawImage(image, 0, 0, 125, 125);
            // canvas에 그려진 이미지를 Blob으로 만들고 다시 File로 만들어 배열에 저장합니다.
            context.canvas.toBlob(
                newImageBlob => {
                    newImageFiles.push(new File([newImageBlob], oldImageFile.name));
                    if (oldImageFiles.length === newImageFiles.length) {
                        ok(newImageFiles);
                    }
                },
                "image/png",
                0.5
            );
        };
    }
});

새로 만들어진 이미지로 ZIP 파일 만들기 & 업로드

1
2
3
4
5
6
7
8
9
10
11
12
const zip = new jszip();
// 크기를 조정한 이미지 File들을 순회하면서 압축 목록에 추가합니다.
for (const newImageFile of newImageFiles) {
    zip.file(newImageFile.name, newImageFile);
}
// 압축을 한 뒤 Blob으로 만들고 다시 File로 저장합니다.
const newZipBlob = await zip.generateAsync({ type: "blob" });
const newZipFile = new File([newZipBlob], oldZipFile.name);
// 압축된 File을 FormData에 담아 fetch로 업로드를 수행합니다.
const formData = new FormData();
formData.append("file", newZipFile);
fetch("url", { method: "post", body: formData }).then(res => { });

참조 문서

각주

[1]: 테스트에 사용한 압축 파일의 경우 25MB에서 300KB까지 용량을 줄일 수 있었습니다.

[2]: 압축을 하거나 푸는데 걸리는 시간도 포함합니다.

[3]: File을 참조하는 URL이 더 이상 필요없을 경우 해제를 통해 메모리를 절약할 수 있습니다.


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.