이번에 대량의 이미지 업로드에 관련해서 프로젝트를 진행하게되었습니다. 그래서 저번 이미지 업로드 방식도 기본적인 방식이 아닌 저번 포스팅에서 언급한 Presigned URL을 사용하여 업로드 하는 방식을 통해 서버의 부담을 줄이는 작업을 하였습니다.
하지만 원본 사진을 S3에 저장하고 해당 사진으로 이미지 목록을 보여줄 경우 사진의 용량이 너무 크기 때문에 아래 영상처럼 이미지를 로딩하는데 상당한 시간이 걸리게됩니다.
이번 포스팅에서는 이러한 문제를 해결하기 위해 AWS Lambda를 사용하여 이미지를 리사아징하는 방법에 대해서 알아보겠습니다.
사진을 저장할 버킷을 생성하고 버킷 정책을 다음과 같이 객체를 PUT, GET, DELETE 동작을 허용하도록 하겠습니다.
{
"Version": "2012-10-17",
"Id": "Policy1721383456634",
"Statement": [
{
"Sid": "Stmt1721383455069",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::image-upload-tutorial/*",
]
}
]
}
image-upload-tutorial 이 부분은 본인의 버킷 이름으로 설정하면 됩니다.
함수 이름을 입력하고, Lambda를 구현할 때 사용할 언어를 선택해 줍니다. 저는 Node.js로 구현할 거기 때문에 Node.js를 선택하였습니다.
또한 Lambda 함수를 생성하고 생성한 함수가 s3 버킷에 쓰기 권한을 가져야 리사이징한 이미지를 넣을 수 있기 때문에 권한 설정이 필요합니다.
구성 → 권한에서 역할 이름을 클릭하여 S3 정책을 추가합니다.
AmazonS3FullAccess 권한을 추가해 줍니다.
이미지 리사이징 하는 작업 코드를 Lambda 함수 양식에 맞춰 구현합니다.
프로젝트 자체를 zip으로 압축한 뒤 그대로 Lambda에 업로드할 것이기 때문에 로컬 서버에서 파일을 추가하여 구현하는 게 아닌 독립된 프로젝트로 새로 생성해서 index.js 파일에 Lambda 함수를 구현해야 합니다.
Lambda 함수를 구현하는 프로젝트 구조는 아래와 같습니다.
리사이징하는 Lambda 함수를 구현할 때 필요한 모듈들을 설치해 줍니다.
npm install aws-sdk
npm install --platform=linux --arch=x64 sharp
sharp 모듈은 Node.js의 빠른 오픈 소스 이미지 처리 모듈입니다. 압축되지 않은 이미지 데이터의 일부 영역만 메모리에 저장하므로 처리 속도가 빠르다는 장점이 있습니다.
해당 모듈을 설치할 때 주의할 점은 Lambda의 경우 리눅스 기반으로 동작하기 때문에, sharp 모듈을 설치할 때 리눅스 바이너리 버전으로 설치해야 된다는 점입니다.
// sharp 모듈을 불러옴 - 이미지 변환을 위해 사용
const sharp = require("sharp");
// aws-sdk 모듈을 불러옴 - AWS 서비스 접근을 위해 사용
const aws = require("aws-sdk");
// S3 서비스 객체 생성
const s3 = new aws.S3();
// 변환 옵션 배열 - 각 옵션에는 이름과 리사이즈할 폭(width)이 정의되어 있음
const tansformationOptions = [
{ name: "w140", width: 140 },
{ name: "w600", width: 600 },
];
// Lambda 함수 핸들러 정의 - S3 이벤트를 처리함
exports.handler = async (event) => {
try {
// 이벤트에서 S3 객체 키를 추출
const Key = event.Records[0].s3.object.key;
// 키에서 파일 이름만 추출
const keyOnly = Key.split("/")[1];
console.log(`Image Resizing: ${keyOnly}`);
// S3에서 이미지를 읽어들이기 위한 스트림 생성
const imageStream = s3
.getObject({ Bucket: "na0man-image-upload-tutorial", Key })
.createReadStream();
// 변환 옵션 배열을 순회하며 각 옵션에 대해 비동기적으로 처리
await Promise.all(
tansformationOptions.map(async ({ name, width }) => {
// 새로운 키(경로) 생성
const newKey = `${name}/${keyOnly}`;
// sharp 변환기 생성 - 이미지를 회전하고 크기를 조정
const transformer = sharp()
.rotate()
.resize({ width, height: width, fit: "outside" });
// 이미지 스트림을 변환기에 파이핑하여 리사이즈된 이미지 버퍼를 생성
const resizedImageBuffer = await new Promise((resolve, reject) => {
const chunks = [];
imageStream
.pipe(transformer)
.on('data', chunk => chunks.push(chunk)) // 데이터를 청크 단위로 수집
.on('end', () => resolve(Buffer.concat(chunks))) // 모든 데이터가 수집되면 버퍼로 결합
.on('error', reject); // 오류 발생 시 reject 호출
});
// 리사이즈된 이미지를 S3 버킷에 저장
await s3
.putObject({
Bucket: "na0man-image-upload-tutorial",
Body: resizedImageBuffer,
Key: newKey
})
.promise();
})
);
// 모든 작업이 성공하면 상태 코드 200과 함께 이벤트 데이터를 반환
return {
statusCode: 200,
body: JSON.stringify(event),
};
} catch (err) {
// 오류가 발생하면 콘솔에 로그를 남기고 상태 코드 500과 함께 이벤트 데이터를 반환
console.error(err);
return {
statusCode: 500,
body: JSON.stringify(event),
};
}
};
이 코드는 AWS Lambda 함수로서, S3에 이미지가 업로드되면 이를 감지하여 여러 크기로 리사이즈한 후 다시 S3 버킷에 저장하는 기능을 합니다. 이를 위해 sharp 라이브러리를 사용하여 이미지를 회전하고 크기를 조정하며, aws-sdk를 통해 S3 객체를 읽고 씁니다.
함수는 이벤트에서 S3 객체 키를 추출한 후, 해당 이미지를 스트림 형태로 읽어들여 변환 옵션에 따라 리사이즈 작업을 비동기적으로 처리합니다. 리사이즈된 이미지는 새로운 키(경로)로 S3 버킷에 저장되며, 모든 작업이 완료되면 성공 상태를 반환하고 오류 발생 시 오류 상태를 반환합니다.
위의 프로젝트를 zip으로 압축하고 Lambda에 업로드합니다.
업로드 방법은 로컬에서 바로 업로드 하는 방법과 S3에 있는 파일을 업로드하는 방법이 있는데 저장소가 로컬이냐 클라우드냐 차이가 있을뿐이라서 손이 가는대로 하면 됩니다.
이미지 리사이징 작업은 매우 높은 CPU 자원을 요구하는 작업입니다. 따라서, AWS Lambda를 사용할 때 기본적으로 제공되는 메모리와 CPU 스펙을 상향 조정하는 것이 필요합니다. 이는 이미지 파일의 크기가 클 경우 리사이징 작업이 원활하게 실행되지 않을 가능성이 높기 때문입니다. 따라서, 이미지 리사이징 작업의 성능과 안정성을 보장하기 위해 Lambda의 스펙을 적절하게 조정하는 것이 필수적입니다.
마지막으로, AWS Lambda에 트리거를 추가하여 이미지 리사이징 작업을 완성합니다. S3 버킷에 파일이 업로드될 때, 특히 "raw" 폴더 안에 파일이 들어갈 때 Lambda 함수가 실행되도록 트리거를 설정해야 합니다. 이를 위해 접두사(prefix)를 반드시 지정해야 하며, 그렇지 않을 경우 무한 루프가 발생할 수 있습니다.
또한, Lambda 함수에서 수행된 작업의 로그는 CloudWatch Logs에서 모니터링할 수 있습니다. 이 로그를 통해 Lambda 함수의 실행 상태와 성능을 실시간으로 확인하고, 필요에 따라 디버깅할 수 있습니다.
원본 이미지 파일의 용량이 2.6MB였는데, 해당 라사이징 작업을 통해 최종적으로 4.5KB까지 용량을 줄이는 데 성공하였습니다. 이는 원본 파일에 비해 약 600배나 더 작아졌다는 것을 의미합니다. 이렇게 큰 폭의 압축률을 달성함으로써, 파일 크기를 획기적으로 줄일 수 있었습니다.
이미지들을 리사이징하기 전에는 모든 이미지가 로딩되는 데 약 10초가 소요되었습니다. 이는 페이지의 성능과 사용자 경험에 부정적인 영향을 미쳤습니다.
그러나 이미지 리사이징을 통해 최적화 작업을 수행한 후, 모든 이미지가 로딩되는 시간이 평균적으로 100밀리초 이내로 단축되었습니다.
리사이징된 이미지를 사용할 때, 사용자 관점에서 페이지를 스크롤하는 동안 중단 없이 자연스럽게 이미지를 감상할 수 있습니다. 이는 페이지의 성능뿐만 아니라 전반적인 사용자 경험을 크게 향상시킵니다. 최적화된 이미지는 빠른 로딩 속도로 인해 사용자 인터페이스가 부드럽고 원활하게 작동하며, 사용자가 페이지를 탐색하는 과정에서 시각적 콘텐츠를 매끄럽게 즐길 수 있도록 합니다.
이로써, 웹 페이지의 효율성과 사용 편의성이 대폭 증가하였으며, 사용자 만족도가 높아지는 긍정적인 효과를 가져왔습니다.