nestJS프로젝트에서 이미지를 업로드하는 방법에 대해서 공식홈페이지에서는 내장된 multer라이브러리를 사용하여, @UseInterceptor데코레이터와 @UploadedFile데코레이터를 사용하여 단일 이미지를 올리고 꺼내 사용할 수 있다고 설명해 주고 있다.
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log(file);
}
이때 파일에 대한 유효성 검사또한 @UploadedFile데코레이터에서 진행할 수 있는 코드도 제공하고 있다.
@UploadedFile(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 1000 }),
new FileTypeValidator({ fileType: 'image/jpeg' }),
],
}),
)
file: Express.Multer.File,
필자는 기존에 기술과제를 바탕으로 연습용 프로젝트를 만들어서 숙소 플래폼의 데이터를 생성할 때 같이 이미지도 넣어주는 방식을 만들려고 했다. 기존에 주어진 숙소의 JSON파일을 데이터는 대략적으로 아래와 같았다.
[
{
"id": 1,
"name": "산포리 펜션",
"description": "입실/퇴실 시간\n ㅁ 입실시간 : 오후 3시 ~ 오후 10시\n ㅁ 퇴실시간 : 익일 오전 11시 까지\n ㅁ 오후 10시 이후의 입실은 미리 연락부탁드립니다.",
"address": "경상북도 울진군 근남면 세포2길 1-21",
"university": "울진대학교",
"houseType": "펜션",
"pricePerDay": 30000,
"images": [
{
"url": "http://si.wsj.net/public/resources/images/OB-YO176_hodcol_H_20130815124744.jpg",
"key": 1
},
{
"url": "https://image.pensionlife.co.kr/penimg/pen_1/pen_19/1977/9734f7418fcc01a2321ba800b1f2c7ee.jpg",
"key": 2
}
]
},
...
]
images에는 url과 이미지 개수에 따른 key를 포함한 배열형태의 데이터를 가지고 있었다. 여러개의 이미지를 포함할 수 있기 때문에 여러 이미지를 받을 수 있어야했다. 이 또한 공식 홈페이지에서 파일배열에 대한 코드가 존재했다.
@Post('upload')
@UseInterceptors(FilesInterceptor('files'))
uploadFile(@UploadedFiles() files: Array<Express.Multer.File>) {
console.log(files);
}
위 단일 데이터와 다른점은 인터셉터의 이름이 복수로 FilesInterceptor라는 점과 @UploadedFiles데코레이터의 복수, files의 타입이 Array<Express.Multer.File>라는 점에 차이가 존재 했다.
🙄어쨋든 만들기에 앞서, 이렇게 이미지를 업로드 하게 되면, 어디에 파일이 저장되는거지? 라는 의문점이 들었다.
일반적으로 위와 같은 코드만 사용하게 된다면, 파일은 메모리스토리지에 저장된다. 메모리스토리지에 저장되는 경우, 서버를 재시작하거나, 서버가 다운된다면 파일이 사라져서 재시작전 업로드되었던 이미지가 보이지 않게되는 현상이 발생할 것이다.
이러한 부분을 해결하기 위해서는 서버 파일 내의 디렉토리 폴더에 업로드된 파일을 저장하는 방식인 디스크 스토리지를 이용하는 방식과, AWS의 S3와 같은 클라우드 스토리지를 이용하는 방식이 존재 한다.
하지만 둘 다 각각의 단점이 존재한다. 디스크 스토리지는 파일이 계속 업로드 됨에 따라, 서버폴더 자체의 용량이 계속적으로 늘어나는 점이 있고, 클라우드 스토리지 같은경우 이용하는 용량에 다라서 요금이 부과된다.
필자는 AWS의 S3를 이용해서 클라우드 스토리지에 저장하는 방식을 택했다.
@UseInterceptor데코레이터의 FilesInterceptor에 step-in해보면 이 함수는 fieldName, maxCount, localOptions를 인자로 받는다.
export declare function FilesInterceptor(fieldName: string, maxCount?: number, localOptions?: MulterOptions): Type<NestInterceptor>
여기서 localOptions을 따로 설정해 주어야 디스크 스토리지 or 클라우드 스토리지를 사용할 수 있다.
따로 muterOption파일을 만들어서 옵션을 정의하고 꺼내준다.
const fileFilter = (req, file, callback) => {
//mimetype는 images/png , images/jpeg, video/mp4 등의 형태 /를 기준으로 나눠주면 파일의 형식이 맞는지 알 수 있다.
const fileTypes = file.mimetype.split('/')[0];
if (fileTypes === 'image') callback(null, true);
else callback(new BadRequestException('이미지 형식 아님'), false);
};
export const multerOptions = multer({
storage: multerS3({
s3,
bucket,
acl: 'public-read',
key(req, file, callback) {
callback(null, `nest-project/${Date.now() + file.originalname}`);
},
}),
fileFilter: fileFilter,
limits: { fileSize: 10 * 1024 * 1024, files: 10 },
});
필자는 multer-s3라이브러리를 함께 사용하여 위와같이 옵션을 설정하고, 아래와 같이 컨트롤러를 작성해 파일들을 업로드하고 사용하였다.
@Post('/')
@UseInterceptors(FilesInterceptor('images', 10, multerOptions as any))
@UsePipes(ValidationPipe)
async createHouse(
@Body() createHouseDto: CreateHouseDto,
@UploadedFiles() files: Array<Express.MulterS3.File>,
) {
await this.housesService.createHouse(createHouseDto, files);
}
기존의 nestJS제공 코드와 다른점은 아무래도 files의 타입이 Array<Express.MulterS3.File>타입 이라는 것이다.
file내부의 location을 사용하고 싶지만, Multer의 File인터페이스에는 location이 존재하지 않고, 이를 상속받아 multer-s3에서 만든 MulterS3.File인터페이스에는 s3에 올릴때 추가적으로 제공되는 정보들을 포함하고 있었기 때문이다.
위와같이 files를 받아서 콘솔에 찍어보면 하나의 파일마다 굉장히 많은 데이터를 받은 배열로 나오게된다.
하지만 내게 필요한 images의 배열은 url과 key만 존재하기 때문에 이 배열을 map을 돌려서 새로운 배열로 만들어주고, 이 배열을 JSON타입으로 MySQL에 저장한다.
const images = files.map((file, index) => {
const url = file.location;
const key = index + 1;
return { url, key };
});
const house = await this.houseRepository.create({
...createHouseDto,
images,
});
const result = await queryRunner.manager.save(house);
위와 같이 작성한 api를 실제 실행해 보고 확인해보면 정상 저장되는 것을 확인할 수 있다.