본문 바로가기
Projects,Activity/DDD(Next)

폴라보는 이미지를 이렇게 최적화합니다.

by 그냥하는거지뭐~ 2024. 8. 28.

서비스 소개 

폴라보는 DDD 웹 1팀에서 진행하고 있는 프로젝트로, 친구 혹은 연인들끼리 "보드"를 만들어 폴라로이드를 붙이면서 보드를 꾸미고 추억을 공유하는 서비스입니다. 제가 이미지 관련된 기능 개발을 담당하게 되면서, 어떤 식으로 이미지를 최적화했는지 소개해보려고 합니다. 
 


이미지 압축을 통한 로딩 시간 개선

1. browser-image-compression

압축 없이 이미지를 업로드하면서 보드에서 폴라로이드 사진을 로딩하는 시간이 너무 오래 걸리는 문제가 있었습니다. 이를 해결하기 위해 S3에 이미지를 업로드하기 전에 먼저 압축하는 방법을 선택했고, 이 과정에서 browser-image-compression이라는 라이브러리를 사용했습니다.

const file = event.target.files[0]
const options = {
    maxSizeMB: 0.2,
    maxWidthOrHeight: 1920,
    useWebWorker: true,
}
const compressedFile = await imageCompression(file, options)
setCompressedFile(compressedFile)

성능을 확인하기 위해 일부러 용량이 큰 이미지를 테스트해 보았습니다.
 

large image

1024로 나누면 KB 단위로 변환할 수 있습니다. 결과적으로, 이미지는 10.60MB에서 384.87KB로 압축되었습니다. 일반적으로 50~300KB 사이의 파일 크기가 이상적이라고 할 수 있는데, 이 라이브러리를 통해 큰 이미지도 효과적으로 압축할 수 있음을 확인했습니다. 이런 식으로 압축된 파일은 폴라로이드 사진 밑에 첨부될 한 줄 문구와 함께 server action을 통해 서버로 전송됩니다. 
 
그런데 한 가지 의문이 들었습니다. 제가 설정해 둔 maxSizeMB는 0.2MB(204.8KB)인데, 압축된 파일의 크기가 이보다 큰 384.87KB인 게 이상하지 않나요? 원인을 분석하기 위해 browser-image-compression 라이브러리의 소스코드를 뜯어보았습니다. 
 

Under the hood

browser-image-compression 라이브러리가 어떤식으로 압축을 처리하지 알아봅시다. https://github.com/Donaldcwl/browser-image-compression

설명을 보면, 애플리케이션 서버에 업로드하기 전에 resolution이나 storage size을 줄여서 jpeg, png, webp, bmp를 더 작게 압축한다고 소개하고 있습니다. 어떤 식으로 구현해 놨는지 소스코드를 열어봅시다. lib/image-compression.js에서 힌트를 찾을 수 있었습니다. 
 
1차 압축: 이미지 크기 조정

const [, origCanvas] = await drawFileInCanvas(file, options); // 원본 이미지를 캔버스에 그림
const maxWidthOrHeightFixedCanvas = handleMaxWidthOrHeight(origCanvas, options);

새로운 canvas를 생성한 후, 사용자가 입력한 options.maxWithOrHeight를 기준으로 이미지를 다시 그리고 이를 이미지 파일로 변환합니다. 
 
2차 압축: quality 조정 
이제 이미지를 canvasToFile 함수를 이용해 지정된 품질(quality)과 파일 형식(outputFileType)으로 변환합니다. 이 단계에서 파일의 크기가 사용자가 설정한 maxSizeMB 제한을 초과하는지 확인합니다

let quality = options.initialQuality || 1.0;
const outputFileType = options.fileType || file.type;
const tempFile = await canvasToFile(
  orientationFixedCanvas,
  outputFileType,
  file.name,
  file.lastModified,
  quality
);
const origExceedMaxSize = tempFile.size > maxSizeByte; // 압축된 이미지가 maxSizeByte보다 크다면 추가 압축

 
압축된 이미지가 maxSizeByte보다 크다면, 무한 루프를 통해 반복 압축합니다. 

while (
  remainingTrials-- &&
  (currentSize > maxSizeByte || currentSize > sourceSize)
) {
  const newWidth = shouldReduceResolution
    ? canvas.width * 0.95  // width를 5% 줄임
    : canvas.width;
  const newHeight = shouldReduceResolution
    ? canvas.height * 0.95 // height를 5% 줄임
    : canvas.height;
  
  [newCanvas, ctx] = getNewCanvasAndCtx(newWidth, newHeight);

  ctx.drawImage(canvas, 0, 0, newWidth, newHeight);

  if (outputFileType === "image/png") { // quality 낮춤
    quality *= 0.85;
  } else {
    quality *= 0.95;
  }

  compressedFile = await canvasToFile(
    newCanvas,
    outputFileType,
    file.name,
    file.lastModified,
    quality
  );
  // ...
}

- width, height를 5%씩 줄여가면서 이미지 크기를 줄입니다
- 파일 형식에 따라 quality를 낮춥니다. (PNG는 85%로, JPEG 등은 95%로)

🤔 왜 파일 형식에 따라 quality 감소 정도를 다르게 한 걸까요? 
형식마다 압축 방식이 다르기 때문입니다.
PNG는 무손실 압축 방식을 사용합니다. 즉, 압축 후에도 원본 이미지의 모든 데이터를 유지한다는 뜻이죠. 그래서 quality 값을 줄여도 파일 크기 감소가 상대적으로 적고, 품질에 미치는 영향이 JPEG보다 적습니다. 
반면, JPEG는 손실 압축 방식을 사용합니다. 따라서 quality 값을 줄이면 파일 크기는 크게 줄어들지만, 이미지 품질이 눈에 띄게 저하될 수 있습니다. 그리고 JPEG는 원래 품질을 손쉽게 조정할 수 있는 포맷으로, quality값을 조금만 줄여도 파일 크기를 크게 줄일 수 있어서 95%로만 줄여도 큰 효과를 볼 수 있습니다. 

 
제가 찾고자 했던 문제의 원인은 무한루프 코드에서 힌트를 찾을 수 있었습니다. 루프가 한 번 돌 때마다 remainingTrials가 1씩 감소하는 게 보이시나요? remainingTrials가 어떤 식으로 선언되어 있는지 찾아봅시다. 

let remainingTrials = options.maxIteration || 10;

사용자가 maxIteration 값을 따로 설정하지 않으면 디폴트 값으로 10이 사용됩니다. 그래서 제가 테스트한 이미지는 10MB로 용량이 매우 컸기 때문에, 10번의 루프로도 목표했던 0.2MB까지 압축되지 못했던 것이죠.
 

2. 압축하는 시점 변경으로 업로드 시간 단축 

이미지를 압축하는 시간이 생각보다 오래걸립니다. 맥북 Chrome 기준으로 133.87KB는 99.80ms, 443.87KB는 2.8초, 10MB는 12초 정도 걸렸습니다. 하지만 모바일로 테스트해 봤을 때 2MB도 체감상 1초도 안 걸렸습니다. 아마 기기별로 이미지 처리 알고리즘이 다르고, 하드웨어 차이일 것이라고 생각됩니다. 
 
대부분의 유저가 모바일로 사용할 것이 예상되어서 이미지 압축 시간이 큰 문제 되지 않을 것 같습니다. 하지만, 압축하는 시점을 변경함으로써 폴라로이드 업로드 속도를 조금이나마 개선했습니다. 
 
변경 전에는 사용자가 사진을 선택하고 한줄 메모까지 입력한 후, '업로드하기' 버튼을 누르면 그제야 압축이 시작됐습니다. 즉, 서버에 보내기 직전에 압축한 후에, 압축된 이미지 파일과 한 줄 메모를 서버에 전송하는 방식이었습니다. 그러다 보니, 서버의 응답 시간 + 이미지 압축 시간이 지난 후에야 사용자는 업데이트된 UI를 볼 수 있었습니다. 

 
로딩 시간을 조금이나마 단축하기 위해 압축 시점을 사진을 선택한 직후로 변경했습니다. 압축이 되는 동안 사용자는 한줄 메모를 입력할 수 있고, 업로드 시간도 단축되었습니다. 


마치면서

어떤 문제가 생겼을 때 라이브러리의 내부 동작 원리를 이해하고 해결하는 것이 그렇지 않은 것과는 큰 차이가 있다고 생각합니다. 내부 동작을 이해할수록, 제가 개발하고 있는 서비스를 더욱 정교하게 개발할 수 있다는 것을 느낄 수 있었습니다. 추가로, 잘 짜여진 코드를 많이 구경해보는게 제 코드 스타일에도 큰 도움이 되는 것 같습니다. 
 
 
 
https://polabo.site/

 

POLABO | 함께 꾸미는 폴라로이드 보드, 폴라보

우리의 일상도 특별하게! 소중한 추억들을 공유하며 폴라로이드로 보드를 꾸며봐요.

polabo.site