본문 바로가기
개발/NestJS

NestJS에서 AWS S3 파일 업로드 깔끔하게 구현하기 🚀

by coking 2024. 11. 15.

안녕하세요! 오늘은 NestJS에서 AWS S3 파일 업로드를 구현하는 방법을 공유해보려고 해요.
저는 우아한형제들의 nestjs-library-config를 사용해서 config 설정을 관리합니다 관련해서 다음에 정리하도록 할게요!

기존 방식의 문제점 🤔

보통 NestJS에서 S3 업로드를 구현할 때 multer-s3를 많이 사용하는데요, 이 방식에는 몇 가지 단점이 있습니다:

  1. S3Client 인스턴스를 여러 번 생성하게 됨
  2. 코드가 복잡해짐
  3. 테스트하기 어려움

개선된 방식 소개 ✨

1. 필요한 패키지 설치

npm install @nestjs/platform-express @aws-sdk/client-s3

2. Module 설정

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { S3Client } from '@aws-sdk/client-s3';
import { FileConfigService } from './file-config.service';
import { FileService } from './file.service';
import { FileController } from './file.controller';

@Module({
  imports: [ConfigModule.forFeature(FileConfigService)],
  providers: [
    FileService,
    {
      provide: 'S3_CLIENT',
      useFactory: (fileConfigService: FileConfigService) => {
        return new S3Client({
          credentials: {
            accessKeyId: fileConfigService.accessKey,
            secretAccessKey: fileConfigService.secretAccessKey,
          },
          region: fileConfigService.region,
        });
      },
      inject: [FileConfigService],
    },
  ],
  controllers: [FileController],
  exports: ['S3_CLIENT', FileService],
})
export class FileModule {}

3. Controller 작성

@Controller('files')
@UseGuards(JwtAuthGuard)
export class FileController {
  constructor(private readonly fileService: FileService) {}

  @Post('upload')
  @UseInterceptors(FilesInterceptor('files', 5))  // 최대 5개 파일
  async uploadFiles(
    @CurrentUser() user: TokenPayload,
    @UploadedFiles(
      new ParseFilePipe({
        validators: [
          new FileTypeValidator({ fileType: /(jpg|jpeg|png|gif)$/ }),
          new MaxFileSizeValidator({ maxSize: 5 * 1024 * 1024 }), // 5MB
        ],
      }),
    ) files: Express.Multer.File[]
  ) {
    return this.fileService.uploadFiles(files, user.sub);
  }

  @Get('list')
  listFiles(@CurrentUser() user: TokenPayload) {
    return this.fileService.listFiles(user.sub);
  }

  @Delete(':key')
  deleteFile(
    @CurrentUser() user: TokenPayload,
    @Param('key') key: string
  ) {
    return this.fileService.deleteFile(user.sub, key);
  }
}

4. Service 구현

@Injectable()
export class FileService {
  constructor(
    @Inject('S3_CLIENT') private readonly s3: S3Client,
    private readonly fileConfig: FileConfig,
  ) {}

  /**
   * 다중 파일 업로드 처리
   * Promise.all을 사용하여 여러 파일을 병렬로 업로드
   */
  async uploadFiles(files: Express.Multer.File[], userId: string) {
    const uploadedFiles = await Promise.all(
      files.map(async (file) => {
        const ext = file.originalname.split('.').pop();
        // 파일명 충돌 방지를 위해 타임스탬프와 랜덤값 추가
        const key = `uploads/${userId}/${Date.now()}-${Math.random().toString(36).substring(7)}.${ext}`;

        // S3에 파일 업로드
        await this.s3.send(
          new PutObjectCommand({
            Bucket: this.fileConfig.bucket,
            Key: key,
            Body: file.buffer,
            ContentType: file.mimetype,
          }),
        );

        return {
          originalname: file.originalname,
          key,
          size: file.size,
          type: file.mimetype,
        };
      }),
    );

    return {
      success: true,
      files: uploadedFiles,
    };
  }
}

이렇게 구현하면 좋은 점 🌈

  1. 성능 최적화

    • S3Client 인스턴스 재사용
    • 병렬 업로드로 처리 속도 향상
  2. 코드 품질

    • 책임 분리가 명확함 (컨트롤러는 검증, 서비스는 업로드 처리)
    • 테스트하기 쉬움
    • 코드가 간결해짐
  3. 확장성

    • 다른 스토리지로 변경하기 쉬운 구조
    • 추가 기능 구현이 용이

마무리 🎉

이렇게 해서 NestJS에서 S3 파일 업로드를 깔끔하게 구현하는 방법을 알아보았습니다.
기존 multer-s3 방식보다 더 깔끔하고 유지보수하기 좋은 코드가 되었고 s3 접근하는 방식도 명확해 진 거 같아서 만족스럽습니다.

혹시 궁금한 점이나 개선할 부분이 있다면 댓글로 알려주세요~ 😊

참고 자료

댓글