백앤드(Back-End)/Nest.js

[Nest.js] Nest.js API 만들기 (11) - 파일 업로드(Multer)

RyanSin 2021. 12. 1. 23:37
반응형

- 개요

안녕하세요. 이번 시간에는 파일 업로드를 진행해 보겠습니다.

 

Multer를 사용해서 파일 업로드를 진행해보겠습니다. Multer는 Node.js에서 대표적인 파일 업로드 라이브러리입니다.

 

Nest.js 공식 홈페이지에서도 Multer를 통해 업로드하는 방식을 소개하고 있습니다.

 

Nest.js 공식 홉페이지 : https://docs.nestjs.kr/techniques/file-upload

 

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 Progamming), FP (Functional Programming), and FRP (Functional Reac

docs.nestjs.com

 

- 설정 

첫 번째로 진행해야 하는 부분은 공식 홈페이지에서도 소개하고 있는 안전한 타입 설정을 위해 아래 패키지를 설치해야 합니다.

multer는 기본 적으로 Nest.js 프레임워크에 내장되어 있습니다. (@nestjs/platform-express 패키지 안에 multer 패키지가 있다.)

 

하지만 개발환경에서 조금 더 안전하게 개발하기 위해 아래 패키지를 선치해야 합니다.

 

#npm
npm i -D @types/multer

#yran
yarn add @types/multer --dev

 

- 단일 & 멀티 업로드 방식

Multer는 단일 다중 특정 개수를 지정한 업로드가 가능합니다.

 

단일
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
@Bind(UploadedFile())
uploadFile(file) {
  console.log(file);
}

 

컨트롤러 핸들러에 @UseInterceptors(FileInterceptor('file'))을 설정하면 첫 번째 file 변수에 파일 정보를 받아 옵니다.

 

또한, FileInterceptor() 함수는 두 개의 Argument 값을 받습니다.

 

  1. fieldName 필드 이름(field) : HTTP 요청 시 파일 field
  2. Options -  MulterOptions 타입

 

다중
@Post('upload')
@UseInterceptors(FilesInterceptor('files'))
@Bind(UploadedFiles())
uploadFile(files) {
  console.log(files);
}

 

다중 업로드 시에는 file -> files로 복수로 설정하면 됩니다.

 

그리고  FileInterceptor() 함수는 이번에는 세 개의 Argument 값을 받습니다.

 

  1. fieldName 필드 이름(field) : HTTP 요청 시 파일 field
  2. maxCount : 업로드 시 허용할 수 있는 최대 파일 수 
  3. Options -  MulterOptions 타입

상황에 맞게 우리는 원하는 업로드 방식을 선택해서 개발을 하면 될 것 같습니다.

 

- Options

기본적으로 업로드 옵션은 아래와 같습니다.

  1. storage : 저장 방식 설정 (디스크 & 메모리)
  2. fileFilter : 파일 유효성 체크
  3. limits : 업로드 시 제한 설정

 

저는 기본적으로 디스크 방식과 메모리 방식에 두 가지 업로드에 대해 설명하겠습니다.

 

@Controller

  /**
   * @author Ryan
   * @description 디스크 방식 파일 업로드 (1)-> Destination 옵션 설정
   *
   * @param {File[]} files 다중 파일
   * @param res Response 객체
   */
  @Post('/disk_upload1')
  @UseInterceptors(FilesInterceptor('files', null, multerDiskOptions))
  @Bind(UploadedFiles())
  uploadFileDisk(files: File[], @Res() res: Response) {
    res.status(HttpStatus.OK).json({
      success: true,
      data: this.userService.uploadFileDisk(files),
    });
  }

  /**
   * @author Ryan
   * @description 디스크 방식 파일 업로드 (2)-> Destination 옵션 미설정
   *
   * @param {File[]} files 다중 파일
   * @param  user_id 유저 아이디
   * @param res Response 객체
   */
  @Post('/disk_upload2')
  @UseInterceptors(
    FilesInterceptor('files', null, multerDiskDestinationOutOptions),
  )
  @Bind(UploadedFiles())
  uploadFileDiskDestination(
    files: File[],
    @Body('user_id') user_id: string,
    @Res() res: Response,
  ) {
    if (user_id != undefined) {
      userId = user_id;
    }

    res.status(HttpStatus.OK).json({
      success: true,
      data: this.userService.uploadFileDiskDestination(userId, files),
    });
  }

  /**
   * @author Ryan
   * @description 메모리 방식 파일 업로드
   *
   * @param {File[]} files 다중 파일
   * @param  user_id 유저 아이디
   * @param res Response 객체
   */
  @Post('/memory_upload')
  @UseInterceptors(FilesInterceptor('files', null, multerMemoryOptions))
  @Bind(UploadedFiles())
  uploadFileMemory(
    files: File[],
    @Body('user_id') user_id: string,
    @Res() res: Response,
  ) {
    if (user_id != undefined) {
      userId = user_id;
    }
    res.status(HttpStatus.OK).json({
      success: true,
      data: this.userService.uploadFileMemory(userId, files),
    });
  }

 

두 방식 모두 소스코드는 다르지 않습니다. 하지만 다른 하나는 세 번째 인자 값인 옵션 값입니다.

 

옵션 설정

multer.options.ts
import { HttpException, HttpStatus } from '@nestjs/common';
import { existsSync, mkdirSync } from 'fs';
import { diskStorage, memoryStorage } from 'multer';
import { extname } from 'path';

export const multerDiskOptions = {
  /**
   * @author Ryan
   * @description 클라이언트로 부터 전송 받은 파일 정보를 필터링 한다
   *
   * @param request Request 객체
   * @param file 파일 정보
   * @param callback 성공 및 실패 콜백함수
   */
  fileFilter: (request, file, callback) => {
    if (file.mimetype.match(/\/(jpg|jpeg|png)$/)) {
      // 이미지 형식은 jpg, jpeg, png만 허용합니다.
      callback(null, true);
    } else {
      callback(
        new HttpException(
          {
            message: 1,
            error: '지원하지 않는 이미지 형식입니다.',
          },
          HttpStatus.BAD_REQUEST,
        ),
        false,
      );
    }
  },
  /**
   * @description Disk 저장 방식 사용
   *
   * destination 옵션을 설정 하지 않으면 운영체제 시스템 임시 파일을 저정하는 기본 디렉토리를 사용합니다.
   * filename 옵션은 폴더안에 저장되는 파일 이름을 결정합니다. (디렉토리를 생성하지 않으면 에러가 발생!! )
   */
  storage: diskStorage({
    destination: (request, file, callback) => {
      const uploadPath = 'uploads';
      if (!existsSync(uploadPath)) {
        // uploads 폴더가 존재하지 않을시, 생성합니다.
        mkdirSync(uploadPath);
      }
      callback(null, uploadPath);
    },
    filename: (request, file, callback) => {
      //파일 이름 설정
      callback(null, `${Date.now()}${extname(file.originalname)}`);
    },
  }),
  limits: {
    fieldNameSize: 200, // 필드명 사이즈 최대값 (기본값 100bytes)
    filedSize: 1024 * 1024, // 필드 사이즈 값 설정 (기본값 1MB)
    fields: 2, // 파일 형식이 아닌 필드의 최대 개수 (기본 값 무제한)
    fileSize: 16777216, //multipart 형식 폼에서 최대 파일 사이즈(bytes) "16MB 설정" (기본 값 무제한)
    files: 10, //multipart 형식 폼에서 파일 필드 최대 개수 (기본 값 무제한)
  },
};

export const multerDiskDestinationOutOptions = {
  /**
   * @author Ryan
   * @description 클라이언트로 부터 전송 받은 파일 정보를 필터링 한다
   *
   * @param request Request 객체
   * @param file 파일 정보
   * @param callback 성공 및 실패 콜백함수
   */
  fileFilter: (request, file, callback) => {
    if (file.mimetype.match(/\/(jpg|jpeg|png)$/)) {
      // 이미지 형식은 jpg, jpeg, png만 허용합니다.
      callback(null, true);
    } else {
      callback(
        new HttpException(
          {
            message: 1,
            error: '지원하지 않는 이미지 형식입니다.',
          },
          HttpStatus.BAD_REQUEST,
        ),
        false,
      );
    }
  },
  /**
   * @description Disk 저장 방식 사용
   *
   * destination 옵션을 설정 하지 않으면 운영체제 시스템 임시 파일을 저정하는 기본 디렉토리를 사용합니다.
   * filename 옵션은 폴더안에 저장되는 파일 이름을 결정합니다. (디렉토리를 생성하지 않으면 에러가 발생!! )
   */
  storage: diskStorage({
    filename: (request, file, callback) => {
      //파일 이름 설정
      callback(null, `${Date.now()}${extname(file.originalname)}`);
    },
  }),
  limits: {
    fieldNameSize: 200, // 필드명 사이즈 최대값 (기본값 100bytes)
    filedSize: 1024 * 1024, // 필드 사이즈 값 설정 (기본값 1MB)
    fields: 2, // 파일 형식이 아닌 필드의 최대 개수 (기본 값 무제한)
    fileSize: 16777216, //multipart 형식 폼에서 최대 파일 사이즈(bytes) "16MB 설정" (기본 값 무제한)
    files: 10, //multipart 형식 폼에서 파일 필드 최대 개수 (기본 값 무제한)
  },
};

export const multerMemoryOptions = {
  /**
   * @author Ryan
   * @description 클라이언트로 부터 전송 받은 파일 정보를 필터링 한다
   *
   * @param request Request 객체
   * @param file 파일 정보
   * @param callback 성공 및 실패 콜백함수
   */
  fileFilter: (request, file, callback) => {
    console.log('multerMemoryOptions : fileFilter');
    if (file.mimetype.match(/\/(jpg|jpeg|png)$/)) {
      // 이미지 형식은 jpg, jpeg, png만 허용합니다.
      callback(null, true);
    } else {
      callback(
        new HttpException(
          {
            message: 1,
            error: '지원하지 않는 이미지 형식입니다.',
          },
          HttpStatus.BAD_REQUEST,
        ),
        false,
      );
    }
  },
  /**
   * @description Memory 저장 방식 사용
   */
  stroage: memoryStorage(),
  limits: {
    fieldNameSize: 200, // 필드명 사이즈 최대값 (기본값 100bytes)
    filedSize: 1024 * 1024, // 필드 사이즈 값 설정 (기본값 1MB)
    fields: 2, // 파일 형식이 아닌 필드의 최대 개수 (기본 값 무제한)
    fileSize: 16777216, //multipart 형식 폼에서 최대 파일 사이즈(bytes) "16MB 설정" (기본 값 무제한)
    files: 10, //multipart 형식 폼에서 파일 필드 최대 개수 (기본 값 무제한)
  },
};

/**
 * @author Ryan
 * @description 파일 업로드 경로
 * @param file 파일 정보
 *
 * @returns {String} 파일 업로드 경로
 */
export const uploadFileURL = (fileName): string =>
  `http://localhost:3000/${fileName}`;

위 소스 코드에서 중요한 부분은 storage:diskStoreage를 사용할 때입니다.

 

destination 옵션을 설정하지 않으면 운영체제 시스템 임시 파일을 저장하는 기본 디렉터리를 생성합니다.

 

만약 바로 업로드를 하고 싶다면, destination 옵션을 위와 같이 설정해서 사용하는 걸 추천드리겠습니다.

 

그렇지 않다면 옵션을 설정하지 않고, Controller를 통해 전송받은 File 정보를 통해 원하는 위치에 업로드하는 방법으로 진행하는 걸 추천드리겠습니다.

 

저는 두 가지 방식을 모두 설정해서 업로드 방식을 설정했습니다.

 

@Service

  /**
   * @author Ryan
   * @description 디스크 방식 파일 업로드 (1)
   *
   * @param files 파일 데이터
   * @returns {String[]} 파일 경로
   */
  uploadFileDisk(files: File[]): string[] {
    return files.map((file: any) => {
      //파일 이름 반환
      return uploadFileURL(file.filename);
    });
  }

  /**
   * @author Ryan
   * @description 디스크 방식 파일 업로드 (2)
   *
   * @param user_id 유저 아이디
   * @param files 파일 데이터
   * @returns {String[]} 파일 경로
   */
  uploadFileDiskDestination(user_id: string, files: File[]): string[] {
    //유저별 폴더 생성
    const uploadFilePath = `uploads/${user_id}`;

    if (!fs.existsSync(uploadFilePath)) {
      // uploads 폴더가 존재하지 않을시, 생성합니다.
      fs.mkdirSync(uploadFilePath);
    }
    return files.map((file: any) => {
      //파일 이름
      const fileName = Date.now() + extname(file.originalname);
      //파일 업로드 경로
      const uploadPath =
        __dirname + `/../../${uploadFilePath + '/' + fileName}`;

      //파일 생성
      fs.writeFileSync(uploadPath, file.path); // file.path 임시 파일 저장소

      return uploadFileURL(uploadFilePath + '/' + fileName);
    });
  }

  /**
   * @author Ryan
   * @description 메모리 방식 파일 업로드
   *
   * @param user_id 유저 아이디
   * @param files 파일 데이터
   * @returns {String[]} 파일 경로
   */
  uploadFileMemory(user_id: string, files: File[]): any {
    //유저별 폴더 생성
    const uploadFilePath = `uploads/${user_id}`;

    if (!fs.existsSync(uploadFilePath)) {
      // uploads 폴더가 존재하지 않을시, 생성합니다.
      fs.mkdirSync(uploadFilePath);
    }

    return files.map((file: any) => {
      //파일 이름
      const fileName = Date.now() + extname(file.originalname);
      //파일 업로드 경로
      const uploadPath =
        __dirname + `/../../${uploadFilePath + '/' + fileName}`;

      //파일 생성
      fs.writeFileSync(uploadPath, file.buffer);

      //업로드 경로 반환
      return uploadFileURL(uploadFilePath + '/' + fileName);
    });
  }

 

DB에 저장하는 부분을 제외한 소스코드를 제공하겠습니다. 헷갈리는 부분이 있다면 댓글을 남겨주시면 친절하게 답변해드리겠습니다.

 

 

Postman 파일

Nest-js.json
0.01MB

 

소스 저장소

Github : https://github.com/Ryan-Sin/Node_Nest/tree/v10

 

GitHub - Ryan-Sin/Node_Nest

Contribute to Ryan-Sin/Node_Nest development by creating an account on GitHub.

github.com