백엔드에서 파일 저장 처리

ddoachi·2025년 5월 6일

TekaPicker

목록 보기
17/30

1. 파일 수신 처리

  • NestJS API GatewayController에서 @UseInterceptors(FileInterceptor) 설정
  • storage: diskStorage({...})로 저장 경로 및 파일명 지정
  • destination: join(process.cwd(), 'uploads')로 프로젝트 루트에 저장

FileInterceptorcallback 함수 동작 방식 상세 설명

NestJS에서 파일 업로드를 처리할 때 사용하는 FileInterceptor는 내부적으로 multerdiskStorage 전략을 사용한다. 이때 destinationfilename 설정은 사용자 정의 함수로 작성하며, 각각의 함수는 다음과 같은 시그니처를 가진다:

destination: (req, file, callback) => { ... }
filename: (req, file, callback) => { ... }

여기서 callback은 우리가 직접 정의하는 함수가 아니라 multer가 내부적으로 제공하는 함수로, 우리가 호출해야 multer가 실제 파일 저장 경로와 이름을 인식하고 처리할 수 있다.


callback의 구조와 의미

callback(error, result);
  • error: 첫 번째 인자. 에러가 있으면 Error 객체 전달, 없으면 null
  • result: 두 번째 인자. 최종적으로 multer에게 넘길 저장 경로나 파일명

이 구조는 Node.js의 전통적인 error-first callback 패턴을 따른다. 예를 들어 fs.readFile() 등 Node.js 표준 API들도 동일한 패턴을 사용한다.


예시: destination 설정

destination: (req, file, callback) => {
  const absPath = join(process.cwd(), 'uploads');
  callback(null, absPath); // 정상 처리
}
  • process.cwd()는 현재 실행 중인 Node.js 프로젝트 루트 경로
  • callback(null, absPath)로 multer에게 저장할 디렉토리 전달

예시: filename 설정

filename: (req, file, callback) => {
  const ext = extname(file.originalname); // 원본 확장자 추출
  const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
  const filename = `menu-${uniqueSuffix}${ext}`;
  callback(null, filename); // multer에게 저장 파일명 전달
}
  • 확장자는 유지하고, 파일명 충돌 방지를 위해 timestamp + 랜덤값 조합
  • 저장 결과는 uploads/menu-123456789.png처럼 됨

에러 발생 시 사용 예

destination: (req, file, callback) => {
  const isBlocked = true;
  if (isBlocked) {
    return callback(new Error('업로드가 허용되지 않은 파일입니다.'));
  }
  callback(null, '/uploads');
}
  • 이렇게 하면 multer는 저장을 중단하고 NestJS에 에러를 전달함
  • 클라이언트는 HTTP 500 에러 응답을 받게 됨

결론

항목설명
callback 제공자multer (diskStorage)
첫 번째 인자에러 (null이면 정상)
두 번째 인자저장 경로 또는 파일명
호출 시점파일 저장 전에 우리가 직접 호출
목적multer에게 경로/파일명을 전달하여 디스크 저장 유도

이처럼 callback은 단순한 함수 호출이 아니라, multer 내부 로직과 연결된 중요한 커넥터 역할을 한다.

실제 코드

@Post('create')
  @UseInterceptors(
    FileInterceptor('file', {
      storage: diskStorage({
        destination: (req, file, callback) => {
          const absPath = join(process.cwd(), 'uploads'); // 루트 기준 uploads
          console.log('💾 파일 저장 경로:', absPath);
          callback(null, absPath);
        },
        filename: (req, file, callback) => {
          const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
          const ext = extname(file.originalname);
          const filename = `menu-${uniqueSuffix}${ext}`;
          callback(null, filename);
        },
      }),
    }),
  )
  createMenu(
    @UploadedFile() file: Express.Multer.File,
    @Body() dto: CreateMenuDto
  ): Observable<CreateMenuResponse> {
    console.log('[Api Gateway] /menu/create 요청 도착');
    console.log('파일: ', file);
    console.log('data: ', dto);

    const imagePath = `/uploads/${file.filename}`;

    const fullDto = new CreateMenuDto();
    Object.assign(fullDto, dto, { imagePath });

    // ✅ 여기서 타입 확인
    console.log('💡 imagePath 타입:', typeof fullDto.imagePath, fullDto.imagePath);

    return this.menuService.createMenu(fullDto);
  }

2. 파일 저장 후 imagePath 삽입

  • @UploadedFile()으로 받은 파일에서 file.filename 확인
  • imagePath = /uploads/${file.filename}로 URL 경로 구성
  • DTO에 Object.assign(dto, { imagePath })로 수동 삽입

3. DTO 수정

  • CreateMenuDto@IsOptional() @IsString() imagePath?: string 추가
  • toGrpc()에서 imagePath: this.imagePath ?? '' 형태로 gRPC 요청 구성

4. gRPC 및 DB 처리

  • menu.service.ts에서 imagePath: data.imagePath를 포함해 Entity 생성
  • menu.entity.ts@Column({ nullable: true }) imagePath 필드 추가
  • 마이그레이션 생성 및 반영

5. 주의 사항

  • proto 수정 후에는 gRPC 서버(menu-service) 반드시 재시작해야 반영됨
  • uploads/ 디렉토리는 존재해야 하며, 없을 경우 직접 생성하거나 자동 생성 필요
profile
내일도 풀스택

0개의 댓글