[AWS] S3 이미지 업로드 (+Swagger 적용)

Jiwon Youn·2021년 2월 19일

Amazon S3

아마존에서 제공하는 Simple Storage Service
확장성, 데이터 가용성 및 보안과 성능을 제공하는 객체 저장소 서비스

NestJS 프레임워크 환경에서 Swagger를 통해 S3 Bucket에 이미지를 업로드

app.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { UploadModule } from './upload/upload.module';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      envFilePath: '.env.dev',
      ignoreEnvFile: process.env.NODE_ENV === 'prod',
    }),
    UploadModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

먼저 AWS 업로드 권한을 위해 환경 변수를 설정해야 하므로 app 모듈에 ConfigModule.forRoot()를 적용한다.

  • isGlobal : 다른 모듈에서 특별히 import하지 않아도 사용 가능하게 전역 모듈로 설정
  • envFilePath : 환경 변수 설정 파일 경로
  • ignoreEnvFile : 환경 변수 파일(.env.dev) 로딩을 비활성화할 때 설정

AWS의 Access Key ID와 Secret access key 값은 권한 문제로 절대 유출되면 안되므로 공개적인 클래스에 선언하지 않는다. 때문에 외부 파일에서 (환경 변수 파일 .env.dev) 가져와 사용할 수 있도록 Config를 설정한다. 권한 key 값이 포함된 환경 변수 파일은 특히 gitHub같은 오픈 소스 사이트에 업로드하지 않도록 주의한다.


upload.module.ts

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { UploadController } from './upload.controller';
import { UploadService } from './upload.service';

@Module({
  imports: [ConfigModule],
  providers: [UploadService],
  controllers: [UploadController],
})
export class UploadModule {}

ConfigModuleisGlobaltrue이므로 imports: [ConfigModule]을 굳이 설정해 줄 필요 없음


upload.controller.ts

import {
  Controller,
  Post,
  UploadedFile,
  UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiBody, ApiConsumes, ApiProperty } from '@nestjs/swagger';
import { UploadService } from './upload.service';

export class FileUploadDto {
  @ApiProperty({ type: 'file' })
  file: any;
}

@ApiBody에 지정할 타입에 @ApiProperty 속성을 부여해 file 지정

@Controller('upload')
export class UploadController {
  constructor(private readonly uploadService: UploadService) {}

  @Post('file')
  @UseInterceptors(FileInterceptor('file'))
  @ApiConsumes('multipart/form-data')
  @ApiBody({
    description: 'image upload',
    type: FileUploadDto,
  })
  uploadFile(@UploadedFile() file) {
    console.log(file);
    return this.uploadService.uploadFile(file);
  }
}
  • @ApiConsumes : swagger에서 input file 추가
  • @ApiBody : swagger에서 file을 받아올 type(DTO) 지정
  • @UseInterceptors : input한 file을 인터셉트 (UploadedFile에 넘겨주는?)
  • @UploadedFile : 업로드한 파일을 객체로 받아옴 (type: object)

console.log(file)을 통해 넘어온 file의 buffer값과 이외 정보들을 확인할 수 있다.


upload.service.ts

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import * as AWS from 'aws-sdk';

@Injectable()
export class UploadService {
  constructor(private readonly configService: ConfigService) {}

  ACCESS_KEY = this.configService.get('ACCESS_KEY_ID');
  SECRET_KEY = this.configService.get('SEC_ACCESS_KEY');
  BUCKET_NAME = this.configService.get('BUCKET_NAME');

npm i aws-sdk 라이브러리 설치 후 import * as AWS from 'aws-sdk'
환경 변수 파일에서 AWS 권한 key 값을 끌어와 선언

  s3 = new AWS.S3({
    accessKeyId: this.ACCESS_KEY,
    secretAccessKey: this.SECRET_KEY,
  });

사용할 스토리지의 accessKeyIdsecretAccessKey로 권한 인증

     this.s3.createBucket(
       {
         Bucket: this.BUCKET_NAME,
         CreateBucketConfiguration: {
           LocationConstraint: 'ap-northeast-2',
         },
       },
       function (err, data) {
         if (err) {
           console.log(err);
          } else {
           console.log('Bucket Created Successfully.', data);
         }
       },
     );

S3에 저장하기 위해선 Bucket이 필요한데 보통 이미 업무에 존재하는 Bucket을 사용하고 직접 Bucket을 Create 해야 하는 상황은 드물다. (하지만 예제 업로드를 위해선 Create 해야 한다.)
→ Bucket name을 설정하고 생성할 region을 설정

    public async uploadFile(file) {
    
    // timestamp for image unique name
    const date = new Date().getTime();
    console.log(date);

    const param = {
      Bucket: this.BUCKET_NAME,
      Key: `image/${date + file.originalname}`,
      Body: file.buffer,
      ACL: 'public-read', // bucket에 조회 public 권한
      ContentType: file.mimetype,
    };
    
     try {
       const stored = await this.s3.upload(param).promise();
       console.log('Image Saved Successfully.');
       return stored;
     } catch (err) {
       console.log(err);
       }
     }
    }
  • Bucket : 저장할 Bucket의 이름 (생성된 Bucket Name)
  • Key : S3에 저장될 파일의 이름 지정. Date().getTime()을 이용해서 S3에 업로드 할 이미지에 고유한 이름 지정
  • Body : Controller에서 넘겨준 file을 그대로 전달하기 위해서 file.buffer 사용
  • ACL : Bucket에 저장된 파일을 권한 없이 조회 가능하게 public-read 선언
  • ContentType : 저장될 타입 MIME 설정. file의 본래 확장자로 저장하기 위해 file.mimetype 사용

이후 s3.upload(param)으로 파일을 업로드한다. 이 때, 비동기 처리를 위해 .promise()를 사용한다. (AWS에서 promise 지원)

    const stored = () => {
      return new Promise((resolve, reject) => {
        this.s3.upload(param, (err, data) => {
          if (err) {
            console.log('rejected');
            reject(err);
          } else {
            console.log('resolved');
            resolve(data);
          }
        });
      });
    };
    return await stored();

.promise()를 사용하지 않고 원시적으로 비동기 처리하는 방법
이 방법을 사용하진 않으나 에러 핸들링 과정을 이해하기 위해 시도해본다.
💡 비동기 함수는 Promise를 return하도록 작성해야 한다.


swagger에서 file을 넘겨주면 위와 같이 S3에 저장된다. ResponseBody엔 s3.upload.promise() 객체를 리턴해 저장된 파일의 URL Location과 저장된 파일의 이름 key값이 조회된다.

0개의 댓글