본문 바로가기

🧑🏻‍💻 Dev/Nest JS

[NestJS] AWS S3, Multer로 이미지 업로드 구현하기

1. S3 버킷 생성


S3 서비스를 이용하려면 AWS 사이트에 들어가서 S3 버킷을 생성해야 합니다.

S3

AWS 사이트에 로그인한 후 위에 검색창에 S3라고 검색하면, S3를 시작할 수 있습니다.

버킷 만들기

노란색 버튼 "버킷 만들기"를 클릭해서 버킷 생성을 시작합니다.

버킷 이름, AWS 리전

버킷 이름을 먼저 작성해 줍니다. 버킷 이름에는 지켜야 하는 규칙이 있습니다. "버킷 이름 지정 규칙 보기"라는 파란색 링크를 클릭하면 확인해보실 수 있습니다. 규칙을 지켜서 내 버킷이름을 입력해 줍니다.

객체 소유권, 퍼블릭 엑세스 차단 설정

객체 소유권은 권장되는 방향인 "ACL 비활성화됨"을 선택했습니다. 저는 역할별로 IAM 사용자를 만들어서 사용할 예정입니다.

 

일단 저는 퍼블릭 액세스 차단 설정을 기본적으로 선택되어 있는 대로 하고 넘어갔습니다. 나중에 버킷에 가서 권한에 대한 설정은 변경할 수 있습니다. 저는 버킷에 대한 정책을 직접 작성해서 사용할 예정이지만, 여기서는 권장되는 방법으로 설정하고 넘어가겠습니다.

 

나머지 선택 사항들은 그대로 놔두고 버킷을 생성하면 버킷 생성이 완료됩니다.

버킷 폴더 만들기

생성된 버킷에 들어가 보면 처음에는 비어있는데, "폴더 만들기"를 눌러서 원하는 폴더명을 입력해서 만들어줍니다. 저는 "images"라는 이름의 폴더를 생성했습니다.

 

 

 

2. IAM 사용자 생성 & 정책 적용


S3라고 치고 들어갔던 검색창에 "IAM"이라고 검색하면 아래 화면으로 이동할 수 있습니다.

IAM 사용자 생성

버킷 생성을 끝냈으면 이제 IAM 사용자를 생성합니다. "사용자 생성" 버튼을 클릭합니다.

IAM 사용자 이름

IAM 사용자의 이름을 설정합니다. 이 이름은 나중에 AWS Console에 IAM 사용자로 로그인할 때 사용되는 정보입니다. 너무 어렵고 길게 만들면... 의미 있게 작성해 주시면 됩니다.

권한 설정, 정책 생성

다음으로 넘어가면 권한 옵션을 설정합니다. 저는 직접 정책을 생성해서 사용할 것이기 때문에 "직접 정책 연결"을 선택합니다.

IAM 사용자를 만든 후에 정책을 따로 만들어도 되지만, 저는 이 페이지에서 바로 사용할 정책을 생성해서 등록하겠습니다.

"정책 생성" 버튼을 클릭합니다.

정책 편집기

정책 생성으로 들어와서 JSON을 선택해 주면 정책을 작성할 수 있는 정책 편집기가 나옵니다.

여기에 이제 정책을 작성해 줍니다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowToAllBuckets",
            "Effect": "Allow",
            "Action": [
                "s3:GetBucketLocation",
                "s3:ListAllMyBuckets"
            ],
            "Resource": "*"
        },
        {
            "Sid": "AllowToAllObjects",
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::my-bucket-name"
        },
        {
            "Sid": "AllowToGetPutDeleteObjects",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:DeleteObject"
            ],
            "Resource": "arn:aws:s3:::my-bucket-name/*"
        }
    ]
}

Sid가 있는 객체 하나하나를 Statemen라고 합니다. Sid는 해당 Statement를 식별하는 id 값입니다. 여러 개의 Statement를 입력해서 목적에 맞게 정책을 여러 개 추가할 수 있습니다.

 

먼저 AllowToAllBuckets는 모든 버킷에 접근할 수 있도록 허용한 것이고, AllowToAllObjects는 객체 리스트에 접근할 수 있고, AllowToGetPutDeleteObjects는 객체 조회, 수정, 삭제에 대한 정책입니다.

 

Resource에는 해당 Statement가 적용되는 대상을 적어줘야 합니다. "*"는 모든 대상을 의미하고, "arn:aws:s3:::my-bucket-name"은 제가 이번 포스트에서 생성한 "my-bucket-name"이라는 이름을 갖는 버킷에 대해서만 적용되는 정책입니다.

 

이렇게 정책 생성을 완료하고, 다시 IAM 생성 과정으로 넘어가서 만들었던 정책을 검색해서 선택한 후 내가 만들고 있는 IAM 사용자에게 내가 만든 정책을 적용시킵니다.

IAM 사용자 검토, 정책 등록 확인

이제 다음으로 넘어가면 최종적으로 지금까지 만들었던 IAM 사용자의 내용을 검토할 수 있습니다. 여기서 내가 생성해서 적용해 준 정책이 정상적으로 등록되었는지 확인합니다. 검토가 완료되었다면 "사용자 생성"을 통해서 IAM 사용자 생성을 완료합니다.

 

 

 

3. Access Key 발급


이제 IAM 사용자를 이용해서 Nest 프로젝트에 적용하기 위해서 필요한 Access Key를 발급해야 합니다. 

IAM > 사용자 > 보안 자격 증명

이제 생성한 IAM 사용자를 누르고 들어가서 밑에 "보안 자격 증명"이라는 버튼을 클릭합니다. 그리고 쭉 밑으로 내려봅니다.

엑세스 키 만들기

밑에 내려보면 "액세스 키 만들기" 버튼이 있습니다. 눌러서 엑세스 키를 만들어줍니다.

사용 사례

사용하시는 목적에 맞게 선택해 주시면 됩니다. 저는 CLI로 선택을 하고 넘어가겠습니다.

설명 태그 설정

선택 사항이기 때문에 작성하든 말든 상관없습니다. 저는 간단하게 "aws-s3-access-key"라고 적은 후 "액세스 키 만들기" 버튼을 눌러 생성을 완료하겠습니다. 생성 완료 후 "Access key ID"와 "Secret access key"를 확인할 수 있는데, CSV 파일을 저장해서 보관하시기 바랍니다. 나중에 Nest 프로젝트에 연결하기 위해서 필요한 정보입니다. 해당 정보는 노출이 절대 절대 안 되게 잘 관리해 주시길 바랍니다. Github에도 올라가지 않도록요!

 

 

 

4. Nest 프로젝트에 적용


이제 Nest 프로젝트에 적용하기 위해서 필요한 라이브러리를 설치해 줍니다.

npm install multer-s3 @aws-sdk/client-s3

 

.env 파일에 S3 관련 설정 정보를 입력합니다. (dotenv가 설치되어있다는 가정하에 진행합니다.)

# AWS S3 Properties
AWS_S3_BUCKET_NAME=my-bucket-name
AWS_REGION=ap-northeast-2
AWS_ACCESS_KEY_ID={엑세스 토큰 발급 시 받은 ACCESS KEY ID)
AWS_SECRET_ACCESS_KEY={엑세스 토큰 발급 시 받은 SECRET ACCESS KEY}

위에서 AWS_ACCESS_KEY_ID와 AWS_SECRET_ACCESS_KEY의 이름반드시 동일하게 작성해 주세요.

 

이제 Nest 프로젝트에서 S3의 이미지 저장 로직을 처리할 코드를 작성합니다. 저는 해당 이미지 저장 로직을 여러 곳에서 사용할 것이기 때문에 core 패키지의 utils에 만들도록 하겠습니다.

import { BadRequestException, Injectable } from "@nestjs/common";
import * as AWS from "aws-sdk";
import { PromiseResult } from 'aws-sdk/lib/request';
import * as path from "path";

@Injectable()
export class S3Service {
  private readonly s3: AWS.S3;
  private readonly MAXIMUM_IMAGE_SIZE: number;
  private readonly ACCEPTABLE_MIME_TYPES: string[];
  public readonly S3_BUCKET_NAME: string;

  constructor() {
    this.s3 = new AWS.S3({
      region: process.env.AWS_REGION
    })
    this.MAXIMUM_IMAGE_SIZE = 3000000; // 이미지 용량 3MB 제한
    this.ACCEPTABLE_MIME_TYPES = ['image/jpg', 'image/png', 'image/jpeg']; // 이미지 확장자 제한
    this.S3_BUCKET_NAME = process.env.AWS_S3_BUCKET_NAME
  }

  async uploadToS3(file: Express.Multer.File): Promise<S3UploadResponse> {
    try {
      if (!this.ACCEPTABLE_MIME_TYPES.includes(file.mimetype)) {
        throw new BadRequestException('이미지 파일 확장자는 jpg, png, jpeg만 가능합니다.');
      }
      if (file.size > this.MAXIMUM_IMAGE_SIZE) {
        throw new BadRequestException('업로드 가능한 이미지 최대 용량은 3MB입니다.');
      }

      const key = `images/${Date.now()}_${path.basename(file.originalname,)}`
        .replace(/ /g, ''); // 공백을 제거하기 위한 regex입니다.

      const s3Object = await this.s3
        .putObject({
          Bucket: this.S3_BUCKET_NAME,
          Key: key,
          Body: file.buffer,
          ContentType: file.mimetype
        })
        .promise();

      const imgUrl = `https://${this.S3_BUCKET_NAME}.s3.amazonaws.com/${key}`;

      return {
        key: key,
        s3Object: s3Object,
        contentType: file.mimetype,
        url: imgUrl
      }
      
    } catch (error) {
      throw error;
    }
  }
}

export type S3UploadResponse = {
  key: string;
  s3Object: PromiseResult<AWS.S3.PutObjectOutput, AWS.AWSError>;
  contentType: string;
  url: string;
}

S3에 이미지를 업로드하는데 필요한 정보들을 S3Service에서 처리하고, 이미지를 업로드하는 메서드인 uploadToS3를 구현했습니다.

업로드 가능한 이미지의 최대 용량은 3MB로 제한을 두고, 이미지 확장자는 png, jpg, jpge로 제한을 두었습니다.

 

위에서 보면 AWS_ACCESS_KEY_ID와 AWS_SECRET_ACCESS_KEY에 대한 설정을 해주지 않는 것을 볼 수 있습니다. 실제로 accessKeyId라고 코드를 쳐보면 deprecated 되었다고 합니다. env 파일에 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY라는 이름으로 만들어 두면, S3에 요청할 때 알아서 확인을 한다고 합니다. (싱기방기)

 

import { Module } from '@nestjs/common';
import { S3Service } from './utils/s3.service';

@Module({
  providers: [S3Service],
  exports: [S3Service]
})
export class CoreModule {}

이제 S3Service를 CoreModule의 providers와 exports에 등록을 하고, 필요한 곳에서 CoreModule을 import 받아 사용할 수 있습니다. CoreModule을 @Global()로 만들어도 되지만, 일단은 직접 import로 주입하는 방법을 이용했습니다.

 

 

🔗 참고


https://blog.donggeun.co.kr/view/641046b677b9e17d235fa923#google_vignette

 

https://blog.donggeun.co.kr/view/641046b677b9e17d235fa923#google_vignette

 

blog.donggeun.co.kr

https://velog.io/@wndbsgkr/AWS-S3%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%9C-%EC%A0%95%EC%B1%85%EA%B3%BC-%EC%97%AD%ED%95%A0-%EC%84%A4%EC%A0%95%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90#4-iam-%EC%A0%95%EC%B1%85-%EC%83%9D%EC%84%B1

 

AWS S3를 사용하기 위한 정책과 역할 설정을 해보자!

S3는 무엇인가요? S3는 확장성, 강력한 일관성, 보안 및 성능을 제공하는 객체 스토리지 서비스이다. S3는 데이터를 버킷이라고 불리는 객채에 대한 컨테이너 안에 객체로 저장하는데 여기서 객체

velog.io