Nest.js를 배워보자/5. NestJS + MySQL 실전 예제 — Excel Import

NestJS + MySQL 실전 예제: Excel Import

_Blue_Sky_ 2025. 12. 2. 17:42
728x90

 


1. 📤 엑셀 파일 업로드 환경 설정 (Multer)

NestJS에서 파일 업로드를 처리하려면 @nestjs/platform-express에서 제공하는 MulterModule을 사용합니다. Multer는 HTTP 요청의 multipart/form-data를 파싱하여 파일을 처리하는 미들웨어입니다.

1.1. MulterModule 설정

파일 업로드를 처리할 모듈(예: UploadModule)에 Multer를 설정합니다. 여기서는 파일을 메모리(Buffer)에 저장하여 바로 읽는 방식을 사용합니다.

// upload.module.ts
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
import { UploadController } from './upload.controller';

@Module({
  imports: [
    // 파일을 메모리에 Buffer 형태로 저장하도록 설정
    MulterModule.register({
      dest: './upload', // 로컬 저장소 경로 (Buffer로 처리하면 사실상 임시 경로)
    }),
  ],
  controllers: [UploadController],
  // ...
})
export class UploadModule {}

 

728x90

1.2. 컨트롤러에서 파일 수신

컨트롤러에서 @UseInterceptors(FileInterceptor('file')) 데코레이터와 @UploadedFile() 데코레이터를 사용하여 클라이언트가 보낸 엑셀 파일을 받습니다.

// upload.controller.ts
import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { UploadService } from './upload.service';

@Controller('upload/excel')
export class UploadController {
  constructor(private readonly uploadService: UploadService) {}

  @Post()
  // 💡 클라이언트가 'file'이라는 이름으로 파일을 보냄
  @UseInterceptors(FileInterceptor('file')) 
  async importExcel(@UploadedFile() file: Express.Multer.File) {
    // file.buffer에 엑셀 파일의 데이터가 담겨 있습니다.
    return this.uploadService.processExcel(file.buffer);
  }
}

2. 📝 xlsx 라이브러리 사용 및 데이터 읽기

xlsx (또는 sheetjs)는 엑셀 파일을 파싱하여 JavaScript 객체로 변환해주는 Node.js 라이브러리입니다.

2.1. 라이브러리 설치

npm install xlsx
npm install -D @types/xlsx

2.2. 특정 시트/특정 행부터 읽기

UploadService에서 파일 버퍼를 받아 원하는 시트의 데이터를 추출합니다.

// upload.service.ts
import { Injectable } from '@nestjs/common';
import * as XLSX from 'xlsx';

@Injectable()
export class UploadService {
  async processExcel(buffer: Buffer) {
    const workbook = XLSX.read(buffer, { type: 'buffer' });
    
    // 1. 특정 시트 이름으로 시트 선택 (예: 'UserList')
    const sheetName = 'UserList';
    const worksheet = workbook.Sheets[sheetName];

    if (!worksheet) {
      throw new Error(`Sheet ${sheetName} not found.`);
    }

    // 2. 특정 범위(A3부터)부터 데이터 읽기 (선택적)
    // worksheet['!ref'] = 'A3:Z999'; 

    // 3. 데이터를 JSON 배열로 변환
    const data = XLSX.utils.sheet_to_json(worksheet, {
      header: 1, // 첫 번째 행을 헤더로 사용하지 않고 배열로 반환
      range: 1, // 엑셀의 2번째 행(1-based)부터 데이터를 읽기 (헤더 행을 건너뜀)
    });
    
    // data[0]는 엑셀 2번째 행의 데이터, data[1]는 3번째 행...

    // 4. DB 저장 로직 호출
    return this.saveToDatabase(data);
  }
  
  // ... (saveToDatabase 메서드 구현)
}

 

728x90

3. 💾 DB 저장 및 대용량 처리 전략 (Prisma)

파싱된 엑셀 데이터 배열을 Prisma를 사용하여 데이터베이스에 저장합니다.

3.1. 기본 저장 (Batch Insert)

대량의 데이터를 효율적으로 삽입하기 위해 createMany 기능을 사용하는 것이 좋습니다. 이는 단일 SQL 쿼리로 여러 레코드를 삽입하여 성능을 크게 개선합니다.

// upload.service.ts (계속)
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class UploadService {
  constructor(private prisma: PrismaService) {}
  
  // ... (processExcel 메서드)

  async saveToDatabase(excelData: any[]) {
    // 💡 엑셀 데이터 포맷을 DB 모델에 맞게 변환하는 과정 필요
    const usersToCreate = excelData.map(row => ({
      email: row[0], // 엑셀 A열
      name: row[1],  // 엑셀 B열
      // ...
    }));

    // 💡 createMany: 단일 트랜잭션으로 대량의 레코드 동시 삽입
    const result = await this.prisma.user.createMany({
      data: usersToCreate,
      skipDuplicates: true, // 중복되는 레코드가 있을 경우 건너뛰기 (선택적)
    });

    return { count: result.count, message: 'Excel data imported successfully.' };
  }
}

3.2. 대용량 처리 시 주의점 (비동기 및 청크 처리)

수만 건 이상의 대용량 파일을 처리할 때는 다음과 같은 전략을 고려해야 합니다.

  1. 청크(Chunk) 처리: 모든 데이터를 한 번에 메모리에 올리거나 createMany로 삽입하는 것은 서버 자원을 과부하 시킬 수 있습니다. 데이터를 작은 덩어리(청크)로 나누어 순차적으로 처리합니다.
  2. 비동기 워커: 파일 업로드 및 파싱은 시간이 오래 걸릴 수 있으므로, HTTP 응답 시간을 초과하지 않도록 메시지 큐(RabbitMQ, Kafka)를 사용하거나 NestJS의 Queue 모듈을 통해 백그라운드 작업으로 분리하여 처리해야 합니다.
  3. CSV 사용 고려: Excel(.xlsx)보다 .csv 파일이 파싱이 더 빠르고 메모리 효율이 좋습니다. 대용량 처리 시 CSV를 권장하는 것이 일반적입니다.
728x90