NestJS: AWS S3 이미지 업로드 구현

2024. 2. 21. 13:18NestJS

최종프로젝트 진행에 있어서 QNA, FAQ, 공지사항 CRUD를 구현함에 있어서 이미지를 첨부 기능을 구현하기에 앞서 현재 프로젝트에서 구현되어져 있는 S3 이미지 첨부방식을 대해서 알아보고자 이 글을 작성하게 되었다.

controller.ts

import {
    Controller,
    Post,
    UploadedFile,
    UseGuards,
    UseInterceptors,
} from "@nestjs/common";
import { AwsService } from "./aws.service";
import { ApiBearerAuth } from "@nestjs/swagger";
import { accessTokenGuard } from "../auth/guard/access-token.guard";
import { FileInterceptor } from "@nestjs/platform-express";

@Controller("aws")
@ApiBearerAuth("accessToken")
@UseGuards(accessTokenGuard)
export class AwsController {
    constructor(private readonly awsService: AwsService) {}

    @Post()
    @UseInterceptors(FileInterceptor("file"))
    uploadFile(@UploadedFile() file: Express.Multer.File) {
        return this.awsService.fileupload(file);
    }
}

API메서드 형식인 Post방식을 기반으로 구현되어져 있음을 확인해볼 수 있다.

그리고  @UseInterceptors(FileInterceptor("file"))은 @nestjs/platform-express 에서 제공하는 Fileinterceptor를 사용하여 파일 업로드를 처리하는데 "file"이라는 이름의 매개변수로 업로드된 파일을 사용할 것임을 알 수 있습니다.

여기서 이  uploadFile(@UploadedFile() file: Express.Multer.File) 메서드는 파일 업로드를 처리하는데 @UploadedFile() 데코레이터는 메서드의 매개변수 "file"에 사용됩니다. 이것은 업로드된 파일에 대한 정보를 가지고 있는 객체를 나타냅니다. "Express.Multer.File" 타입은 Multer 미들웨어에 의해 처리된 파일에 대한 타입입니다.

따라서, uploadFile 메서드 내에서는 file 매게변수를 통해 업로드된 파일의 속성에 접근할 수 있습니다. 이 객체는 업로드된 파일의 원본 이름, 버퍼(파일 내용), mimetype 등과 같은 정보를 포함하고 있습니다. 이를 활용하여 서비스로 전달하여 S3버킷에 파일을 업로드하는 등의 작업을 수행할 수 있습니다.

Service.ts

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


@Injectable()
export class AwsService {
    constructor(private readonly configService: ConfigService) {}
    async fileupload(file: Express.Multer.File) {
        const s3Client = new S3Client({
            region: this.configService.get<string>("AWS_REGION"),
            credentials: {
                accessKeyId:
                    this.configService.get<string>("AWS_ACCESS_KEY_ID"),
                secretAccessKey: this.configService.get<string>(
                    "AWS_SECRET_ACCESS_KEY",
                ),
            },
        });
        const objectKey = `${Date.now()}-${file.originalname}`;
        const upload = await s3Client.send(
            new PutObjectCommand({
                Bucket: this.configService.get<string>("S3BUCKET"),
                Key: objectKey,
                Body: file.buffer,
                ACL: "public-read",
                ContentType: file.mimetype,
            }),
        );
        console.log(upload);
        const objectUrl = `https://${this.configService.get<string>(
            "S3BUCKET",
        )}.s3.amazonaws.com/${objectKey}`;
        return objectUrl;
    }
}

큰 특징으로는 생성자에서 주입된 ConfigService를 사용하여 구성값을 가져오고 있으며

async fileupload(file:Express.Multer.File)이 메서드는 Express Multer 파일을 매개변수로 받아와서 해당 파일을 AWS S3버킷에 업로드 합니다.

AWS SDK for JavaScript v3("@aws-sdk/client-s4")를 사용하여 새 S3Client를 생성합니다.

AWS자격 증명 및 지역 정보는 주입된 "ConfigService에서 가져오게 됩니다.

그리고 const objectKey = `${Date.now()}-${file.originalname}`; 에서 타임스탬프와 파일의 원래 이름을 기반으로 고유한 객체 키를 생성합니다.

PutObjectCommand를 사용하여  파일을 지정된 S3 버킷에 업로드하는데 Public-read ACL(액세스 제어 목록)을 사용하여 파일을 공개적으로 접근 가능하게 합니다.

결과적으로 S3 업로드의 결과를 로그에 남기고 업로드된 객체의 URL을 구성하게 되며 업로드된 객체의 URL을 반환하여 업로드가 성공적으로 이루어졌음을 나타낸다.

CRUD에 적용해보기

import {
    Controller,
    Post,
    Body,
    Param,
    UseGuards,
    UseInterceptors,
    UploadedFile,
    HttpStatus,
} from "@nestjs/common";
import { QnaService } from "./qna.service";
import { CreateQnaDto } from "./dto/createQna.dto";
import { UpdateQnaDto } from "./dto/updateQna.dto";
import { ApiBearerAuth, ApiTags } from "@nestjs/swagger";
import { accessTokenGuard } from "src/auth/guard/access-token.guard";
import { FileInterceptor } from "@nestjs/platform-express";
import { UserId } from "src/auth/decorators/userId.decorator";

@ApiTags("QNA")
@Controller("qna")
@ApiBearerAuth("accessToken")
export class QnaController {
    constructor(private readonly qnaService: QnaService) {}

    @UseGuards(accessTokenGuard)
    @UseInterceptors(FileInterceptor("file"))
    @Post()
    async createQna(
        @UserId() userId: number,
        @Body() createQnaDto: CreateQnaDto,
        @UploadedFile() file: Express.Multer.File,
    ) {
        const data = await this.qnaService.createQna(
            userId,
            createQnaDto,
            file,
        );

        return {
            statusCode: HttpStatus.CREATED,
            message: "QNA가 생성되었습니다.",
            data,
        };
    }

    @UseGuards(accessTokenGuard)
    @UseInterceptors(FileInterceptor("file"))
    @Put(":qnaId")
    async updateQna(
        @UserId() userId: number,
        @Param("qnaId") qnaId: number,
        @UploadedFile() file: Express.Multer.File,
        @Body() updateQnaDto: UpdateQnaDto,
    ) {
        const data = await this.qnaService.updateQna(
            userId,
            qnaId,
            file,
            updateQnaDto,
        );

        return {
            statusCode: HttpStatus.OK,
            message: "QNA가 수정되었습니다.",
            data,
        };
    }
}

이 CRUD의 controller에서는 앞서 파일 업로드의 구현에 사용되었던 @nestjs/platform-express 에서 제공하는 Fileinterceptor를 사용하여 파일 업로드를 처리를 동일하게 사용하는 것을 알 수 있다.

 uploadFile(@UploadedFile() file: Express.Multer.File) 메서드를 동일하게 사용하였으며 "Express.Multer.File" 타입은 Multer 미들웨어에 의해 처리된 파일에 대한 타입으로 사용됩니다.

import {
    BadRequestException,
    Injectable,
    NotFoundException,
    UnauthorizedException,
} from "@nestjs/common";
import { CreateQnaDto } from "./dto/createQna.dto";
import { UpdateQnaDto } from "./dto/updateQna.dto";
import { InjectRepository } from "@nestjs/typeorm";
import { User } from "src/entity/user.entity";
import { Repository } from "typeorm";
import { Qna } from "src/entity/qna.entity";
import { AwsService } from "src/aws/aws.service";

@Injectable()
export class QnaService {
    constructor(
        @InjectRepository(User)
        private readonly userRepository: Repository<User>,
        @InjectRepository(Qna)
        private readonly qnaReporitory: Repository<Qna>,
        private readonly awsService: AwsService,
    ) {}

    async createQna(
        userId: number,
        createQnaDto: CreateQnaDto,
        file: Express.Multer.File,
    ) {
        const { ...restOfQna } = createQnaDto;
        const user = await this.veryfiyUser(userId);

        if (user.userType === "user" && file) {
            const Qna = await this.qnaReporitory.save({
                userId: user.id,
                userName: user.name,
                image: await this.awsService.fileupload(file),
                ...restOfQna,
            });

            return Qna;
        } else if (user.userType === "user" && !file) {
            const Qna = await this.qnaReporitory.save({
                userId: user.id,
                userName: user.name,
                ...restOfQna,
            });

            return Qna;
        } else {
            throw new BadRequestException("QNA는 사용자만 작성할 수 있습니다.");
        }
    }
    
    async updateQna(
        userId: number,
        qnaId: number,
        file: Express.Multer.File,
        updateQnaDto: UpdateQnaDto,
    ) {
        const { ...restOfQna } = updateQnaDto;
        await this.veryfiyQna(qnaId);

        const user = await this.veryfiyUser(userId);
        if (user.userType === "user" && file) {
            const qna = await this.qnaReporitory.save({
                id: qnaId,
                image: await this.awsService.fileupload(file),
                ...restOfQna,
            });
            return qna;
        } else if (user.userType === "user" && !file) {
            const qna = await this.qnaReporitory.save({
                id: qnaId,
                ...restOfQna,
            });
            return qna;
        }
    }

    private async veryfiyUser(userId: number) {
        const masterUser = await this.userRepository.findOne({
            where: { id: userId },
        });

        if (!masterUser) {
            throw new NotFoundException("해당 유저는 존재하지 않습니다.");
        }

        return masterUser;
    }

    private async veryfiyQna(qnaId: number) {
        const qna = await this.qnaReporitory.findOne({
            where: { id: qnaId },
            relations: { qnaComment: true },
        });

        if (!qna) {
            throw new NotFoundException("QNA가 존재하지 않습니다.");
        }
        return qna;
    }
}

controller에서 넘겨받은 file의 매개변수 같은 경우 메서드는 Express Multer 파일을 매개변수로 받아와서 해당 파일을 AWS S3버킷에 업로드하고 있다.

qnaRepository에 저장할 때에는 생성자함수에 주입된 file로 된 매개변수를 awsService를 사용하여 AWS S3에 이미지를 업로드하고, 업로드된 이미지의 URL을 QNA 엔티티에 저장하고 있다.

 

앞으로 개인 프로젝트를 진행할 생각이다. 개인프로젝트를 진행함에 있어서 내가 필요할 것 같은 기술에 대한 고민과 미리 준비하는 습관을 기르는 것은 중요하다고 생각한다. 개인 프로젝트를 진행함에 있어서 이미지 업로드에 필요한 기술 습득은 중요한 부분이라고 생각한다.