Inhu는 인하대 후문의 술집, 밥집, 카페 등 여러 장소를 소개하고 추천해주는 서비스입니다.
안녕하세요. Inhu 프로젝트의 백엔드 개발을 담당하고 있는 팀원입니다.
오늘은 AWS S3 이미지 업로드를 구현하는 과정에서 고민했던 점들에 대해 이야기하고자 합니다.
Inhu 서비스에서는 리뷰, 장소, 프로필 등 다양한 이미지 업로드가 필요했습니다.
이를 위해 AWS S3 버킷을 이용해 이미지를 저장하는 방식을 선택했습니다.
처음에는 multer-s3
라이브러리를 사용하여 다음과 같은 구조로 업로드를 구현했습니다.
클라이언트 -> 백엔드 서버 -> (multer-s3) -> S3 버킷에 저장
그러나 이 방식에는 몇 가지 단점이 있었습니다.
따라서 클라이언트가 직접 S3에 업로드하도록 Presigned URL
방식을 도입했습니다.
👉 Presigned URL
이란?
AWS S3에서 제공하는 일종의 임시 URL로, 서버가 S3에 직접 접근할 권한(버킷, 객체 경로, 만료 시간, 업로드 조건 등)을 미리 설정한 뒤 클라이언트에게 전달하는 방식입니다.
클라이언트는 이 URL을 사용해 인증 과정 없이 제한된 조건에서 S3에 바로 업로드할 수 있습니다.
Presigned URL을 발급받는 방식에는 크게 두 가지가 있습니다.
getSignedUrl
방식
장점: 간단하게 사용 가능
단점: Content-Type, 파일 용량 제한 등을 강제하기 어려움
createPresignedPost
방식
장점: 조건(확장자, Content-Type, 용량 등)을 세밀하게 제어 가능
단점: 설정이 조금 더 복잡함
우리 서비스에서는 보안과 안정성을 위해 createPresignedPost
방식을 선택했습니다.
이미지 1장 업로드의 경우
async getPresignedUrl({
folder,
extension,
maxSize,
contentType,
}: GetPresignedUrlInput): Promise<PresignedUrlModel> {
const key = `${folder}/${uuidv4()}.${extension}`;
const result = await createPresignedPost(this.s3Client, {
Bucket: this.bucketName,
Key: key,
Expires: 5 * 60, // presigned url 유효기간 (5분)
Conditions: [
['eq', '$acl', 'public-read'], // 업로드된 파일의 접근 권한을 public-read 로 고정
['content-length-range', 0, maxSize * 1024 * 1024], // 용량 제한 입력 (MB)
['starts-with', '$Content-Type', contentType], // Content-Type 제한
],
});
return {
...result,
fileHost: this.fileHost,
filePath: `/${key}`,
};
}
이미지 여러장 업로드의 경우
async getPresignedUrls({
folder,
extensions, // example : ["jpg", "png", "jpg"]
maxSize,
contentType,
}: GetPresignedUrlsInput): Promise<PresignedUrlModel[]> {
return await Promise.all(
extensions.map((extension) =>
this.getPresignedUrl({ folder, extension, maxSize, contentType }),
),
);
}
}
최종적으로 백엔드는 Presigned URL 발급만 담당하고, 프론트엔드는 이를 이용해 직접 S3에 이미지를 업로드하도록 구조를 변경했습니다.
이후에는 업로드된 객체의 filePath
만 DB에 저장하여, 추후 조회시 효율적으로 접근할 수 있도록 했습니다.
그 결과 서버 부하를 줄이고 업로드 성능을 개선할 수 있었으며, 확장성 있는 파일 업로드 구조를 갖추게 되었습니다.