복's

[ Nest JS ] File Upload 본문

교육/SW 정글 프로젝트(나만무)

[ Nest JS ] File Upload

나복이 2023. 11. 24. 07:18
728x90

이미지 파일들을 서버로 업로드할 일이 생겼다.

당장은 큰 용량의 이미지들이 올라가지 않기 때문에 S3 같은 스토리지 사용 대신에 서버 폴더에 업로드 하기로 했다.


[ 📌 목표 ]

  • 이미지 업로드를 한 곳에서만 하지 않기 때문에 공통적으로 사용할 수 있도록 모듈화 하기
  • 이미지 압축하기
  • 이미지 관련한 예외 케이스 처리하기
  • 파일 사이즈 제한하기

[ 📌 파일 업로드 하기 & 모듈화 ]

다행히도 좋은 자료가 많았지만 맨 처음으로 접한건 역시 Docs다.

기본적인 예시는 제공되기 때문에 먼저 파일을 전송 했을 때 파일이 받아지는지 확인 해본다.

$ npm i -D @types/multer

Multer를 사용하기 때문에 라이브러리 다운로드를 받고 시작한다.

  @UseInterceptors(FileInterceptor('file'))
  @Patch('/:todo_id/user/:user_id/image')
  updateImage(
    @Param('todo_id') todo_id: number,
    @Param('user_id') user_id: number,
    @UploadedFile() file: Express.Multer.File
  ){
    this.logger.debug("todo_id", todo_id);
    this.logger.debug("user_id", user_id);
    this.logger.debug(file);
    
    return this.groupService.updateImage(todo_id, user_id, file);
  }

Docs가 제공하는 기본적인 형태를 그대로 작성 해보았다.

과연 logger에서 찍히는 file은 Docs와 같은 결과가 나오나? (요청은 postman을 통해서 진행)

[ Reslt - console ]

설정을 전역적으로 등록해서 사용하는데에는 여러 방법이 있는데, MulterOptionsFactory 인터페이스를 구현한 클래스를 Config를 사용하는 하는 방법이다.

MulterModule.registerAsync({
  useClass: MulterConfigService,
});

Docs에서 제공하는 MulterModule이 요구하는 option object이다. 

import { HttpStatus, Injectable, Logger } from "@nestjs/common";
import { MulterOptionsFactory } from "@nestjs/platform-express";
import * as multer from 'multer';
import * as path from 'path';
import * as fs from 'fs';
import { DoWithException } from "src/do-with-exception/do-with-exception";

@Injectable()
export class MulterConfig implements MulterOptionsFactory{
  dir_path: string;

  constructor(){
    this.dir_path = process.env.IMAGE_PATH;
    this.mkdir();
  }

  mkdir(){
    try{
      fs.readdirSync(this.dir_path);
    } catch(err){
      fs.mkdirSync(this.dir_path);
    }
  }

  changePath(dir_path: string){
    this.dir_path = dir_path;
  }

  createMulterOptions(){
    const dir_path = this.dir_path;
    const option = {
      storage: multer.diskStorage({
        destination(req, file, done){

          done(null, dir_path);
        },
        filename(req, file, done){
          const allowed_exts = ['.jpg', '.jpeg', '.png', '.gif'];
          const ext = path.extname(file.originalname);
          const name = path.basename(file.originalname, ext);

          if(!allowed_exts.includes(ext.toLowerCase())) {
            done(new DoWithException('지원하지 않는 파일 확장자입니다.', '1000', HttpStatus.BAD_REQUEST), file.originalname);
          }

          done(null, `${name}_${Date.now()}${ext}`);
        }
      }),
      limits: { fileSize: 1 * 1024 * 1024 } // 1 MB
    };

    return option;
  }
}
  • 선언부: implements MulterOptionsFactory를 통해서 요구한 인터페이스 구현
  • 생성자: file upload path 설정
  • mkdir: 폴더가 없는 경우 폴더를 생성하는 코드인데 미리 만들고 안써도 상관은 없을 것 같다.
  • changePath: 모듈을 사용하는 코드가 여러개라서 경로를 다르게 하고 싶었다.
  • createMulterOptions: 옵션들
    • storage: file을 저장할 공간으로 메모리가 아닌 디스크 공간에 저장하도록 했다.
      • filename: file 이름을 설정하는 공간인데 나는 여기서 확장자에 따라서 예외를 처리했다. (본래라면 pipe를 사용해서 하는거 같은데.. 음..) -> 변경이 필요한 부분은 맞는거 같다. (pipe에서 validation이 이뤄지고 예외는 400 Bad Request로 던져주는 것 같다.)
    • limits: 파일 최대 크기를 설정했다. (1 MB)
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { MulterConfig } from './fileUpload/MulterConfigService';

@Module({
  imports: [
    MulterModule.registerAsync({
      useClass: MulterConfig
    })
  ],
  exports: [ MulterModule ]
})
export class UtilsModule {}

그리고 만든 옵션을 적용한 모듈을 등록하면 된다.


[ 📌 이미지  압축 ]

멘토링 받다가 이미지 압축 관련 라이브러리로 sharp를 듣게 되었다.

노드 진영에서 오랫동안 살아남은 성능이 좋은 라이브러리 보였다.

현재 프로젝트에서 3D 렌더링도 하고있기 때문에 다른 resource로 부터의 자원 소모를 줄이는데 도움이 될 것 같았고, 서버에 올리는 이미지 중에서 프로필 같이 확대도 안되고 딱히 큰 용도가 없는 이미지는 압축해도 괜찮을거 같다는 생각이 들었다.

$ npm install sharp

 

사실 제공하는 옵션은 많지만 나는 딱히 사용의 필요성은 못 느꼈고,

 try {
      const filePath = file.path;
      
      await sharp(filePath).resize({ width: 700, height: 700, fit: 'contain' }) // 원하는 크기로 조정
                           .toFile(filePath, async(err, info) => {
                             try{
                               await fs.unlink(filePath);
                             } catch(err){
                               // 원본 삭제 에러
                             }
                           });
    } catch(err) {
      // 리사이징 에러
    }

에러에 대해서 처리는 아직 하지 않았지만 만들어놓은 에러로 wrapping해서 던져야겠다라고 생각하고 주석만 처리해놨다.

크기는 660KB 파일 넣으면 34KB 까지 줄어 드는데 파일 용도에 따라서 얼만큼 줄일지는 잘 생각 해봐야겠다.


[ 📌 Reference ]

https://docs.nestjs.com/techniques/file-upload#file-validation

 

Documentation | NestJS - A progressive Node.js framework

Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, is built with TypeScript and combines elements of OOP (Object Oriented Programming), FP (Functional Programming), and FRP (Functional Rea

docs.nestjs.com

https://sharp.pixelplumbing.com/

 

sharp - High performance Node.js image processing

 

sharp.pixelplumbing.com

 

728x90