๐ NestJS ๋ฐฑ์๋ ๊ฐ๋ฐ: ๊ธฐ์ด๋ถํฐ ์ค์ ๊น์ง - NestJS์์ ํ์ผ ์ ๋ก๋ ๋ฐ ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ
๐ NestJS ๋ฐฑ์๋ ๊ฐ๋ฐ: ๊ธฐ์ด๋ถํฐ ์ค์ ๊น์ง - NestJS์์ ํ์ผ ์ ๋ก๋ ๋ฐ ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ
NestJS๋ ํ์ผ ์
๋ก๋ ๋ฐ ์คํธ๋ฆฌ๋ฐ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ฌ ์ด๋ฏธ์ง, ๋์์, ๋ฌธ์ ๋ฑ ๋ค์ํ ํ์ผ์ API๋ฅผ ํตํด ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.
์ด๋ฒ ๊ธ์์๋ @nestjs/platform-express์ Multer๋ฅผ ์ฌ์ฉํ ํ์ผ ์
๋ก๋, ๊ทธ๋ฆฌ๊ณ ๋์์ ๋ฐ ๋์ฉ๋ ํ์ผ์ ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ์ ์๊ฐํฉ๋๋ค. ๐
9.1 NestJS์์ ํ์ผ ์ ๋ก๋๋ฅผ ์ํ ์ค๋น
โ Multer ์ค์น (ํ์ผ ์ ๋ก๋ ๋ฏธ๋ค์จ์ด)
NestJS๋ ๋ด๋ถ์ ์ผ๋ก Multer๋ฅผ ์ฌ์ฉํด ํ์ผ ์ ๋ก๋๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค. ๋ค์ ๋ช ๋ น์ด๋ก ํ์ํ ํจํค์ง๋ฅผ ์ค์นํฉ๋๋ค.
npm install @nestjs/platform-express multer
npm install --save-dev @types/multer
9.2 ๋จ์ผ ํ์ผ ์ ๋ก๋ ๊ตฌํํ๊ธฐ
๐ users.controller.ts
import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
@Controller('users')
export class UsersController {
@Post('upload')
@UseInterceptors(FileInterceptor('file', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, callback) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = extname(file.originalname);
callback(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
},
}),
}))
uploadFile(@UploadedFile() file: Express.Multer.File) {
return {
originalname: file.originalname,
filename: file.filename,
size: file.size,
};
}
}
โ
@UseInterceptors(FileInterceptor)๋ฅผ ์ฌ์ฉํ์ฌ ๋จ์ผ ํ์ผ ์
๋ก๋ ์ฒ๋ฆฌ
โ
diskStorage๋ฅผ ์ด์ฉํด ์
๋ก๋ ๊ฒฝ๋ก์ ํ์ผ๋ช
์ปค์คํฐ๋ง์ด์ง
โ
์
๋ก๋๋ ํ์ผ์ ./uploads ๋๋ ํฐ๋ฆฌ์ ์ ์ฅ๋จ
9.3 ๋ค์ค ํ์ผ ์ ๋ก๋ ์ฒ๋ฆฌ
๐ users.controller.ts
import { UploadedFiles, UseInterceptors } from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
@Post('uploads')
@UseInterceptors(FilesInterceptor('files', 10)) // ์ต๋ 10๊ฐ๊น์ง ์
๋ก๋
uploadMultipleFiles(@UploadedFiles() files: Array<Express.Multer.File>) {
return files.map(file => ({
originalname: file.originalname,
filename: file.filename,
}));
}
โ
FilesInterceptor๋ฅผ ์ฌ์ฉํด ๋ค์ค ํ์ผ ์ฒ๋ฆฌ ๊ฐ๋ฅ
โ
@UploadedFiles()๋ฅผ ํตํด ํ์ผ ๋ฐฐ์ด์ ๋ฐ์ ์ ์์
9.4 ํ์ผ ์ ๋ก๋ ์ ํํฐ๋ง ๋ฐ ์ ํ ์ค์
@UseInterceptors(FileInterceptor('file', {
fileFilter: (req, file, cb) => {
if (!file.mimetype.match(/\/(jpg|jpeg|png)$/)) {
return cb(new Error('Only image files are allowed!'), false);
}
cb(null, true);
},
limits: {
fileSize: 5 * 1024 * 1024, // 5MB ์ ํ
},
}))
โ
ํน์ MIME ํ์
๋ง ํ์ฉ (์: ์ด๋ฏธ์ง ํ์ผ๋ง)
โ
์
๋ก๋ ํ์ผ ํฌ๊ธฐ ์ ํ ๊ฐ๋ฅ
9.5 ์ ๋ก๋๋ ํ์ผ ์ ๊ณต (Static Serve)
@nestjs/serve-static ํจํค์ง๋ฅผ ์ฌ์ฉํ๋ฉด ์ ์ ํ์ผ ๊ฒฝ๋ก๋ก ์ ๋ก๋๋ ํ์ผ์ ์ ๊ทผํ ์ ์์ต๋๋ค.
npm install @nestjs/serve-static
๐ app.module.ts
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
@Module({
imports: [
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'uploads'),
serveRoot: '/uploads',
}),
],
})
export class AppModule {}
โ http://localhost:3000/uploads/ํ์ผ๋ช ์ผ๋ก ํ์ผ ์ ๊ทผ ๊ฐ๋ฅ
9.6 ํ์ผ ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ (๋์์/๋์ฉ๋ ํ์ผ)
๋์์, ์ค๋์ค, ๋์ฉ๋ ํ์ผ์ ์ผ๋ฐ ๋ค์ด๋ก๋ ๋์ ์คํธ๋ฆฌ๋ฐ ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌํ๋ ๊ฒ์ด ํจ์จ์ ์ ๋๋ค.
๐ stream.controller.ts
import { Controller, Get, Res, Req } from '@nestjs/common';
import { createReadStream, statSync } from 'fs';
import { join } from 'path';
import { Response, Request } from 'express';
@Controller('stream')
export class StreamController {
@Get('video')
streamVideo(@Req() req: Request, @Res() res: Response) {
const videoPath = join(__dirname, '..', 'uploads', 'sample.mp4');
const stat = statSync(videoPath);
const fileSize = stat.size;
const range = req.headers.range;
if (range) {
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
const chunksize = end - start + 1;
const file = createReadStream(videoPath, { start, end });
res.writeHead(206, {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Type': 'video/mp4',
});
file.pipe(res);
} else {
res.writeHead(200, {
'Content-Length': fileSize,
'Content-Type': 'video/mp4',
});
createReadStream(videoPath).pipe(res);
}
}
}
โ
Range ํค๋๋ฅผ ์ด์ฉํด ๋ถ๋ถ ์คํธ๋ฆฌ๋ฐ ์ง์
โ
ํด๋ผ์ด์ธํธ(๋ธ๋ผ์ฐ์ )๋ ๋น๋์ค๋ฅผ ์ ์ง์ ์ผ๋ก ๋ค์ด๋ก๋ํ๋ฉฐ ์ฌ์ ๊ฐ๋ฅ
9.7 ํ์ผ ์ ๋ก๋ ๋ฐ ์คํธ๋ฆฌ๋ฐ ์ค์ ํ
โ ํ์ผ ์ด๋ฆ ์ค๋ณต ๋ฐฉ์ง๋ฅผ ์ํด UUID ๋๋ ํ์์คํฌํ ํ์ฉ
โ ์
๋ก๋๋ ํ์ผ์ ์ ๊ทผ ๊ถํ์ ์ค์ ํ์ฌ ๋ณด์ ์ ์ง
โ S3 ๋๋ ํด๋ผ์ฐ๋ ์ ์ฅ์ ์ฐ๋์ผ๋ก ํ์ฅ ๊ฐ๋ฅ
โ ํ์ผ ์
๋ก๋/์ญ์ /๋ค์ด๋ก๋๋ฅผ RESTfulํ๊ฒ ์ค๊ณํ๊ธฐ
๐ก NestJS์ Express ๊ธฐ๋ฐ ๊ตฌ์กฐ ๋๋ถ์ Multer ๋ฐ ํ์ผ ์คํธ๋ฆฌ๋ฐ ์ฒ๋ฆฌ๊ฐ ๋งค์ฐ ์ ์ฐํฉ๋๋ค.
9.8 ๊ฒฐ๋ก : NestJS์์์ ํ์ผ ์ ๋ก๋ ๋ฐ ์คํธ๋ฆฌ๋ฐ ์ ๋ฆฌ
โ
Multer๋ฅผ ์ฌ์ฉํ ๋จ์ผ ๋ฐ ๋ค์ค ํ์ผ ์
๋ก๋ ์ง์
โ
ํ์ผ ํ์
์ ํ ๋ฐ ์
๋ก๋ ์ฉ๋ ์ ํ ์ค์ ๊ฐ๋ฅ
โ
ServeStaticModule๋ก ์
๋ก๋๋ ํ์ผ ์ ์ ์ ๊ณต
โ
Stream ๋ฐฉ์์ผ๋ก ๋์์ ๋ฑ ๋์ฉ๋ ํ์ผ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌ
๋ค์ ๊ธ์์๋ **"NestJS์ WebSocket์ ํ์ฉํ ์ค์๊ฐ ๊ธฐ๋ฅ ๊ตฌํ"**์ ๋ค๋ฃน๋๋ค. ๐
๐ ๋ค์ ๊ธ ์๊ณ : NestJS์ WebSocket์ ํ์ฉํ ์ค์๊ฐ ๊ธฐ๋ฅ ๊ตฌํ
๐ ๋ค์ ํธ: 10. NestJS WebSocket ์ค์๊ฐ ์ฒ๋ฆฌ