하려는 프로젝트 중 이용자가 파일을 업로드할 수 있는 환경을 만들기가 있다.
작성한 채보 파일과 음악 파일을 사람들과 공유할 수 있도록 하는 것.
이를 구현하기 위해 파일 업로드를 참고하였다.
yarn이든 npm이든 @types/multer를 Dev mode로 설치해주자.
npm i -D @types/multer
yarn add @types/multer --dev
이제 Express.Multer.File 유형을 사용할 수 있다!
해당 유형은 nest에서 파일 정보를 임시 저장하는 인터페이스이다.
FileInterceptor()은 2가지 필드를 가진다.
MulterOptions. Optional이라 써도 되고 안써도 된다.FilesInterceptor()은 3가지 필드를 가진다.MulterOptions. Optional이라 써도 되고 안써도 된다.Get() 과 res.download(filepath, file show name, err => {})을 사용하면 끝!FileInterceptor()과 @UploadFile()을 이용한다.
주의!
FileInterceptor() may not be compatible with third party cloud providers like Google Firebase or others., 즉 FIleInterceptor()는 Firebase나 다른 서드파티에 적용할 수 없을 가능성이 있다고 한다.
파일 업로드는 vscode의 Tunder Client 익스텐션을 이용해서 실습하였다.
nest g mo files
nest g co files
를 이용하여 module과 controller를 생성해주자.
이때 module에 controller를 import를 해주어야한다! ← 이렇게 안하면 메인 모듈이 경로를 못찾는다.
생성한 controller에 아래를 추가해주자. 이 예제는 단일 파일 업로드를 지원한다.
@Post("upload-single")
@UseInterceptors(FileInterceptor('file')) //
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log(file);
}
그리고 파일을 업로드 하면...

?
??
??????????
?????????????????????????????????????????
왜 언디파인드?
문제를 조금 살펴보니 파일은 Binary가 아니라 Form으로 보내야 하는 것 같다. 이를 Form의 File Upload로 바꾸어보았다.
| Binary | Form |
|---|---|
![]() | ![]() |
두 가지가 어느 면에서 다른 것인지 잘 모르겠다.
이곳을 참고하는 것이 좋겠다.
Form type으로 바꿔서 리퀘스트를 보내니 정상 동작하였다.

만약 여기서 파일을 2개 이상 보낸다면?

바로 400을 반환해버린다. 이를 해결하기 위해선
FilesInterceptor()과 @UploadFiles()을 이용한다.
결국 인터셉터와 데코레이터만 바뀔 뿐이다. 그리고 forEach를 이용해서 각 각 출력하게 하였다.
@Post("upload-multi")
@UseInterceptors(FilesInterceptor('file')) // 이 부분과
uploadFiles(@UploadedFiles() files: Express.Multer.File[]) { // 이 부분만 바뀌었다.
// 여기서 Express.Multer.File[]은 Array<Express.Multer.File>으로도 사용 가능하다.
// Array<Express.Multer.File> 을 쓰는 것이 더 나을듯?
files.forEach(element => {
console.log(element)
});
}
controller를 수정하고 post 요청을 보내면

잘 업로드 된 것을 볼 수 있다.
FileFieldsInterceptor을 사용하는 방법이 있다.
이 경우엔 fieldName에 따라 maxCount를 설정할 수 있다!
즉, 아래와 같이 적용할 수 있다.
FileFieldsInterceptor([
{ name:"ico", maxCount:1 },
{ name:"plain-text", maxCount: 5}
], multerOption)
자세한 옵션은 이쪽으로. 설명이 잘 되어있다.
기본적으로 Option을 비우면 메모리에 저장이 된다고 한다.
로컬에 저장을 간단하게 구현해보았다.
@Post("upload-single")
@UseInterceptors(FileInterceptor('file', {
dest: "./files/upload"
}))
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log(file);
}
이것이 잘 올라갔는지 확인하였는데,

잘 올라갔다!
파일 이름이 저렇게 된 것은 multer에서 기본적으로 파일 이름을 랜덤으로 지정하기 때문.
이 부분을 수정해보자!
현재, 위의 코드는 하나의 라우터에만 작동하게 해놨다.
전체 컨트롤러에 적용되는 코드를 작성해보자.
// files.modules.ts 를 수정하자!
import * as fs from 'fs';
@Module({
controllers: [FilesController],
imports: [
MulterModule.registerAsync({ // ① 이 부분에서 많이 헤맸다.
imports: [ConfigModule],
useFactory: async (config: ConfigService) => ({
storage: diskStorage({
destination: (req, file, cb) => {
const dest = './src/files/upload/' // 나중에 변경! 환경변수로 해도 되고
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, {recursive: true})
}
cb(null, dest);
},
filename: (req, file, cb) => {
const randNum = Array(8)
.fill(null)
.map( () => Math.round(Math.random() * 16).toString(16) )
.join('')
cb(null, `${file.originalname}-${randNum}`) // ② NestJS: 파일 오류!!!!!!!
}
})
})
})
]
})
export class FilesModule {}
MulterModule.register( {storage: ...} )으로 하니 file.filename의 결과가 undefined가 떴다.register 메소드의 경우 정적으로 동작해서 사용자의 파일 이름을 잘 인식하지 못하였다.${randNum}.${file.originalname}으로 하려 하였으나, ts 파일이 들어오는 경우 서버를 구동하는데 필요한 파일으로 인식해서 오류가 발생하였다.${file.originalname}-${randNum}으로 바꾸었다.그리고 controller에서 FileInterceptor에 적용된 { dest: "./files/upload" }를 지우자.
그 후 POST 요청을 보내면...

정상적으로 올라갔음을 볼 수 있다!
수정이 몇가지 있었다.
/* 폴더 구조 */
nest - root
└── static
└── upload
./static/upload으로 수정하였다.이제 이 수정점을 바탕으로 간단한 파일 다운로더를 만들어보자.
// files.controller.ts
@Get("download")
downloadFile(@Req() req, @Res() res: Response, @Query('file') file: string)
const filepath = `./static/upload/${file}`;
const fileShowName = 'download';
res.download(filepath, fileShowName, (err) => { // ① : 파일 다운로드
if(err) {
res.status(500).send('File download failed');
}
})
}
이 상태로 다운로드 요청을 보내면 Axios 오류가 뜬다!
..???
???????????????????
ㅋㅋㅋㅋ........
정상적인 현상이다.
FastAPI에서도 겪었던 현상.
지금은 실습용이니 "모든 곳에서 요청을 받는다"를 하자.
main.ts에 app.enableCors(); 하나를 추가해주자.
이제 모든 곳에서 요청을 받을 수 있다.
앞으로 가야 할 발자취는...
둘 다 Guard로 처리하도록 한다.
2가지 Guard를 만들어준다.
NoQueryGuard - 쿼리에 'file'이 있는가?@Injectable()
export class NoParamGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
// context로 들어온 데이터를 http request로 변환. call by ref처럼 동작한다.
const req = context.switchToHttp().getRequest();
const query = req.query;
if (!(query && query.file)) { // 쿼리가 있는지 확인한다! 없으면 오류
throw new HttpException("'file' Query Not Found", HttpStatus.BAD_REQUEST);
}
return true;
}
}
FileExistGuard - 파일이 존재하는가?
@Injectable()
export class FileExistGuard implements CanActivate {
// async라 Promise<boolean>으로 반환
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
const filename = req.query.file;
const filepath = './static/upload';
try {
// 해당 경로에 있는 모든 파일을 가져옴
const files = await fs.promises.readdir(filepath); // 비동기 처리를 통해 성능 저하를 최대한 막음.
// 판별 함수를 적어도 하나라도 통과하는지 확인.
// 여기서는 파일 이름이 `filename`으로 시작하는지 확인.
const fileExists = files.some( file => file.startsWith(filename) );
// `filename`으로 시작하는 파일을 가져옴.
const filteredFiles = files.filter( (fileName) =>
fileName.startsWith(filename)
);
if (!fileExists) { // 파일이 없다면 에러 발생
throw new BadRequestException('Error reading file');
}
// 라우터로 보낼 데이터 설정
req.filenameRequest = filteredFiles;
return true;
} catch (err) {
throw new NotFoundException('Error reading directory');
}
}
}
controller의 router에도 적용해준다.
@Get("download")
@UseGuards(NoQueryGuard, FileExistGuard)
downloadFile(@Req() req, @Res() res: Response) { // ① : @Query 삭제
const file = req.filenameRequest;
const filepath = `./static/upload/${file}`;
const fileShowName = 'download';
res.download(filepath, fileShowName, (err) => { // ② : 다운로드 실행
if(err) {
res.status(500).send('File download failed');
}
})
}
res.download(파일_경로, 다운로드시_보여질_이름, 에러_핸들러)이제 테스트용 HTML을 다운받아 테스트 해보자.
하려는 프로젝트가 웹에서 바로 데이터를 뜯어봐야하기 때문에 로컬 다운로드 대신 메모리에 다운로드 하도록 하였다.
파일 이름이 유사한 것이 있다면 내용을, 파일 이름이 유사한 것이 없다면 에러를 화면에 출력할 것이다.
이로써 파일 업로드/다운로드까지 진행해보았다!
업로드에서 더 많은 MulterOption이 있지만, 여기서는 자세히 작성하지 않았다. 실제로 이정도만 해도 큰 문제가 없기도 하고.
디렉터리 구조를 벨로그 MD에서 사용해보자!├── data
│ ├── train
│ ├── test
│ └── validation
├── code
│ ├── train.py
│ ├── classify.py
│ ├── model.py
│ └── dataset.py
└── run.sh