NestJS 애플리케이션에서 AWS S3를 이용해 파일을 업로드하는 기능을 개발한다. 이를 통해 대용량 파일 저장, 안정성, 확장성, 비용 효율성 등의 이점을 활용하며 서비스 품질을 높이는 것이 목적이다.
src/
├── file-upload/
│ ├── file-upload.controller.ts
│ ├── file-upload.service.ts
│ └── file-upload.module.ts
AWS_REGION=ap-northeast-2
AWS_ACCESS_KEY_ID=YOUR_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=YOUR_SECRET_KEY
AWS_S3_BUCKET=your-bucket-name
import { Module } from '@nestjs/common';
import { FileUploadController } from './file-upload.controller';
import { FileUploadService } from './file-upload.service';
@Module({
controllers: [FileUploadController],
providers: [FileUploadService],
})
export class FileUploadModule {}
import {
Controller,
Post,
UploadedFile,
UploadedFiles,
UseInterceptors,
Body,
BadRequestException,
} from '@nestjs/common';
import { FileUploadService } from './file-upload.service';
import {
FileInterceptor,
FileFieldsInterceptor,
} from '@nestjs/platform-express';
import { ApiTags, ApiOperation, ApiConsumes } from '@nestjs/swagger';
@ApiTags('File Upload')
@Controller('upload')
export class FileUploadController {
constructor(private readonly fileUploadService: FileUploadService) {}
@Post('single')
@ApiOperation({ summary: '단일 파일 업로드' })
@ApiConsumes('multipart/form-data')
@UseInterceptors(FileInterceptor('file'))
async uploadSingle(
@UploadedFile() file: Express.Multer.File,
@Body() body: any,
) {
if (!file) throw new BadRequestException('파일이 존재하지 않습니다');
return await this.fileUploadService.uploadSingle(file, body);
}
@Post('multiple')
@ApiOperation({ summary: '다중 파일 업로드 (최대 5개)' })
@ApiConsumes('multipart/form-data')
@UseInterceptors(
FileFieldsInterceptor(
[{ name: 'files', maxCount: 5 }],
{ limits: { fileSize: 20 * 1024 * 1024 } },
),
)
async uploadMultiple(
@UploadedFiles() files: { files: Express.Multer.File[] },
@Body() body: any,
) {
if (!files?.files?.length) throw new BadRequestException('파일이 없습니다');
return await this.fileUploadService.uploadMultiple(files.files, body);
}
}
import {
Injectable,
InternalServerErrorException,
NotFoundException,
} from '@nestjs/common';
import { extname } from 'path';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { ConfigService } from '@nestjs/config';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class FileUploadService {
private s3: S3Client;
private bucket: string;
constructor(private readonly configService: ConfigService) {
this.s3 = new S3Client({
region: this.configService.get('AWS_REGION'),
credentials: {
accessKeyId: this.configService.get('AWS_ACCESS_KEY_ID'),
secretAccessKey: this.configService.get('AWS_SECRET_ACCESS_KEY'),
},
});
this.bucket = this.configService.get('AWS_S3_BUCKET');
}
async uploadSingle(file: Express.Multer.File, body: any) {
const key = this.generateFileKey(file.originalname, body?.subdir);
await this.uploadToS3(file.buffer, file.mimetype, key);
return {
url: this.getFileUrl(key),
name: file.originalname,
key,
};
}
async uploadMultiple(files: Express.Multer.File[], body: any) {
const results = await Promise.all(
files.map((file, index) => this.uploadSingle(file, { ...body, index })),
);
return results;
}
private generateFileKey(filename: string, subdir = 'default'): string {
const ext = extname(filename);
const uniqueName = `${uuidv4()}${ext}`;
return `${subdir}/${uniqueName}`;
}
private async uploadToS3(
buffer: Buffer,
contentType: string,
key: string,
) {
try {
await this.s3.send(
new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: buffer,
ContentType: contentType,
}),
);
} catch (err) {
console.error(err);
throw new InternalServerErrorException('S3 업로드 실패');
}
}
private getFileUrl(key: string): string {
return `https://${this.bucket}.s3.${this.configService.get('AWS_REGION')}.amazonaws.com/${key}`;
}
}
POST /upload/single
요청 본문: multipart/form-data
응답:
{
"url": "https://your-bucket.s3.ap-northeast-2.amazonaws.com/path/to/file.jpg",
"name": "file.jpg",
"key": "path/to/file.jpg"
}