포시코딩

[Nest.js] 원하는 시점에 AWS S3에 파일 저장 본문

Node.js

[Nest.js] 원하는 시점에 AWS S3에 파일 저장

포시 2023. 2. 23. 01:03
728x90

개요

https://devkkiri.com/post/96bdd7e2-3328-4450-8e54-332cd90d4066

 

NestJS 파일 업로드하기(2) | Kkiri Blog

지난 포스팅에 이어서 이번에도 NestJS에서 파...

devkkiri.com

위 블로그를 참고하여 기존의 서버 내부에 저장되던 이미지 저장 기능을 

S3에 저장되게 변경하긴 했으나

 

service에 들어오기도 전에 S3에 이미지가 저장되고 있었고

만약 게시글 저장이 실패한다면 저장되지 말아야 하는 파일이 그대로 AWS S3에 남아있는 문제가 있었다.

 

이 상황에 대해 같은 팀원이 원하는 결과를 얻어 

해당 코드를 정리해보고자 한다.

https://muja-coder.tistory.com/88

 

20230222 TIL - nestjs AWS S3 file upload

Today I learned 이번 작업은 내가 원하는 위치에서 AWS S3에 file을 업로드 하는 것이 목표이다 작업을 진행하기 전에 사용할 패키지를 다운받자 npm install aws-sdk 지금 테스트는 aws-sdk만 이용해서 작업

muja-coder.tistory.com

 

설치

npm i @nestjs/config @aws-sdk/client-s3 multer-s3
npm i -D @types/multer-s3 @types/multer

이중에 필요 없는 것도 있는 거 같은데 npm prune 해봐도 사라지는 게 없어서.. 일단 다 설치 ㄱㄱ

 

코드

common/utils/multer.options.factory.ts

import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
import { diskStorage } from 'multer';
import { extname } from 'path';

export const multerOptionsFactory = (): MulterOptions => {
  return {
    storage: diskStorage({
      filename(req, file, cb) {
        const imageExt = extname(file.originalname);
        cb(null, `${Date.now()}${imageExt}`);
      },
    }),
  };
};

제일 먼저 multer를 세팅해 준다. 

filename의 cb에서 두 번째 인자로 전달되는 값이 나중에 DB에 넣을, S3에 저장할 파일명이 되니

변경하고 싶으면 이 부분을 변경하면 된다.

* 참고로 나는 한글 파일명 깨지는 문제를 회피하기 위해 그냥 현재 시간으로 파일명을 지어줬다.

 

s3.service.ts

import { ConfigService } from '@nestjs/config';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { Injectable } from '@nestjs/common';
import { createReadStream } from 'fs';

@Injectable()
export class S3Service {
  private readonly client: S3Client;
  private readonly region: string = this.configService.get<string>('AWS_BUCKET_REGION');
  private readonly bucket: string = this.configService.get<string>('AWS_BUCKET_NAME');

  constructor(private readonly configService: ConfigService) {
    this.client = new S3Client({
      region: this.region,
      credentials: {
        accessKeyId: this.configService.get<string>('AWS_ACCESS_KEY_ID'),
        secretAccessKey: this.configService.get<string>('AWS_SECRET_ACCESS_KEY'),
      },
    });
  }

  async putObject(file) {
    const { path, filename } = file;
    await this.client.send(
      new PutObjectCommand({
        Bucket: this.bucket,
        Key: filename,
        Body: createReadStream(path),
      }),
    );
    // return `https://${this.bucket}.s3.${this.region}.amazonaws.com/${filename}`;
  }
}

해당 클래스를 통해 S3에 파일을 저장할 수 있다. 

위의 multer에서 설정한 filename이

putObject에서 전달받은 file 내부의 filename으로 꺼내져 사용되는 걸 확인할 수 있다.

 

.env

AWS_BUCKET_REGION=
AWS_BUCKET_NAME=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=

package.json과 같은 위치에 생성하면 되며

AWS에서 발급받은 값들을 맞는 위치에 넣어주면 된다.

 

boards.module.ts

@Module({
  imports: [
    TypeOrmModule.forFeature([Board]),
    MulterModule.registerAsync({
      imports: [ConfigModule],
      useFactory: multerOptionsFactory,
      inject: [ConfigService],
    }),
  ],
  controllers: [BoardsController],
  providers: [BoardsService, S3Service]
})
export class BoardsModule {}

기본적인 Boards 관련 Module에

MulterModule과 S3Service Privoder가 추가된 모습

 

boards.controller.ts

@Post()
@UseInterceptors(FileInterceptor('file'))
createBoard(
  @Body() boardData: CreateBoardDto,
  @UploadedFile() file: Express.Multer.File,
) {
  return this.boardsService.createBoard(boardData, file);
}

FormData로 전달받기 때문에 게시글의 title, content 등이 담긴 boardData와

이미지 파일이 있는 file을 같이 받는 모습이다. 

 

관련 자세한 내용은 [링크] 참고

file에는 해당 이미지 파일의 데이터와 multer에서 세팅한 filename이 담겨있다.

 

boards.service.ts

@Injectable()
export class BoardsService {
  constructor(
    @InjectRepository(Board) private boardRepository: Repository<Board>,
    private readonly s3Service: S3Service
  ) {}

  async createBoard(boardData: CreateBoardDto, file: Express.Multer.File) {
    if (!file) {
      throw new BadRequestException('파일이 존재하지 않습니다.');
    }
    
    // 게시글 저장
    boardData.imagePath = file.filename;
    this.boardRepository.insert(boardData);

    // S3 이미지 저장
    await this.s3Service.putObject(file);
  }
}

boardRepository.insert()를 통해 파일명이 담긴 게시글의 저장까지 완료되어야 

비로소 S3로 저장이 된다. 

 

만약 중간에 에러가 난다면 S3에 저장이 되지 않는다.

이전에는 미들웨어에서 이미 이미지 파일을 저장하고 service에 들어와

만약 service 로직이 실패한다면 S3에 저장한 이미지를 다시 지워야 해서

저장, 삭제 두 번의 AWS 요청이 있어야 됐는데 

 

위 로직을 통해 실패 시 단 한 번의 요청 없이 처리할 수 있게 되었다.

 

정리

99%는 위에서 소개한 팀원이 만든 로직이다.

검색해서 나온 블로그의 도움으로 

  • 서버 내부에 저장할 때 원하는 시점에 저장하기
  • S3에 저장할 때 Interceptor를 통해 저장하며 service 진입하기

두 단계까진 혼자 스스로 진행했었으나

 

Interceptor의 기능을 원하는 시점으로 돌릴 방법이 도저히 떠오르지 않는 상태에서

그냥 service를 만들어 쓰는 아주 간단한 방법으로 해결을 하신걸 보고 감탄하지 않을 수 없었다.

그저 respect..

아직 이해가 가지 않는 코드들도 있어서

나중에 좀 더 Nest.js에 익숙해지면 다시 돌아와 어떤 원리로 위 로직이 가능했고

개선할 사항이 있진 않은지 체크해 보는 시간을 가져야 할 것 같다.

728x90