안녕하세요, 503입니다.
최근 NestJS로 숙소 예약 서비스를 개발하는 팀프로젝트를 진행했었습니다.
바로 이전에 진행했던 프로젝트에서 AWS S3를 사용해봤던이 필자가 그 기능을 맡아 구현하기로 했습니다. 사실 그 전까지는 Express 환경에서 단일 파일 업로드 기능을 구현해 본 경험이 없었는데, 문제는 그것만이 아니었습니다.
바로 "시간"이 엄청나게 부족했던 겁니다.
발표와 마감일은 당장 코앞이었고 급하기 기능이라도 돌아가게 해야하는 상황이었던 필자는... 결국 엉망진창으로 코드를 짰던 기억이 납니다..
그렇게 얼레벌레 발표를 마치고나서.. 아쉬운 부분이 많았던 팀원들과 함께 리팩토링 기간을 가졌는데요.
이왕 하는거 단일 파일 업로드 -> 다중파일 업로드로 개선하고, layer도 좀 더 세분화해서 코드를 다시 짜기로 했습니다.
그러면서 공부했던 내용을 간단하게 정리해 보도록 하겠습니다.
NestJS에서 파일 업로드를 처리하기 위해서 multer 미들웨어를 사용합니다.
Multer는 주로 파일 업로드에 사용되는 multipart/form-data
를 처리하기 위한 미들웨어입니다.
인터셉터는 파일과 필드 형태에 따라 FileFieldsInterceptor, FilesInteceptor, FileInterceptor 가 있습니다.
단일 파일을 업로드하고 싶을 때는 FileInterceptor()
를 핸들러에 연결한 다음 @UploadedFile()
데코레이터를 사용하여 request에서 file을 가져옵니다.
@Post('uploads')
@UseInterceptors(FileInterceptor('file')) // 인자로 filedName
async uploadFile(@UploadedFile() file) {
console.log(file);
}
파일 배열(Array of files)을 업로드하기 위해서는 FilesInterceptor()
를 사용합니다. 이 인터셉터는 fieldName
, maxCount
(동시 업로드 가능한 최대 파일 수), 선택적 MulterOptions
객체를 인자로 가집니다. request 객체에서 파일을 선택하기 위해 @UploadedFile()
데코레이터를 사용합니다.
@Post('uploads')
@UseInterceptors(FilesInterceptor('file', 10))
async uploadFiles(@UploadedFiles() files) {
console.log(file);
}
여러 필드(multiple files, 모두 다른 키)를 업로드하기 위해서는 FileFieldsInterceptor()
데코레이터를 사용합니다.
@Post('uploads')
@UseInterceptors(FileFieldsInterceptor([
{ name: 'avatar', maxCount: 1 },
{ name: 'background', maxCount: 1 },
]))
uploadFile(@UploadedFiles() files) {
console.log(files);
}
파일인터셉터에 storage를 설정해줄 수 있는데, 이 부분을 multerS3나 distStorage를 넣어주면 해당 경로에 데이터를 저장할 수 있습니다.
물론 필자는 service 단에서 처리했기 때문에 아래처럼 하지는 않았습니다.
@UseInterceptors(
FilesInterceptor('file', 10, {
storage: multerS3({
s3: new AWS.S3(),
bucket: `${process.env.AWS_BUCKET_NAME}`,
key: function (req, file, cb) {
cb(null, `${Date.now() + file.originalname}`);
},
}),
}),
)
async uploadFile(@UploadedFiles() files) {
// 생략
}
가장 먼저 NestCLI 명령어를 통해서 자동으로 모듈, 컨트롤러, 서비스 파일을 만듭니다.
$ nest generate module uploads
$ nest generate controller uploads --no-spec
$ nest generate service uploads --no-spec
// --no-spec : 테스트를 위한 소스 코드를 생성하지 않는 옵션
NestJS에선 명령어를 사용하면 알아서 모듈에 서비스나 컨트롤러가 등록되니까 편한 것 같습니다.
다시봐도 정말 급하게 구현한게 보이는데요... 진짜로 layer을 나눌 시간조차 없었기에 controller 파일에 코드를 다 때려 박았습니다.
아무튼, 클라이언트에서 form-data를 이용해 파일을 업로드해주면 S3 버킷에 저장된 주소값을 반환해주려고 합니다. 반환하는 파일 주소값은 imgurl 변수에 있는데 필자는 cloudfront로 CDN을 구축했었기 때문에 cloudFront의 배포 도메인주소 뒤에 파일 이름을 붙여 반환해줬습니다. AWS S3, CloudFront를 이용한 CDN 구축하기는 시간이 된다면 나중에 정리해서 올려보겠습니다^^
import {
Controller,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { Express } from 'express';
import * as AWS from 'aws-sdk';
@Controller('uploads')
export class UploadsController {
@Post('')
@UseInterceptors(FileInterceptor('file'))
async uploadFile(@UploadedFile() file: Express.Multer.File) {
AWS.config.update({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY,
secretAccessKey: process.env.AWS_SECRET_KEY,
},
});
try {
const key = `${Date.now() + file.originalname}`;
// AWS 객체 생성
const upload = await new AWS.S3()
.putObject({
Key: key,
Body: file.buffer,
Bucket: process.env.AWS_BUCKET_NAME,
})
.promise();
const imgurl = process.env.AWS_CLOUDFRONT + key;
return Object.assign({
statusCode: 201,
message: `이미지 등록 성공`,
data: { url: imgurl },
});
} catch (error) {
console.log(error);
}
}
}
변경된 내용을 정리하면 아래와 같습니다
FileInterceptor()
와 @UploadedFile()
데코레이터를 FilesInterceptor()
와 @UploadedFiles()
로 수정import { Injectable } from '@nestjs/common';
import * as AWS from 'aws-sdk';
@Injectable()
export class UploadsService {
private readonly s3;
constructor() {
AWS.config.update({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY,
secretAccessKey: process.env.AWS_SECRET_KEY,
},
});
this.s3 = new AWS.S3();
}
async uploadImage(file: Express.Multer.File) {
const key = `${Date.now() + file.originalname}`;
const params = {
Bucket: process.env.AWS_BUCKET_NAME,
ACL: 'private',
Key: key,
Body: file.buffer,
};
return new Promise((resolve, reject) => {
this.s3.putObject(params, (err, data) => {
if (err) reject(err);
resolve(key);
});
});
}
}
import {
Controller,
Post,
UploadedFiles,
UseInterceptors,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
import { UploadsService } from './uploads.service';
@Controller('uploads')
export class UploadsController {
constructor(private readonly uploadsService: UploadsService) {}
@Post('')
@UseInterceptors(FilesInterceptor('files', 10)) // 10은 최대파일개수
async uploadFile(@UploadedFiles() files) {
console.log(files);
const imgurl: string[] = [];
await Promise.all(
files.map(async (file: Express.Multer.File) => {
const key = await this.uploadsService.uploadImage(file);
imgurl.push(process.env.AWS_CLOUDFRONT + key);
}),
);
return {
statusCode: 201,
message: `이미지 등록 성공`,
data: imgurl,
};
}
}
폼 데이터로 key는 files, value에는 파일을 줘야합니다.
data에 업로드한 파일의 url을 배열로 반환해줬습니다.
{
"statusCode": 201,
"message": "이미지 등록 성공",
"data": [ "cloudfront주소/user-info.gif", "주소/order.gif", "주소/stay.gif" ]
}
개발하면서 필자가 만났었던 오류들인데요.. 덕분에 많이 헤맸던 기억에 눈물이 나서 정리해둡니다.
multer-s3
와 aws-sdk
의 버전이 틀리면 안됩니다.
Multer S3 2.x는 AWS SDK 2.x와 호환되고, Multer S3 3.x는 AWS SDK 3.x와 호환된다는 사실...
따라서 AWS SDK를 업그레이드하거나 Multer S3를 다운그레이드 해야합니다.
npm install --save -g multer-s3@version
예기치 않은 필드 오류. file의 key이름을 잘못 지정했을 때 뜨는 오류입니다.
name 속성이 있는 type= 파일이 전달된 매개변수 이름과 같아야 합니다.
좋은 글 감사합니다 :)