일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- It
- back-end
- Producer
- 개발자
- Nest.js
- 개발이 취미인 사람
- kafka
- java
- 코틀린
- react
- 조건문
- front-end
- component
- vue
- node.js
- javascript
- 자바
- spring boot
- Kotlin
- props
- state
- 상속
- swagger
- class
- restful api
- 반복문
- file upload
- AWS
- SWIFT
- Sequelize
- Today
- Total
개발이 취미인 사람
[Nest.js] Nest.js API 만들기 (11) - 파일 업로드(Multer) 본문
- 개요
안녕하세요. 이번 시간에는 파일 업로드를 진행해 보겠습니다.
Multer를 사용해서 파일 업로드를 진행해보겠습니다. Multer는 Node.js에서 대표적인 파일 업로드 라이브러리입니다.
Nest.js 공식 홈페이지에서도 Multer를 통해 업로드하는 방식을 소개하고 있습니다.
Nest.js 공식 홉페이지 : https://docs.nestjs.kr/techniques/file-upload
- 설정
첫 번째로 진행해야 하는 부분은 공식 홈페이지에서도 소개하고 있는 안전한 타입 설정을 위해 아래 패키지를 설치해야 합니다.
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 값을 받습니다.
- fieldName 필드 이름(field) : HTTP 요청 시 파일 field
- Options - MulterOptions 타입
다중
@Post('upload')
@UseInterceptors(FilesInterceptor('files'))
@Bind(UploadedFiles())
uploadFile(files) {
console.log(files);
}
다중 업로드 시에는 file -> files로 복수로 설정하면 됩니다.
그리고 FileInterceptor() 함수는 이번에는 세 개의 Argument 값을 받습니다.
- fieldName 필드 이름(field) : HTTP 요청 시 파일 field
- maxCount : 업로드 시 허용할 수 있는 최대 파일 수
- Options - MulterOptions 타입
상황에 맞게 우리는 원하는 업로드 방식을 선택해서 개발을 하면 될 것 같습니다.
- Options
기본적으로 업로드 옵션은 아래와 같습니다.
- storage : 저장 방식 설정 (디스크 & 메모리)
- fileFilter : 파일 유효성 체크
- 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 파일
소스 저장소
Github : https://github.com/Ryan-Sin/Node_Nest/tree/v10