티스토리 뷰

사용자가 이미지 주소나 이미지 파일을 업로드했을 때, 이미지 크기를 조정하고 Base64로 인코딩하는 과정을 알아보겠습니다.

개발 환경

  • 클라이언트: Typescript
  • 서버: Node.js + Express +  Typescript

상황

사용자가 업로드 한 이미지를 서버에서 Base64로 인코딩해야 한다.

  1. 이미지 가로x세로 크기 조정을 서버와 클라이언트 중 어디에서 할까? 
    • 사용자가 용량이 큰 이미지를 업로드했을 때를 고려하여, 클라이언트에서 크기를 조정하고 용량을 줄인 후 서버로 전송하겠습니다.
  2. 클라이언트에서 서버로 이미지를 전송할 때 어떤 형태로 전송할까? (이미지 주소, 파일, Blob, Base64 등)
    • 클라이언트에서 Canvas를 사용하여 이미지 크기를 줄일 수 있습니다.
    • Canvas의 toBlob 함수를 사용하여 크기를 줄인 이미지를 이진 데이터 형태로 받을 수 있기 때문에 Blob을 서버에 전송하겠습니다.

+) 클라이언트에서 Base64로 인코딩해도 되지 않을까?

  • FileReader 객체로 이미지를 읽은 후 Base64로 인코딩할 수 있습니다.
  • 하지만 어차피 서버에 이미지를 전달해야 하는 상황이고, 미미한 차이지만 Base64로 인코딩하면 사이즈가 기존보다 4/3 (약 33%) 증가하기 때문에 서버에서 인코딩하도록 하겠습니다.
  • 참고 자료: Why is a base 64 encoded file 33% larger than the original?
 

Why is a base 64 encoded file 33% larger than the original?

Understanding what is base64 encoding & how it works

bharatkalluri.com

 

목표

  1. 클라이언트에서 이미지 크기를 조정한다 (with Canvas)
  2. 이미지 주소와 파일을 Blob 데이터로 변환하여 서버에 전송한다.
  3. 서버에서 Blob 데이터를 Base64로 인코딩한다. (with Buffer)

🎨 캔버스로 이미지 크기 조정하고 Blob으로 반환

사용자는 이미지 주소를 제출하거나 드롭다운으로 이미지 파일을 업로드합니다. 이미지 주소일 경우 img src 속성에 바로 넘기면 되고, 이미지 파일일 경우 URL.createObjectURL() 함수를 사용하여 url로 변환 후 img src 속성에 넘겨줍니다. 캔버스를 활용한 이미지 크기 조정 코드는 다음과 같습니다.

function resizeImage(
  fileOrUrl: File | string,
  width: number,
  height: number
): Promise<Blob | null> {
  return new Promise((resolve, reject) => {
    const img = new Image();
    // 외부 데이터를 캔버스에 그리고 Blob으로 변환할 경우 CORS 에러가 발생한다.
    // 이를 허용하기 위해 crossOrigin 속성을 사용한다.
    // 출처: https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image
    img.crossOrigin = "anonymous"; 

    // 이미지 주소인 경우
    if (typeof fileOrUrl === "string") img.src = fileOrUrl;
    // 파일인 경우
    else img.src = URL.createObjectURL(fileOrUrl as Blob);

    img.onload = () => {
      const canvas = document.createElement("canvas");
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext("2d");
      ctx?.drawImage(img, 0, 0, width, height);
      
      // 크기를 조정한 이미지를 blob으로 반환
      canvas.toBlob((blob) => {
        resolve(blob);
      }, "image/png");
    };
    
    // [에러 처리] 이미지 주소가 아닌 주소를 넘기면 에러가 발생
    img.onerror = (error) => reject(error); 
  });
}

🚀 Blob 데이터 서버로 전송하기

Blob은 이진데이터를 다루는 객체입니다. 이진 데이터를 서버로 전송하려면 Content-Type을 application/octet-stream 으로 설정합니다.

출처: https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types#application

본인이 구현한 api 주소로 POST 요청을 날려줍니다. 코드는 아래와 같이 작성했습니다.

async function postImage(blob: Blob) {
  return await fetch("your api url", {
    method: "POST",
    headers: { "Content-Type": "application/octet-stream" },
    body: blob,
  });
}

🔒 서버에서 이진 데이터를 Base64로 인코딩하기

request body에 담긴 blob을 Buffer로 응답받습니다. 클라이언트에서 이진 데이터를 다루는 객체가 Blob이라면, 서버에서 이진 데이터를 다루는 객체는 Buffer라고 합니다. Buffer 객체에 toString 메소드를 사용하여 base64로 쉽게 인코딩할 수 있습니다.

apiRouter.post("your api url", encodeBase64);

async function encodeBase64(req: Request, res: Response) {
  const buffer = req.body as Buffer; // Buffer 타입 지정
  const base64 = buffer.toString("base64"); // 인코딩 완료
  //...
  res.send("ok");
}

원래 이미지 크기 조정, 인코딩 모두 서버에서 하는 방식으로 구현했었습니다. 이미지 크기가 작으면 문제가 없었지만, 이미지 크기가 클 경우 인코딩할 때까지 7~8초의 시간이 걸리기도 했습니다 😱 그러다 생각한 방법이 Canvas로 클라이언트에서 이미지 크기를 조정하고 서버로 전송하는 것이었습니다. 그 결과 이미지 크기가 커도 (너비가 약 1300~2000px 정도 되는 이미지여도) 2초대에서 이미지 전송 및 인코딩을 마칠 수 있었습니다. (+ 이렇게 하면 디스크에 이미지를 저장하는 Multer와 이미지 크기 조정에 필요한 Sharp 라이브러리도 필요하지 않더라고요!) 

 

이미지 크기 조정, 이미지 전송할 일이 있으면 이 방법이 도움이 되었으면 합니다. 읽어주셔서 감사합니다 :)

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
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 29 30
글 보관함