티스토리 뷰
이미지 최적화
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
- AWS
- dockerfile
- 서버 to 서버
- nodejs
- docker
- 오블완
- Storybook
- React
- 티스토리챌린지
- 스벨트
- Vite
- Embedding
- NUXT
- seo
- vscode
- Github Actions
- 타입스크립트
- svelte
- nuxt2
- vue router
- cors
- 네이버 서치 어드바이저
- NextJS
- vue composition api
- openAI
- Git
- nextjs13
- nextjs14
- webpack
- 깃허브
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |