댕댕워크 서비스에는 '산책 중 사진 찍기' 기능이 있어 이미지 서버 구축이 필요합니다.
이미지 저장용 서버를 따로 두고, 해당 서버에 접근하는 URL만 저장하는 형식으로 구현하려고 합니다.
세 가지가 주요 포인트입니다.
1, 2번은 서버와의 API 통신을 통해 처리해야 합니다.
서버에서 이미지를 처리하는 과정에서 CPU 리소스를 많이 소모하게 됩니다.
유저가 적을 때는 괜찮겠지만, 늘어날수록 서버에 부하가 커지게 됩니다.
클라이언트는 서버에게 이미지 업로드가 필요함을 알립니다. 파일 이름과 파일 타입을 명시해 API 요청을 날립니다. 이 때 인증 / 인가가 자연스럽게 수행됩니다.
서버는 S3에 특정 파일 이름 / 타입의 이미지를 업로드 할 수 있는 특수한 URL인 Presigned URL을 요청합니다.
S3가 presigned URL을 전송하면, 서버가 이를 클라이언트에 다시 전송합니다.
presigned URL을 통해 클라이언트에서 직접 S3에 이미지를 업로드합니다. 이미지 처리 과정에서 생기는 CPU 부하를 클라이언트에서 처리하도록 합니다.
성공시 서버에 성공함을 알립니다. 서버는 새로운 이미지가 저장된 URL을 DB에 저장합니다.
S3에 작업 명령을 내릴 수 있는 URL입니다.
원래 Key를 통해 인증된 사용자만 S3와 통신할 수 있지만, 이 URL은 인증되지 않은 사용자도 사용할 수 있습니다.
대신 URL을 발급받을 때 인증 절차를 거칩니다.
또 특정한 리소스에 대해서만 접근할 수 있게 제한이 걸려있으며, URL을 사용할 수 있는 시간 제한도 존재합니다.
export class S3Service {
private readonly s3Client;
constructor(
private readonly configService: ConfigService,
private readonly logger: WinstonLoggerService,
) {
this.s3Client = new S3Client({ region: this.configService.getOrThrow('AWS_S3_REGION') });
}
async createPresignedUrlWithClientForPut(userId: number, type: FileType[]): Promise<PresignedUrlInfo[]> {
const filenameArray = this.makeFileName(userId, type);
const presignedUrlInfoPromises = filenameArray.map(async (curFileName) => {
const command = new PutObjectCommand({
Bucket: BUCKET_NAME,
ContentType: `image/${type}`,
Key: curFileName,
}); //커맨드 생성
const url = await getSignedUrl(this.s3Client, command, { expiresIn: 3600 }); //presigned url 요청
return { filename: curFileName, url };
});
return Promise.all(presignedUrlInfoPromises);
}
PutObjectCommand
함수에 버킷 이름, 이미지 타입, 파일 이름을 인자로 넣어 이미지 생성 커맨드를 만듭니다.
getSignedUrl
함수를 호출해 S3에 presigned URL 발급 요청을 보냅니다.
파일 이름 = s3에 이미지를 저장할 경로인데,
나중에 특정 유저가 회원 탈퇴를 하거나 해서 정보를 지워야 할 때를 고려하면 유저별로 저장하는 게 좋기 때문에
경로를 {userId}/{random file name(UUID)}.{fileType}
으로 만들었습니다.
makeFileName(userId: number, type: string): string {
return `${userId}/${generateUuid()}.${type}`;
}
이후 프론트엔드에서 S3에 사진을 업로드하고, 산책일지 API의 산책 사진 필드에 업로드가 성공한 URL을 전송하면 서버는 이 URL만 저장해둡니다.
이 때, URL 전체가 아닌 도메인 부분을 제외한 문자열만 보냅니다:
https://mybucket.s3.ap-northeast-2.amazonaws.com/ + {파일경로}
https://mybucket.s3.ap-northeast-2.amazonaws.com/
를 제외한 뒷부분(파일 경로)만 DB에 저장합니다.reference: 유데미 Node JS: Advanced Concepts 강의