Presigned Url로 효율적인 S3 파일 업로드 구현하기

dvnchi·2025년 8월 16일
7

Inhu

목록 보기
2/4
post-thumbnail

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을 발급받는 방식에는 크게 두 가지가 있습니다.

  1. getSignedUrl 방식
    장점: 간단하게 사용 가능
    단점: Content-Type, 파일 용량 제한 등을 강제하기 어려움

  2. 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에 저장하여, 추후 조회시 효율적으로 접근할 수 있도록 했습니다.
그 결과 서버 부하를 줄이고 업로드 성능을 개선할 수 있었으며, 확장성 있는 파일 업로드 구조를 갖추게 되었습니다.


참고
AWS POST Policy

0개의 댓글