이미지 최적화 하기(aka.이미지 압축)

이희제·2024년 7월 9일
post-thumbnail

개발 업무를 진행하면서 서비스의 웹 성능 최적화를 위해 사용될 이미지를 관리자에서 압축을 하고 CDN에 업로드할 필요가 있었다. (서비스의 특성상 이미지가 많이 사용된다.)

어떻게 적용할 수 있는 지 알아보자.


1. 적용 방법

유저가 이미지를 업로드를 했을 때 해당 이미지를 압축하고 해당 이미지를 CDN 서버에 업로드해야 된다.

일단 클라이언트 단에서 이미지를 받고 압축을 하기 위한 방법으로 찾아본 결과 다음 2가지 방법이 있었다.

  1. 이미지 데이터를 직접 webp로 압축 및 변환
  2. browser-image-compression 모듈 사용을 통한 이미지 압축 및 webp 변환
  3. compressorjs 모듈 사용

각각 하나 씩 적용해보자.

1-1. 직접 webp로 압축 및 변환

코드로 살펴보자.

"use client"

import { useState } from 'react';

export default function WebpComponent() {
  const [originalImage, setOriginalImage] = useState(null);
  const [compressedImage, setCompressedImage] = useState(null);
  const [quality, setQuality] = useState(0.8); // 초기 퀄리티 설정 (0.8 = 80%)

  const handleImageUpload = (event) => {
    const imageFile = event.target.files[0];
    setOriginalImage(URL.createObjectURL(imageFile));
    const reader = new FileReader();
    reader.readAsDataURL(imageFile);
    reader.onloadend = () => {
      const image = new Image();
      image.src = reader.result;
      image.onload = () => {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        
        // 원하는 크기로 캔버스 크기 설정 (여기서는 원본 크기)
        canvas.width = image.width;
        canvas.height = image.height;
        
        ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
        
        // WebP 형식으로 변환 및 압축
        const compressedDataUrl = canvas.toDataURL('image/webp', quality);
        setCompressedImage(compressedDataUrl);
      };
    };
  };

  const handleQualityChange = (event) => {
    setQuality(Number(event.target.value));
  };

  return (
    <div>
      <h1>Image Upload and Compression</h1>
      <input type="file" accept="image/*" onChange={handleImageUpload} />
      <div style={{ margin: '20px 0' }}>
        <label>
          Quality: 
          <input
            type="number"
            min="0.1"
            max="1"
            step="0.1"
            value={quality}
            onChange={handleQualityChange}
          />
        </label>
      </div>
      <div style={{ margin: '20px 0' }}>
        {originalImage && (
          <div>
            <h2>Original Image</h2>
            <img src={originalImage} alt="Original" style={{ maxWidth: '100%' }} />
          </div>
        )}
        {compressedImage && (
          <div>
            <h2>Compressed Image (WebP)</h2>
            <img src={compressedImage} alt="Compressed" style={{ maxWidth: '100%' }} />
          </div>
        )}
      </div>
    </div>
  );
}

사용자의 이미지를 입력받고 해당 이미지를 webp로 압축/변환하고 화면에 보여주도록 코드를 구성했다.

canvas에 이미지를 다시 그리는 방식으로 압축을 진행하는 것이다. 과정은 다음과 같다.
를 통해

  1. readAsDataURL 통해 File 객체를 읽어 데이터 URL로 반환한다.
  2. 새로운 <canvas> 요소를 생성 생성하고 2D 그래픽 컨텍스트를 가져온다.
  3. 2D 그래픽 컨텍스트의 drawImage 메서드를 통해 이미지를 그린다.
  4. HTML5 Canvas API의 메서드인 canvas.toDataURL을 통해 캔버스의 콘텐츠를 데이터 URL 형식으로 변환하고 반환한다. (이미지 압축, 포맷 변경 포함)
    • type은 image/webp를 설정하고 손실 압축을 지원하는 image/jpeg 또는 image/webp의 퀄리티를 지정할 수 있는 encoderOptions 값을 0.7로 설정했다.

toBlob을 통해 이미지 데이터를 Blob 객체로 변환하여 추후 CDN에 업로드할 때 사용할 수 있을 것이다. (비동기적으로 동작한다.

결과를 확인해보자. jpg 파일을 업로드 했을 때 절반 가량 이미지 용량이 줄어든 것을 확인할 수 있었다.

압축 전

압축 후


1-2. browser-image-compression 사용

모듈을 사용하면 더 간단하게 이미지를 압축하고 webp로 변환할 수 있다. (용량 참고)

코드는 다음과 같다.

"use client"

import { useState } from 'react';
import imageCompression from 'browser-image-compression';

export default function ImageUpload() {
  const [originalImage, setOriginalImage] = useState(null);
  const [compressedImage, setCompressedImage] = useState(null);

  const handleImageUpload = async (event) => {
    const imageFile = event.target.files[0];
    setOriginalImage(URL.createObjectURL(imageFile));

    const options = {
      maxSizeMB: 10,
      useWebWorker: true,
      initialQuality: 0.7,
      fileType: 'image/webp'
    };

    try {
      const compressedFile = await imageCompression(imageFile, options);
      const compressedImageUrl = URL.createObjectURL(compressedFile);
      setCompressedImage(compressedImageUrl);
    } catch (error) {
      console.error('Error compressing the image:', error);
    }
  };

  return (
    <div>
      <h1>Image Upload and Compression</h1>
      <input type="file" accept="image/*" onChange={handleImageUpload} />
      <div style={{ margin: '20px 0' }}>
        {originalImage && (
          <div>
            <h2>Original Image</h2>
            <img src={originalImage} alt="Original" style={{ maxWidth: '100%' }} />
          </div>
        )}
        {compressedImage && (
          <div>
            <h2>Compressed Image</h2>
            <img src={compressedImage} alt="Compressed" style={{ maxWidth: '100%' }} />
          </div>
        )}
      </div>
    </div>
  );
}

위 코드를 기반으로 이미지 압축/변환 과정은 다음과 같다.

  1. 이미지 파일 객체를 들고 온다.

  2. option 객체를 정의하고 내부 설정을 한다.

    • 파일 사이즈 제한
    • 이미지 파일 타입
    • 웹 워커 사용 여부 (기본 true)
    • 이미지 퀄리티
  3. browser-image-compression에서 import한 imageCompression 메서드에 imageFile, options을 넘긴다. (옵션 참고)

  4. 반환된 결과값을 URL.createObjectURL 메서드를 통해 임시 URL을 생성하여 화면에 보여준다.

    • 반환된 결과값은 File 객체이다.
    • createObjectURL- 메모리에 있는 객체(일반적으로 Blob이나 File 객체)에 대한 임시 URL을 생성 (참고)

압축 전

압축 후

직접 구현한 것이랑 차이가 없는 것을 확인할 수 있다. 즉 동일한 로직을 통해 압축을 적용했다는 것을 예상할 수 있는데 모듈 내부 코드를 확인해보자.

https://github.com/Donaldcwl/browser-image-compression/blob/master/lib/utils.js#L256


1-3. compressorjs 사용

browser-image-compression 모듈과 비교했을 때 5분의 1수준의 용량을 가지고 있어 가볍다. (참고)

코드를 통해 살펴보자.

'use client'

import React, { useState } from 'react';
import Compressor from 'compressorjs';

const CompressorComponent = () => {
  const [compressedImage, setCompressedImage] = useState(null);
  const [originalImage, setOriginalImage] = useState(null);

  const handleImageUpload = (event) => {
    const file = event.target.files[0];
    if (file) {
      setOriginalImage(URL.createObjectURL(file));
      new Compressor(file, {
        quality: 0.7, // 압축 품질 설정 (0 ~ 1)
        mimeType: "image/webp",
        success: (compressedResult) => {
          setCompressedImage(URL.createObjectURL(compressedResult));
        },
        error(err) {
          console.error(err.message);
        },
      });
    }
  };

  return (
    <div>
      <h1>Image Compressor</h1>
      <input type="file" accept="image/*" onChange={handleImageUpload} />
      {originalImage && (
        <div>
          <h2>Original Image</h2>
          <img src={originalImage} alt="Original" style={{ maxWidth: '100%', margin: '10px 0' }} />
        </div>
      )}
      {compressedImage && (
        <div>
          <h2>Compressed Image (WebP)</h2>
          <img src={compressedImage} alt="Compressed" style={{ maxWidth: '100%', margin: '10px 0' }} />
        </div>
      )}
    </div>
  );
};

export default CompressorComponent;

이미지 압축/변환 과정은 browser-image-compression를 사용했을 경우와 역시나 비슷하다.

  1. 이미지 파일 객체를 들고 온다.

  2. import한 Compressor(class이다)
    를 이용해 new Compressor로 인스턴스를 생성한다. 인스턴스를 생성할 때 이미지 파일과 옵션값을 넘겨준다. (옵션 참고)

    • 반환 이미지 파일 타입 (mimeType)
    • 이미지 퀄리티 (quality)
  3. success fallback 함수를 통해 압축된 결과값을 받고 createObjectURL를 통해 임시 URL를 생성해서 화면에 노출한다. (비동기로 압축 진행)

결과를 보면 앞서 본 2개의 방식과 차이가 없다.

압축 전

압축 후


2. 브라우저 호환성 확인

이미지를 모두 webp로 변환을 했는데 브라우저 호환성을 살펴보자. 서비스는 chrome 59에서도 동작을 해야한다.

따라서 이미지 포맷의 호환성이 중요한데, 거의 모든 환경에서 지원되지만 ios 14미만에서는 안되는 것을 확인할 수 있다. 서비스는 ios 14이상부터 지원되기 때문에 이슈가 없다.

https://caniuse.com/?search=webp

이미지를 사용할 때뿐만 아니라 외부 패키지를 사용할 때도 브라우저 호환성을 꼭 확인하자.


3. 더 알아보기

webp란?

webp란 구글에서 개발한 이미지 파일 포맷이다. JPEG와 PNG 포맷보다 더 높은 압축 효율을 제공하며, 무손실 및 유손실 압축을 모두 지원한다. 이를 통해 웹페이지 로딩 속도를 향상시키고 저장 공간을 절약할 수 있다.

대표적으로 JPG/JPEG와 PNG에 대해 손실/무손실 압축 지원 여부를 보자.

JPG/JPEG

  • 손실 압축: 지원
  • 무손실 압축: 미지원
  • 특징: 주로 사진과 같은 복잡한 이미지를 압축하는 데 사용되며, 압축률이 높지만 화질이 저하될 수 있다.

PNG

  • 손실 압축: 미지원
  • 무손실 압축: 지원
  • 특징: 투명도 지원이 필요하거나 화질 저하 없이 이미지를 저장해야 할 때 사용된다. 일반적으로 그래픽, 아이콘, 로고 등에 적합하다.

canvas란?

canvas란 HTML5의 요소 중 하나로, 자바스크립트를 사용하여 동적으로 그래픽을 그리기 위한 도구이다. 픽셀 단위의 그림을 그릴 수 있는 2D 그래픽 컨텍스트와 3D 그래픽을 그릴 수 있는 WebGL 컨텍스트를 제공한다. 주로 게임, 데이터 시각화, 애니메이션 등에 사용된다.

blob이란?

blob이란 Binary Large Object의 약자로, 대용량의 이진 데이터를 저장하기 위한 데이터 타입이다. 파일, 이미지, 비디오 등의 바이너리 데이터를 다룰 때 사용된다. JavaScript에서는 Blob 객체를 사용하여 파일이나 데이터를 로컬에 저장하거나 서버로 전송할 수 있다.


참고내역
https://youngble.tistory.com/73
https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
https://www.jeong-min.com/40-image-upload-preview/

profile
그냥 하자

0개의 댓글