study/๋ฐฑ์—”๋“œ

๐Ÿ“Œ NestJS ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ: ๊ธฐ์ดˆ๋ถ€ํ„ฐ ์‹ค์ „๊นŒ์ง€ - NestJS์—์„œ ํŒŒ์ผ ์—…๋กœ๋“œ ๋ฐ ์ŠคํŠธ๋ฆฌ๋ฐ ์ฒ˜๋ฆฌ

octo54 2025. 3. 21. 11:41
๋ฐ˜์‘ํ˜•

๐Ÿ“Œ 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 ์‹ค์‹œ๊ฐ„ ์ฒ˜๋ฆฌ