티스토리 뷰

이미지 최적화

PC, Mobile 에서 작업을 하다보면 이미지 크기로 인해 웹페이지 성능이 안 좋아지는 경우가 있다.
웹페이지 성능개선을 하다보면 LightHouse 를 사용하게 되는데 여기서 png, jpeg, jpg 등의 이미지 확장자를 사용하는 경우,
webp 로 변경해서 최적화하라는 해결방안을 볼 수 있을 것이다.

Webp

Webp 는 구글이 만든 이미지 포맷으로 png, jpeg 보다 더 나은 압축을 제공하여 품질은 같지만, 크기를 더 작게 저장할 수 있다.

설계

기존은 클라이언트에서 이미지를 업로드하면 CDN 을 걸쳐서 S3 에 이미지를 업로드 하고, 사용시에는 S3 에 저장된 이미지를 불러서
사용하는 방식을 이용하고 있었다.
여기에 Lamda@edge 를 이용해서 cdn 에 왔을시 리사이징한 이미지를 주고 캐싱 후 응답하는 방안을 채택하였다.

Lamda@edge 는 cloudFront 에 Lamda 함수를 등록하는 것 이라고 생각하면 됨.

설치

필요한 라이브러리들을 우선 설치해보자.

Sharp 설치[https://sharp.pixelplumbing.com/install]

yarn add sharp

sharp 설치시 주의해야할 점이 있다!!
sharp 라이브러리 사용시 플랫폼에 따라서 변경해주어야 한다.
리눅스 기반으로 설치시 아래 명령어로 해주자.

SHARP_IGNORE_GLOBAL_LIBVIPS=1 npm_config_arch=x64 npm_config_platform=linux yarn add sharp

aws-@sdk/client-s3

사실 aws-sdk 의 경우, 람다함수에 기본 내장되어 있기 때문에 크게 문제 없다. 설치하지 않아도 된다.

yarn add @aws-sdk/client-s3 --dev

이제부터 변환하는 로직을 만들어보자. 해당 로직은 lamda 가 아닌, 별도의 로직으로 가져가서 사용할 수 있는 로직이다.

Buffer 를 webp base64 데이터로 변환

  • 기본 quality 값은 90 (변환되는 webp 의 퀄리티를 의미)
  • sharp 를 이용해서 image 객체를 가져옴.
  • 이미지의 width 를 가져와서 해당 width 의 사이즈를 비교하여 resize 할지 말지를 정함.
  • webp 로 변환하고 이를 버퍼로 다시 변환함.
  • 변환한 버퍼사이즈를 1MB 와 비교하고 1MB 보다 클시 quality 를 낮춰서 변환하도록 재귀호출함.
  • 완료된 버퍼를 base64 로 인코딩해서 리턴함.
const convertToWebPAndBase64 = async (buff, quality = 90) => {
  if (quality === 30) throw new Error('quality is too low');

  try {
    const image = sharp(buff);
    const { width } = await image.metadata();
    const webpBuffer = await image
      .resize(width > RESIZE_WIDTH ? RESIZE_WIDTH : null)
      .webp({ quality })
      .toBuffer();

    if (webpBuffer.byteLength > MB) {
      const reduceQuality = quality - QUALITY_DEGREE;
      return await convertToWebPAndBase64(buff, reduceQuality);
    }

    return webpBuffer.toString('base64');
  } catch (error) {
    throw error;
  }
};

해당 로직을 이제 원하는 곳에 넣고 buff 값으로 받기 만하면 원하는 규정 사이즈, 퀄리티에 맞게끔 webp 로 컨버팅된 이미지를 받을 수 있음.

만약, 사이즈 조정이 굳이 필요로 하지 않다면, 사이즈 조정 부분만 빼면 됨.
아래와 같이 코드를 수정하면 끝!

await sharp(buff).webp().toBuffer();

이제 해당 로직을 lamda@edge 에 적용하면 된다. 아래는 풀 코드로 참고해주면 된다.

'use strict';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
import sharp from 'sharp';

const s3Client = new S3Client({});
const SUC_EXTENSION = ['jpg', 'jpeg', 'png', 'gif'];
const MB = 1 * 1024 * 1024;
const QUALITY_DEGREE = 10;
const RESIZE_WIDTH = 800;

// Main 함수
export const main = async (event, context, callback) => {
  const { request, response } = event.Records[0].cf;
  const orgRes = deepCopy(response);

  try {
    // 404 test, s3 에 있는지, 없는지
    const imgUrl = decodeURIComponent(request.uri);
    if (!imgUrl) {
      response.status = 404;
      return callback(null, response);
    }

    // 이미지 변환 불가능 포멧
    const imgExtension = imgUrl.split('.').pop().toLowerCase(); // 이미지 포멧 가져오기
    if (!SUC_EXTENSION.includes(imgExtension)) {
      response.headers['err-foramt'] = [
        { key: 'Err-Foramt', value: imgExtension },
      ];
      return callback(null, response); // webp 로 변환 가능한 이미지 인지? false 가 맞음.
    }

    // 여기서부터 정상 동작 가능.
    const Key = imgUrl.substring(1); // 앞에 루트를 잘라줌.
    const Bucket = request.origin.s3.domainName.split('.')[0]; // 버킷 이름 가져오기
    const params = {
      Bucket, // 가져올 이미지가 있는 S3 버킷의 이름을 입력하세요.
      Key, // 가져올 이미지의 객체 키를 입력하세요. 예: 'images/sample.jpg'
    };

    const command = new GetObjectCommand(params);
    const { Body } = await s3Client.send(command); // s3 Client
    const b = await Body.transformToByteArray();
    const buff = Buffer.from(b, 'utf-8');

    // webp base64 로 변환
    const webpBase64 = await convertToWebPAndBase64(buff);

    // cache 설정 (중간 서버, 모든 유저)
    response.headers['cache-control'] = [
      { key: 'Cache-Control', value: 'public, max-age=31536000' },
    ];
    response.headers['content-type'] = [
      { key: 'Content-Type', value: 'image/webp' },
    ];
    response.headers['content-length'] = [
      { key: 'Content-Length', value: webpBase64.length.toString() },
    ];
    response.body = webpBase64;
    response.bodyEncoding = 'base64';

    callback(null, response);
  } catch (error) {
    callback(null, orgRes);
  }
};

// deepCopy
const deepCopy = (obj) => {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  if (Array.isArray(obj)) {
    return obj.map((item) => deepCopy(item));
  }

  const clonedObj = {};
  for (const key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      clonedObj[key] = deepCopy(obj[key]);
    }
  }
  return clonedObj;
};

// Webp Covert 파일 base64 리턴
const convertToWebPAndBase64 = async (buff, quality = 90) => {
  if (quality === 30) throw new Error('quality is too low');

  try {
    const image = sharp(buff);
    const { width } = await image.metadata();
    const webpBuffer = await image
      .resize(width > RESIZE_WIDTH ? RESIZE_WIDTH : null)
      .webp({ quality })
      .toBuffer();

    if (webpBuffer.byteLength > MB) {
      const reduceQuality = quality - QUALITY_DEGREE;
      return await convertToWebPAndBase64(buff, reduceQuality);
    }

    return webpBuffer.toString('base64');
  } catch (error) {
    throw error;
  }
};

'개발.. > Node' 카테고리의 다른 글

Nodejs, sharp 이미지 리사이즈시 이미지가 돌아가는 현상  (0) 2023.09.20
nodejs 에서 openai embedding 및 코사인 유사도 사용  (0) 2023.08.21
npkill  (0) 2023.03.31
cross-env  (0) 2022.09.26
yarn berry  (0) 2022.06.22
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
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 31
글 보관함