API Gateway의 Controller에서 @UseInterceptors(FileInterceptor) 설정storage: diskStorage({...})로 저장 경로 및 파일명 지정destination: join(process.cwd(), 'uploads')로 프로젝트 루트에 저장FileInterceptor의 callback 함수 동작 방식 상세 설명NestJS에서 파일 업로드를 처리할 때 사용하는 FileInterceptor는 내부적으로 multer의 diskStorage 전략을 사용한다. 이때 destination과 filename 설정은 사용자 정의 함수로 작성하며, 각각의 함수는 다음과 같은 시그니처를 가진다:
destination: (req, file, callback) => { ... }
filename: (req, file, callback) => { ... }
여기서 callback은 우리가 직접 정의하는 함수가 아니라 multer가 내부적으로 제공하는 함수로, 우리가 호출해야 multer가 실제 파일 저장 경로와 이름을 인식하고 처리할 수 있다.
callback의 구조와 의미callback(error, result);
error: 첫 번째 인자. 에러가 있으면 Error 객체 전달, 없으면 nullresult: 두 번째 인자. 최종적으로 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에게 저장 파일명 전달
}
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);
}
@UploadedFile()으로 받은 파일에서 file.filename 확인imagePath = /uploads/${file.filename}로 URL 경로 구성Object.assign(dto, { imagePath })로 수동 삽입CreateMenuDto에 @IsOptional() @IsString() imagePath?: string 추가toGrpc()에서 imagePath: this.imagePath ?? '' 형태로 gRPC 요청 구성menu.service.ts에서 imagePath: data.imagePath를 포함해 Entity 생성menu.entity.ts에 @Column({ nullable: true }) imagePath 필드 추가proto 수정 후에는 gRPC 서버(menu-service) 반드시 재시작해야 반영됨uploads/ 디렉토리는 존재해야 하며, 없을 경우 직접 생성하거나 자동 생성 필요