[NestJS] Sharp를 이용한 이미지 포맷 최적화 (to webp)
작업 배경
이번에 라멘로드 에 리뷰 기능을 개발하게 되었습니다.
리뷰 기능이 추가됨에 따라 S3 요금 걱정도 하게 되었습니다. S3에서 이미지를 불러오고 업로드할 때 용량 걱정이 되더라구요. 물론 사용자가 적어서 비용이 발생하더라도 크진 않겠지만..
고민을 하던 도중, 라멘로드 프론트엔드 팀원 중 한분께서 webp 포맷을 적용해보는 건 어떠냐고 말씀주셨습니다. 최근 웹에서 이미지를 다운로드 받을 때 webp 로 다운로드되는 경우가 있어서 짜증나는 마음에 webp 를 알고있었는데, 이미지 용량 최적화에 뛰어난 포맷이라고 알려주시더라구요.
또, 최근 많은 기업이 최적화를 목적으로 이미지 파일을 webp 로 저장한다는 사실도 알게 되었습니다. (ex. 구글, 무신사 등)
이런 배경에서 비용 절감 + 최적화를 목적으로 이미지 포맷 최적화 작업을 하기로 했습니다.
Webp란 ?
webp, 웹피는 구글에서 만든 이미지 포맷입니다. jpeg, jpg, png 등과 같은 이미지 포맷 중에 하나죠.
webp는 타 포맷들에 비해 적은 용량을 자랑합니다. 손실 압축은 30%, 무손실 압축은 20% 정도 압축된다고 하네요.
Sharp
이미지 포맷 변환을 위해 Sharp 라이브러리를 사용했습니다. Node 에서 가장 많이 쓰이는 이미지 프로세싱 라이브러리 중 하나이며 성능이 좋다고 하네요. 저는 그냥 래퍼런스가 많아서 사용했습니다.
코드 리팩토링
기존 코드
리뷰를 작성할 때 이미지 업로드 과정을 거치는데 기존에는 아래 코드를 사용하고 있었습니다.
async uploadImageFileToS3(
path: string,
name: string,
file: Express.Multer.File,
): Promise<string> {
if (!this.ACCEPTABLE_MIME_TYPES.includes(file.mimetype)) {
throw new BadRequestException(
'이미지 파일 확장자는 jpg, png, jpeg만 가능합니다.',
);
}
if (file.size > this.MAXIMUM_IMAGE_SIZE) {
throw new BadRequestException(
'업로드 가능한 이미지 최대 용량은 5MB입니다.',
);
}
try {
await new AWS.S3()
.putObject({
Key: path + name,
Body: file.buffer,
Bucket: this.S3_BUCKET_NAME,
ContentType: file.mimetype,
})
.promise();
const url = https://${this.S3_BUCKET_NAME}.s3.amazonaws.com/${path}${name}
return url;
} catch (error) {
return new InternalServerErrorException('S3 업로드 실패');
}
}
이미지 확장자와 용량 확인 후 S3 올린 후 저장 위치를 반환하는 로직입니다.
Sharp 설치
먼저, sharp 라이브러리를 설치해주었습니다.
yarn add sharp
Import Sharp
service.ts 에서 Sharp 라이브러리를 사용하기 위해 Import 해줬구요.
import * as sharp from 'sharp';
이미지 파일의 buffer 를 webp 포맷으로 변경하는 메소드
이제, 본격적인 작업입니다. jpeg, png 등 다양한 이미지 포맷을 webp 로 변경하기 위해 아래 메소드를 만들었습니다.
async convertToWebp(buffer: Buffer): Promise<Buffer> {
try {
const webpBuffer = await sharp(buffer).webp().toBuffer();
return webpBuffer;
} catch (error) {
throw new InternalServerErrorException('WebP 변환 실패');
}
}
위 uploadImageFileToS3 메소드에서 해당 메소드를 사용할 것 입니다.
저는 이미지 화질이 크게 중요하지 않아 손실 압축 방식으로 진행했습니다.
손실 값은 webp()에 { quality: 90 } 와 같은 옵션을 통해 설정할 수 있습니다. 손실 기본 값은 80이고 저는 기본 값으로 진행했습니다.
const webpBuffer = await sharp(buffer).webp({ quality: 90 }).toBuffer();
무손실 압축은 webp() 에 { lossless: true } 옵션을 추가하면 됩니다.
const webpBuffer = await sharp(buffer).webp({ lossless: true }).toBuffer();
uploadImageFileToS3 코드 리팩토링
async uploadImageFileToS3(
path: string,
name: string,
file: Express.Multer.File,
): Promise<string | Error> {
if (!this.ACCEPTABLE_MIME_TYPES.includes(file.mimetype)) {
throw new BadRequestException(
'이미지 파일 확장자는 jpg, png, jpeg만 가능합니다.',
);
}
if (file.size > this.MAXIMUM_IMAGE_SIZE) {
throw new BadRequestException(
'업로드 가능한 이미지 최대 용량은 5MB입니다.',
);
}
//변경점 1
//모든 이미지는 webp로 변환하여 업로드
const webpBuffer = await this.convertToWebp(file.buffer);
try {
//변경점 2
await new AWS.S3()
.putObject({
Key: path + name + '.webp',
Body: webpBuffer,
Bucket: this.S3_BUCKET_NAME,
ContentType: 'image/webp',
})
.promise();
//변경점 3
const url = https://${this.S3_BUCKET_NAME}.s3.amazonaws.com/${path}${name}.webp;
return url;
} catch (error) {
return new InternalServerErrorException('S3 업로드 실패');
}
}
변경점 1.
만들어 둔 convertToWebp 메소드에 file.buffer 를 매개변수로 넣어 webp 형식의 Buffer를 리턴받아옵니다.
변경점 2.
S3 버킷에 업로드 시, ContentType 를 'image/webp'로 설정하고 파일 명(Key)의 확장자를 .webp 로 설정하여 저장합니다. (ContentType 만 'image/webp' 로 설정하면 파일 형식이 변경되지 않습니다.)
변경점 3.
uploadImageFileToS3 메소드가 리턴하는 url 엔드포인트를 변경합니다.
결과
테스트 결과, 120KB 용량의 이미지가 83.1 KB 로 약 30% 정도 압축되는 걸 확인했습니다.