[TTI(Time to interaction)를 개선]
이미지를 개선하기 위한 방법은 크게 압축과 리사이징 두 가지로 나눌 수 있다.
글을 작성하기 앞서 프로젝트 진행중 multer를 통한 이미지 업로드 중에 이미지의 크기로 인해
업로드 속도가 느린 현상을 발견했고 이 문제점에 대해 성능을 개선하고자 알게 된 것이
S3의 lambda 함수이다.
이미지 리사이징 작업은 CPU와 메모리를 많이 사용한다. 때문에 로컬 서버에서 작업을 하게되면
다른 사용자의 요청을 못받는 현상이 생길 수 있다.
싱글 스레드 기반 Node 서버는 비동기 프로그래밍 기반 I/O 작업에 유리하나 CPU 작업에는 불리하다.
그래서 이미지 리사이징 전용 서버를 따로 돌리자니 이것은 자원 낭비이다.
이러한 상황에서 서버리스 서비스를 활용해 문제점을 해결할 수 있다.
서버리스 서비스를 통해 특정 동작을 수행하는 로직을 저장하고 요청이 들어오면 이를 작동시킨다.
마치 함수처럼 호출 할 때 실행하여 Faas(Function-as-a-Service, 서비스로서의 함수 기능)라고 불린다.
이처럼 람다 함수를 활용해 독립적인 서버에서 함수를 실행 시켜, 로컬 서버의 부담을 줄일 수 있다.
온디멘드(on-demand, 사용한 만큼 청구) 형식이라 쓴만큼 요금을 지불하면 되서 항상 컴퓨팅을 실행시키는 자원 낭비를 줄일수 있다.
현재 Node.js 기반 이미지 리사이징 패키지인 sharp 모듈을 통해 구현해볼 예정이다.
sharp를 이용하여 이미지 리사이징 API 로컬 서비스를 구현해보도록 하겠다.
Node 진영에는 많은 이미지 리사이징 패키지들이 있었지만, 끝까지 살아남은 모듈이 sharp로 알고있다.
이미지 리사이징 동작 자체가 cpu와 메모리를 잡아먹는데,
가끔 out of memory로 node가 죽는 경우가 있다고 알고 있으며,
그런 관점에서 sharp 모듈은 리사이징 속도도 빠르며 메모리도 다른 동종 모듈 대비 많이 잡아 먹지 않는다고 알고있다.
때문에 나는 sharp 모듈을 사용하고자 한다.
Tip
주의할 점은sharp모듈을 설치할때 리눅스 바이너리 버전으로 설치해야 된다는 점이다.
(람다는 Amazon Linux 기반으로 돌아감)
$ npm install --platform=linux sharp
lambda S3 이미지 리사이징 아키텍쳐 실행 순서(흐름) 요약

bucket original/ 경로에 업로드한다.lambda에서 bucket에 original/에 있는 이미지를 가져오고,bucket thumb/ 경로에 리사이징된 이미지를 업로드multer를 통해 S3에 이미지 업로드는 구현했다 가정하고 lambda 함수 사용법에 대해서만
포스팅을 하겠습니다.
일단 가장 먼저 저장할 S3 버킷을 만들고, 버킷 정책을 다음과 같이 객체를 PUT, GET, DELETE 동작을 허용하도록 하자.




버킷의 권한 설정에서 버킷 정책 편집기를 작성한다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AddPerm", // 권한을 추가한다.
"Effect": "Allow", // 허용권한
"Principal": "*", // 모두
"Action": [
"s3:GetObject", // s3로부터 이미지를 가져오는 것
"s3:PutObject", // s3로부터 이미지를 넣는 것
],
"Resource": "arn:aws:s3:::<버킷명>/*" // 나의 버킷명
}
]
}
multer에 AWS의 s3에 이미지를 저장할 수 있게 경로를 설정한다.
// awsMulterModule
const multer = require('multer');
const AWS = require('aws-sdk');
const multerS3 = require('multer-s3');
const path = require('path');
AWS.config.update({
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
region: 'ap-northeast-2',
});
//* AWS의 S3란?
// multer-s3는 multer를 통해 S3에 저장하기 위해서 사용하는 라이브러리
module.exports = multer({
storage: multerS3({
s3: new AWS.S3(),
bucket: 'kyh-my-bucket',
key(req, file, cb) {
// original 폴더 안에 업로드한 파일을 넣을 것이다.
// 이름이 겹치지 않게 파일 이름에 타임스템프를 더해준다.
// 이렇게 s3 버켓에 저장한다.
cb(null, `original/${Date.now()}_${path.basename(file.originalname)}`);
},
}),
limits: { fileSize: 20 * 1024 * 1024 }, // 20MB
});
router 안에서 req.file.location;을 통해 s3에 올라가있는 주소를 제공해준다.
app.get("/", (req, res, next) => {
const imgFileInfo = req.file.location;
console.log(req.file);
res.status(200).json({
url: imgFileInfo
})
})
이미지 리사이징 하는 작업 코드를 람다 함수 양식에 맞춰 구현한다.
이때 로컬 서버에서 파일을 추가하여 구현하는게 아닌 독립된 프로젝트로 새로 생성해서 index.js 파일에 구현을 해야한다.
왜냐하면 프로젝트 자체를 zip으로 압축한뒤 그대로 람다에 업로드할 것이기 때문이다.

프로젝트에 폴더를 하나 만들고 npm init을 통해 package.json이 생기도록 한다.
이는 독립된 프로젝트를 생성하기 위함이다.
Mini-Project-RamTem-API/lambda$ npm install aws-sdk
Mini-Project-RamTem-API/lambda$ npm install --platform=linux sharp
// lambda 독립적으로 사용할 프로젝트 파일
// 해당 lambda/ 폴더의 프로젝트 자체를 zip으로 압축한 뒤 그대로 lambda에 업로드 할 예정
const AWS = require('aws-sdk');
const sharp = require('sharp'); // 이미지 리사이징 패키지 모듈
const s3 = new AWS.S3(); // 람다 자체가 AWS 에서 돌려준다. 알아서 설정한 나의 정보가 들어간다.
exports.handler = async (event, context, callback) => {
//
const Bucket = event.Records[0].s3.bucket.name; // 버킷명
// original/12312312_abc.png, decodeURIComponent: 한글 깨짐현상 해결
const Key = decodeURIComponent(event.Records[0].s3.object.key); // 파일명
console.log('Bucket: ', Bucket, 'Key: ', Key);
const filename = Key.split('/')[Key.split('/').length - 1]; // 파일명에서 파일이름 추출
const ext = Key.split('.')[Key.split('.').length - 1].toLowerCase(); // 파일명에서 확장자 추출.toLowerCase(), 확장자 대문자를 소문자로
const requiredFormat = ext === 'jpg' ? 'jpeg' : ext; // sharp에서는 jpg 대신 jpeg를 사용합니다.
console.log('filename', filename, 'ext', ext);
try {
// getObject: s3로부터 이미지를 가져오는 것
const s3Object = await s3.getObject({ Bucket, Key }).promise(); // 버퍼로 가져오기
console.log('original', s3Object.Body.length);
// sharp 이미지 리사이징 관련 공식문서 참고
// 키워드: sharp documentation
// https://sharp.pixelplumbing.com/api-resize
const resizedImage = await sharp(s3Object.Body) // sharp를 통한 리사이징
.resize(400, 400, { fit: 'inside' }) // 비율 유지하면서 꽉차게
.toFormat(requiredFormat) // jpg -> jpeg, png는 그대로 png 사용
.toBuffer(); // 리사이징된 결과물이 Buffer로 나온다.
await s3 // thumb 폴더에 저장
.putObject({ // putObject: s3로 부터 이미지를 넣는것
Bucket,
Key: `thumb/${filename}`, // ex> original/test.png (크기: 20mb) -> thumb/test.png (크기: 4mb)
Body: resizedImage,
})
.promise(); // getObject와 putObject는 .promise() 붙여야만 위의 await을 사용 가능하다. 에러 조심
console.log('put', resizedImage.length);
// http 요청을 통해서 람다를 호출하는 경우에만 callback이 의미가 있다. (http 요청이 있어야 응답이 있다.)
// s3를 통해 람다를 호출하는 경우에는 callback이 의미가 없다.
return callback(null, `thumb/${filename}`); // 2번째 인자: 성공객체
} catch (error) {
console.error(error);
return callback(error); // 첫 번째 인자: 에러
}
};
완료 후 git add > git commit > git push