사용자가 업로드 한 이미지 압축하여 서버로 보내기 (feat.Browser Image Compression, Upload Images Converter, webp)

보투게더·2023년 10월 4일
2
post-custom-banner

이미지 압축 패키지 비교 (23.10.04 기준)

이미지 성능 최적화

사용자가 서버로 보내는 이미지의 용량을 줄여서 서버의 용량도 절약해주고, 사용자가 이미지를 불러올 때 가볍게 불러올 수 있도록 하려고 했습니다. 그래서 사용자가 인풋에서 이미지를 업로드 할 때 webp로 변환하는 기능을 구현하게 되었습니다

Browser Image Compression

https://www.npmjs.com/package/browser-image-compression

다운로드 수 : 137,399
번들 사이즈 : 55.51KB
마지막 업데이트 : 7개월 전


Upload Images Converter

https://www.npmjs.com/package/upload-images-converter

다운로드 수 : 31
번들 사이즈 : 5.05KB
마지막 업데이트 : 2개월 전

import { imageConverter } from 'upload-images-converter';

  const compressedBlob = await imageConverter({ files: [imageFile], width: 1280, height: 1280 });


결론

이미지를 webp로 변환하는 기능만 필요하고, 두 패키지의 번들 사이즈가 11배 차이가 나다보니 더 가벼운 패키지인 Upload Images Converter을 사용하기로 했습니다.

다만 Browser Image Compression에서 지원하는 기능 중 자동으로 사진 비율대로 사진 크기를 잘라서 반환하는 기능을 Upload Images Converter에서 지원하지 않기 때문에 추가로 코드를 작성하여서 구현했습니다


구현 과정

원하는 요구 사항

  1. webp로 변환되어야 한다
  2. 사진의 크기가 지정한 크키보다 클 시 사진의 가로, 세로 중 큰 값을 지정한 크기로 변환하고 사진의 비율대로 줄어들어야 한다.

문제 사항

Upload Images Converter는 가로, 세로를 직접 지정해서 사용하는 방법만 제공해주고 있었습니다. 그래서 이미지의 사이즈를 구하고, 직접 가로 세로 값을 계산하여서 가로, 세로 값을 입력해 webp 사진을 반환받으려고 했습니다.

1. 이미지 사이즈 구하기

  1. Promise를 사용해서 사용하는 측에서 await로 width, height를 접근할 수 있도록 했습니다.
  2. FileReader을 이용하여 이미지 파일을 읽고, new Image에 이미지 src를 넣어주었습니다
  3. img의 naturalWidth, naturalHeight을 이용하여 사진의 사이즈를 알아내고 반환했습니다.

getImageSize.ts

export const getImageSize = (imageFile: File): Promise<{ width: number; height: number }> => {
  return new Promise((resolve, reject) => {
    const img = new Image();
    const reader = new FileReader();

    img.onload = () => {
      const width = img.naturalWidth;
      const height = img.naturalHeight;

      resolve({ width, height });
    };

    reader.onload = () => {
      img.src = reader.result?.toString() ?? '';
    };

    reader.readAsDataURL(imageFile);

    img.onerror = error => {
      reject(error);
    };

    reader.onerror = error => {
      reject(error);
    };
  });
};

2. 최대 사이즈를 기준으로 가로, 세로 사이즈 구하기

  1. 가로, 세로 둘 중 어느 것도 최대 사이즈를 넘지 않는다면 그대로 반환한다.
  2. 가로, 세로 중 큰 값이 최대 사이즈를 넘는다면 가로, 세로 둘 중 큰 값은 최대 사이즈로 고정시키고, 작은 값은 비율대로 줄여서 반환한다.

비율의 사이즈를 구하는 공식은 작은값 * 최대값 / 큰값 으로 하였습니다.

공식의 원리는 이해하지 못했습니다만 공식을 활용하여 계산했습니다

calculateAspectRatioSize.ts

export const calculateAspectRatioSize = ({
  originWidth,
  originHeight,
  maxWidthOrHeight,
}: {
  originWidth: number;
  originHeight: number;
  maxWidthOrHeight: number;
}) => {
  const maxSize = Math.max(originWidth, originHeight);

  if (maxSize <= maxWidthOrHeight) {
    return { width: originWidth, height: originHeight };
  }

  if (originWidth === maxSize) {
    const width = maxWidthOrHeight;
    const height = Number(((originHeight * maxWidthOrHeight) / originWidth).toFixed(1));

    return { width, height };
  }

  const width = Number(((originWidth * maxWidthOrHeight) / originHeight).toFixed(1));
  const height = maxWidthOrHeight;

  return { width, height };
};

calculateAspectRatioSize.test.ts

import { calculateAspectRatioSize } from '@utils/image/calculateAspectRatioSize';

test.each([
  [
    { originWidth: 3000, originHeight: 820, maxWidthOrHeight: 1280 },
    { width: 1280, height: 349.9 },
  ],
  [
    { originWidth: 1280, originHeight: 1303, maxWidthOrHeight: 1280 },
    { width: 1257.4, height: 1280 },
  ],
  [
    { originWidth: 2403, originHeight: 603, maxWidthOrHeight: 1280 },
    { width: 1280, height: 321.2 },
  ],
  [
    { originWidth: 1200, originHeight: 820, maxWidthOrHeight: 1280 },
    { width: 1200, height: 820 },
  ],
  [
    { originWidth: 200, originHeight: 200, maxWidthOrHeight: 1280 },
    { width: 200, height: 200 },
  ],
])(
  'calculateAspectRatioSize에서 입력받은 너비, 높이가 `maxWidthOrHeight`보다 크다면, 너비를 `maxWidthOrHeight`로 설정하고, 높이를 원래의 가로 세로 비율을 유지하며 계산하여 반환합니다. 너비, 높이 둘 중 긴 값이 최대 너비 혹은 높이보다 작다면 기존의 너비, 높이를 반환합니다.',
  ({ originWidth, originHeight, maxWidthOrHeight }, expected) => {
    const result = calculateAspectRatioSize({ originWidth, originHeight, maxWidthOrHeight });

    expect(result).toEqual(expected);
  }
);

3. 파일을 받아 이미지를 변환하여 파일 리스트를 반환

  1. 가로, 세로의 사이즈를 구한 뒤 imageConverter 함수를 이용하여 변환된 파일을 만듭니다
  2. input의 파일을 교체하기 위해서는 dataTransfer을 이용해서 input.files = dataTransfer.files를 해줘야 합니다

compressedBlob = [File]


outputWebpFile = File


dataTransfer.files = FileList


convertImageToWebP.ts

import { imageConverter } from 'upload-images-converter';

import { calculateAspectRatioSize } from './calculateAspectRatioSize';
import { getImageSize } from './getImageSize';

export const convertImageToWebP = async (imageFile: File) => {
  const { width: originWidth, height: originHeight } = await getImageSize(imageFile);

  const { width, height } = calculateAspectRatioSize({
    originWidth,
    originHeight,
    maxWidthOrHeight: 1280,
  });

  const compressedBlob = await imageConverter({
    files: [imageFile],
    width,
    height,
  });

  const outputWebpFile = new File([compressedBlob[0]], `${Date.now().toString()}.webp`);

  const dataTransfer = new DataTransfer();

  dataTransfer.items.add(outputWebpFile);

  return dataTransfer.files;
};

사용 방법

  const webpFileList = await convertImageToWebP(imageFile);

  inputElement.files = webpFileList;

참고자료

https://developer.mozilla.org/ko/docs/Web/API/DataTransfer

profile
Fun from Choice! 오늘도 즐거운 한 표
post-custom-banner

0개의 댓글