Nest.js에서 대용량 파일 청크 업로드 구현

파일 업로드는 웹 개발에서 흔히 접하는 요구사항입니다. content-typemultipart/form-data로 지정하면 데이터가 다음과 같은 형태로 서버에 전달됩니다:

서버는 multipart/form-data 형식을 파싱하여 파일 데이터를 추출할 수 있습니다. 그러나 대용량 파일의 경우 상황이 달라집니다. 100MB 파일 업로드에 3분이 소요된다면, 1GB 파일은 약 30분이 걸립니다. 이러한 대용량 파일 업로드를 최적화하기 위해 파일을 작은 조각으로 분할하여 병렬 업로드한 후 서버에서 재조합하는 방식인 청크 업로드를 사용합니다.

파일 분할 및 병합 방법

브라우저에서는 Blob 객체의 slice 메서드를 사용하여 파일을 분할할 수 있습니다. File 객체는 Blob을 상속하므로 동일한 방법으로 분할이 가능합니다.

Node.js 환경에서는 fs.createWriteStreamstart 옵션을 활용하여 각 청크를 파일의 특정 위치에 기록함으로써 원본 파일로 병합할 수 있습니다.

Nest.js 프로젝트 설정

먼저 Nest.js 프로젝트를 생성합니다:

nest new chunk-file-upload

Multer 타입 정의를 설치합니다:

npm install -D @types/multer

AppController에 파일 업로드를 처리할 라우트를 추가합니다. POST 엔드포인트로 전송된 파일을 처리하며, Multer 인터셉터를 사용합니다:

import { Controller, Post, UseInterceptors, UploadedFiles, Body } from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
import * as fs from 'fs';

@Controller()
export class AppController {
  @Post('upload')
  @UseInterceptors(FilesInterceptor('chunks', 10, { dest: 'temp' }))
  async receiveChunks(@UploadedFiles() chunks: Express.Multer.File[], @Body() body: { chunkName: string }) {
    const originalName = body.chunkName.replace(/-\d+$/, '');
    const chunkStorageDir = `temp/${originalName}_chunks`;

    if (!fs.existsSync(chunkStorageDir)) {
      fs.mkdirSync(chunkStorageDir);
    }

    fs.renameSync(chunks[0].path, `${chunkStorageDir}/${body.chunkName}`);
  }
}

메인 애플리케이션 파일에서 CORS를 활성화합니다:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.enableCors();
  await app.listen(3000);
}
bootstrap();

클라이언트 구현

다음 HTML 파일은 파일 입력을 받아 청크로 분할하고 서버에 전송합니다:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Chunk Upload Demo</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
    <input type="file" id="fileSelector" />
    <script>
        document.getElementById('fileSelector').onchange = async function(e) {
            const selectedFile = e.target.files[0];
            const segmentSize = 20 * 1024; // 20KB 단위로 분할
            const segments = [];

            let currentOffset = 0;
            while (currentOffset < selectedFile.size) {
                const segment = selectedFile.slice(currentOffset, currentOffset + segmentSize);
                segments.push(segment);
                currentOffset += segmentSize;
            }

            segments.forEach(async (seg, idx) => {
                const formPayload = new FormData();
                formPayload.append('chunks', seg);
                formPayload.set('chunkName', `${selectedFile.name}-${idx}`);
                await axios.post('http://localhost:3000/upload', formPayload);
            });

            // 모든 청크 업로드 후 병합 요청
            await axios.get(`http://localhost:3000/merge?filename=${selectedFile.name}`);
        };
    </script>
</body>
</html>

파일 병합 처리

모든 청크 업로드가 완료되면 서버에서 병합을 수행합니다:

@Get('merge')
mergeFile(@Query('filename') filename: string) {
  const chunkDirectory = `temp/${filename}_chunks`;
  const chunkFiles = fs.readdirSync(chunkDirectory);

  let writePosition = 0;
  chunkFiles.forEach(chunkFile => {
    const chunkPath = `${chunkDirectory}/${chunkFile}`;
    const readStream = fs.createReadStream(chunkPath);
    readStream.pipe(fs.createWriteStream(`uploads/${filename}`, { start: writePosition }));
    writePosition += fs.statSync(chunkPath).size;
  });

  // 임시 청크 디렉토리 정리
  fs.rmSync(chunkDirectory, { recursive: true });
}

태그: Nest.js File Upload Chunk Upload Multer Node.js

8월 16일 03:25에 게시됨