(번역) Blob API에 대한 소개와 활용 사례

sehyunny·2022년 9월 15일
14

FE 번역글

목록 보기
12/12
post-thumbnail

원문 : https://javascript.plainenglish.io/you-dont-know-blob-api-f2c5e9754f29

활용 사례 : 1. 청크 업로드, 2. 인터넷에서 데이터 다운로드, 3. 이미지 압축, 4. Blob URL 생성, 5. Blob에서 Data URL로의 전환

당신은 아마 캔버스 기반의 이미지 편집기를 사용해본 적이 있을 것입니다. 그리고 이미지 편집을 마친 뒤 다운로드를 선택하면 속도가 매우 빠르다는 것을 느꼈을 것입니다. 사실, 이 다운로드 기능은 Blob API가 사용되었을 가능성이 큽니다.

위의 상황을 제외하고도 Blob API는 큰 파일 업로드와 다운로드, 로컬 이미지 미리보기, 그리고 이미지 압축 및 업로드 과정에 사용될 수 있습니다. 지금부터 Blob API와 일반적인 사용 시나리오를 하나씩 소개해 드리겠습니다.

Blob이 무엇인가요?

Blob (Binary Large Object) 은 바이너리 타입의 큰 객체를 나타냅니다. 데이터베이스 관리 시스템에서 바이너리 데이터는 단일 엔티티의 컬렉션으로 저장됩니다. Blob은 일반적으로 이미지, 소리, 그리고 멀티미디어 파일입니다. 자바스크립트에서 Blob 타입의 객체는 불변성을 지니는 파일과 같은 원시 데이터를 나타냅니다.

Blob을 더 직관적으로 이해하기 위해 우선 아래 그림에 나와 있는 것처럼 Blob 생성자를 이용해서 myBlob 객체를 생성해보겠습니다.

위에 보이는 것처럼 myBlob 객체는 size와 type의 2가지 속성을 갖습니다. size 속성은 데이터의 크기(바이트)를 나타내는 데 사용되고, type 속성을 MIME 타입을 나타냅니다. Blob 객체는 optional typeblobParts로 이뤄져 있습니다.

Blob이 반드시 자바스크립트 네이티브 형식으로 데이터를 나타내는 것은 아닙니다. 예를 들어, File 인터페이스는 Blob에 기반하며, Blob의 기능을 상속받고, 사용자 시스템의 파일을 지원하도록 확장합니다.

Blob API에 대한 소개를 마쳤으니, 이제 실무에서 Blob API의 5가지 주요 사용 시나리오에 대해 소개하겠습니다.

Blob API 사용 시나리오

1. 청크 업로드

File 객체는 Blob의 특수한 타입이며 모든 Blob 타입의 맥락에서 사용할 수 있습니다. 따라서 큰 파일을 전송하는 상황에서 slice 메서드를 사용해 파일을 나눈 후 청크로 업로드를 할 수 있습니다. 구체적인 예시는 아래와 같습니다.

const file = new File(["a".repeat(1000000)], "bytefer.txt");
const chunkSize = 40000;
const url = "https://httpbin.org/post";
async function chunkedUpload() {
  for (let start = 0; start < file.size; start += chunkSize) {
    const chunk = file.slice(start, start + chunkSize + 1);
    const fd = new FormData();
    fd.append("data", chunk);
    await fetch(url, { method: "post", body: fd })
      .then((res) => res.text());
  }
}

만약 큰 파일을 청크로 동시에 업로드하는 방법에 대해 자세히 알아보려면 다음의 글을 참조 바랍니다.

자바스크립트로 큰 파일 동시에 업로드하기

2. 인터넷에서 데이터 다운로드

다음 예제의 downloadBlob 함수를 이용하여 인터넷에서 데이터를 다운로드하고, 이를 Blob 객체에 저장할 수 있습니다.

const downloadBlob = (url, callback) => {
  const xhr = new XMLHttpRequest()
  xhr.open('GET', url)
  xhr.responseType = 'blob'
  xhr.onload = () => {
    callback(xhr.response)
  }
  xhr.send(null)
}

물론 스트림의 바이너리 데이터를 얻기 위해 XMLHttpRequest API 대신에 fetch API를 사용할 수도 있습니다. 여기서는 온라인에서 이미지를 가져와서 로컬에 나타내기 위해 어떻게 fetch API를 사용해야 하는지를 알아보겠습니다. 구체적인 구현 방식은 다음과 같습니다.

const myImage = document.querySelector('img');
const myRequest = new Request('flowers.jpg');
fetch(myRequest)
  .then(function(response) {
    return response.blob();
  })
 .then(function(myBlob) {
   let objectURL = URL.createObjectURL(myBlob);
   myImage.src = objectURL;
});

fetch 요청이 성공적으로 이뤄지면 response 객체의 blob() 메서드를 호출하여 response 객체로부터 Blob 객체를 읽어옵니다. 그런 다음 objectURL 생성을 위해 createObjectURL() 메서드를 호출하고, 이를 img 요소의 src 속성에 할당하여 이미지를 표시하도록 합니다.

파일을 업로드할 때 동시 업로드를 진행할 수 있습니다. 이와 유사하게 파일을 다운로드할 때도 동시에 다운로드를 진행할 수 있습니다. 구체적인 구현 방식은 아래의 글을 참조하세요.

자바스크립트로 큰 파일 동시에 다운로드하기

3. 이미지 압축

가끔 로컬 이미지를 업로드하는 상황에서 우선 이미지를 어느 정도 압축한 뒤, 서버에 전송하여 전송되는 데이터의 크기를 줄이고 싶은 경우가 있습니다. 프런트엔드에서 이미지 압축을 하기 위해 Canvas 객체에서 제공하는 toDataURL() 메서드를 사용할 수 있습니다. 이 메서드는 typeencoderOptions 라는 2가지 선택적 매개변수를 받습니다.

타입이 이미지 형식을 나타낼 때, 기본값은 "image/png" 입니다. encoderOptions는 이미지의 품질을 나타내는 데 사용됩니다. 지정된 이미지 형식이 "image/jpeg" 또는 "image/webp" 일 때 이미지의 품질을 0부터 1까지 선택할 수 있습니다. 만약 값이 범위를 벗어난 경우, 기본값인 0.92가 사용되며 다른 매개변수는 무시됩니다.

const MAX_WIDTH = 800;

function compress(base64, quality, mimeType) {
  let canvas = document.createElement("canvas");
  let img = document.createElement("img");
  img.crossOrigin = "anonymous";
  return new Promise((resolve, reject) => {
    img.src = base64;
    img.onload = () => {
      let targetWidth, targetHeight;
      if (img.width > MAX_WIDTH) {
        targetWidth = MAX_WIDTH;
        targetHeight = (img.height * MAX_WIDTH) / img.width;
      } else {
        targetWidth = img.width;
        targetHeight = img.height;
      }
      canvas.width = targetWidth;
      canvas.height = targetHeight;
      let ctx = canvas.getContext("2d");
      ctx.clearRect(0, 0, targetWidth, targetHeight);
      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
      let imageData = canvas.toDataURL(mimeType, quality / 100);
      resolve(imageData);
    };
  });
}

Data URL 형식으로 반환된 이미지 데이터의 경우 전송되는 데이터의 양을 더 줄이기 위해 다음과 같이 Blob 객체로 변환할 수 있습니다.

function dataUrlToBlob(base64, mimeType) {
  let bytes = window.atob(base64.split(",")[1]);
  let ab = new ArrayBuffer(bytes.length);
  let ia = new Uint8Array(ab);
  for (let i = 0; i < bytes.length; i++) {
    ia[i] = bytes.charCodeAt(i);
  }
  return new Blob([ab], { type: mimeType });
}

사실, Canvas 객체에서는 toDataURL() 메서드를 제외하고도 아래와 같은 시그니처로 사용할 수 있는 toBlob() 메서드도 제공합니다.

canvas.toBlob(callback, mimeType, qualityArgument)

toDataURL() 메서드와 비교했을 때 toBlob() 메서드는 비동기적이기 때문에 추가적인 callback 매개변수가 존재합니다. 기본적으로 이 callback 함수의 첫 번째 매개변수는 변환된 blob 파일 정보입니다.

4. Blob URL 생성

Blob URL/Object URL은 Blob 및 File 객체를 이미지의 URL 소스나 바이너리 데이터를 다운로드하기 위한 링크 등으로 사용할 수 있도록 하는 의사 프로토콜(pseudo-protocol)입니다. 브라우저에서 Blob URL을 생성하기 위해 URL.createObjectURL 을 사용합니다. 이 메서드는 Blob 객체를 가져와서 blob:<origin>/<uuid> 형태의 고유한 URL을 생성합니다. URL의 예시는 아래와 같습니다.

blob:https://example.org/40a5fb5a-d56d-4a33-b4e2-0acf6a8e5f641

브라우저는 내부적으로 URL.createObjectURL을 통해 생성한 URL에 대해 URL → Blob 맵을 저장합니다. 하지만 blob은 그 자체로 메모리 내에 남아있으며 브라우저는 이를 해제할 수 없습니다. 도큐먼트가 언로드되면 맵핑이 자동으로 정리되며, 따라서 Blob 객체도 해제됩니다. 그러나, 만약 애플리케이션이 오래 지속되는 경우 메모리 해제는 거의 발생하지 않게 됩니다. 따라서 이 상황에서 blob URL을 생성한다면, blob이 더 이상 필요하지 않은 상황에서도 계속 메모리 내에 남아 있게 됩니다.

이 문제의 경우 URL.revokeObjectURL 메서드를 호출하여 내부 맵의 참조를 제거하여 blob을 삭제하고, 메모리를 해제할 수 있습니다. 다음으로 Blob URL을 사용하여 클라이언트 측에서 파일 다운로드를 진행하는 예제를 살펴보겠습니다.

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Blob URL Demo</title>
  </head>
<body>
    <button id="downloadBtn">Download</button>
    <script src="index.js"></script>
  </body>
</html>

index.js

const download = (fileName, blob) => {
  const link = document.createElement("a");
  link.href = URL.createObjectURL(blob);
  link.download = fileName;
  link.click();
  link.remove();
  URL.revokeObjectURL(link.href);
};
const downloadBtn = document.querySelector("#downloadBtn");
downloadBtn.addEventListener("click", (event) => {
  const fileName = "blob.txt";
  const myBlob = new Blob(["You Don’t Know Blob API"], 
   { type: "text/plain" });
  download(fileName, myBlob);
});

위의 예제에서 Blob 생성자를 호출하여 "text/plain" 타입의 Blob 객체를 생성한 뒤, 동적으로 생성한 요소를 통해 파일을 다운로드합니다.

5. Blob에서 Data URL로의 변환

HTML 웹 페이지를 작성할 때 작은 이미지의 경우 주로 이미지 컨텐츠를 웹 페이지에 직접 포함해서 불필요한 네트워크 요청을 줄이려고 합니다. 그런데 이미지 데이터는 바이너리 데이터인데 이를 어떻게 포함시킬 수 있을까요? 오늘날 대부분 브라우저는 Data URL로 불리는 기능을 지원합니다. 이 기능은 이미지와 다른 파일의 바이너리 데이터를 base64로 인코딩하여 웹 페이지에 텍스트 문자열 형태로 포함될 수 있도록 해줍니다.

Data URL은 접두사(data:)와 데이터 타입을 나타내는 MIME 타입, 텍스트가 아닐 경우 선택적인 base64 태그, 그리고 데이터 그 자체, 총 네 가지 부분으로 이뤄져 있습니다.

data:[<mediatype>][;base64],<data>

mediatype은 MIME 타입 문자열로, JPEG 이미지 파일의 경우 "image/jpeg"가 됩니다. 만약 생략할 경우 기본값은 "text/plain;charset=US-ASCII"가 됩니다. 만약 데이터가 텍스트 타입이면 텍스트를 바로 포함할 수 있습니다. 만약 바이너리 타입일 경우 포함하기 전에 base64 인코딩을 할 수 있습니다. 예를 들어, 이미지를 포함할 경우 다음과 같습니다.

<img alt="logo" 
  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...">

하지만 이미지가 크고 색상 범위가 다양하다면 이 방법을 사용하기엔 적합하지 않습니다. 왜냐하면 base64 인코딩한 문자열 값이 매우 크기에 HTML 페이지의 크기를 증가시키고, 이는 로딩 속도에 영향을 주기 때문입니다. 게다가 FileReader API를 사용하면 간편하게 이미지의 로컬 미리보기 기능을 구현할 수 있습니다. 상세한 코드는 다음과 같습니다.

<input type="file" accept="image/*" onchange="loadFile(event)">
<img id="output"/>
<script>
  const loadFile = function(event) {
    const reader = new FileReader();
    reader.onload = function(){
      const output = document.querySelector('output');
      output.src = reader.result;
    };
    reader.readAsDataURL(event.target.files[0]);
  };
</script>

위의 예시에서는 파일 타입 입력 박스의 onchange 이벤트 핸들러에 loadFile을 바인딩했습니다. 이 함수에서는 FileReader 객체를 생성하고 해당 객체에 onload 이벤트 핸들러를 바인딩한 다음, FileReader 객체의 readAsDataURL() 메서드를 호출하여 로컬 이미지에 해당하는 File 객체를 Data URL로 변환합니다.

로컬 이미지 미리보기를 완료한 이후, 우리는 서버에 관련 이미지의 Data URL을 직접적으로 전송할 수 있습니다. 이러한 상황에 대응하여 서버는 업로드된 이미지를 정상적으로 저장하기 위해 몇 가지 관련 처리를 수행해야 합니다. Express를 사용한 예시 코드는 다음과 같습니다.

const app = require('express')();
app.post('/upload', function(req, res){
    let imgData = req.body.imgData;
    let base64Data = imgData.replace(/^data:image\/\w+;base64,/, "");
    let dataBuffer = Buffer.from(base64Data, 'base64');
    fs.writeFile("image.png", dataBuffer, function(err) {
        if(err){
          res.send(err);
        }else{
          res.send("Image upload successfully!");
        }
    });
});

FileReader 객체는 Blob/File 객체를 Data URL로 변환되도록 지원하는 것 외에도, Blob/File 객체가 다른 데이터 형식으로 변환될 수 있도록 readAsArrayBuffer()메서드와 readAsText()메서드를 제공합니다. 다음은 readAsArrayBuffer() 의 사용 예시입니다.

let fileReader = new FileReader();
fileReader.onload = function(event) {
  let arrayBuffer = fileReader.result;
};
fileReader.readAsArrayBuffer(blob);

Blob과 ArrayBuffer 객체는 서로 변환될 수 있습니다.

  • FileReader의 readAsArrayBuffer() 메서드를 사용하여 Blob 객체를 ArrayBuffer 객체로 변환할 수 있습니다.
  • new Blob([new Uint8Array(data)])와 같이 Blob 생성자를 이용하여 ArrayBuffer 객체를 Blob 객체로 변환할 수 있습니다.

이 글을 읽은 뒤, Blob API와 애플리케이션 시나리오에 대해 어느 정도 이해하셨을 거라 생각합니다. 실무에서 Blob API를 사용하여 다른 기능을 수행하고 있다면 제게 공유 부탁드립니다.

profile
먹고, 보고, 만드는 걸 좋아하는 FE 개발자입니다.

1개의 댓글

comment-user-thumbnail
2022년 9월 16일

좋은 글 고맙습니다^^

답글 달기