훌륭한 Nest.js 블로그 만들기: 이미지 업로드
Daniel Hayes
Full-Stack Engineer · Leapcell

이전 튜토리얼에서는 블로그에 댓글 답글 기능을 구현했습니다.
이제 게시글 댓글 기능이 상당히 완성되었으므로, 게시글 자체만으로는 내용을 표현하기에 다소 부족해 보입니다. 지금은 일반 텍스트만 지원하기 때문입니다.
다음 튜토리얼에서는 게시글이 이미지 삽입을 지원하도록 하여 표현력을 풍부하게 만들 것입니다.
이미지 삽입의 원리는 다음과 같습니다:
- 사용자가 이미지를 선택하여 백엔드로 업로드합니다.
- 백엔드는 이미지를 특정 위치에 저장하고, 이미지 리소스에 접근할 수 있는 URL을 반환합니다.
- 프런트엔드는 이미지 URL을 게시글 내용에 삽입합니다.
- 게시글 내용은 최종적으로 웹 페이지로 렌더링되고, 이미지는 이미지 URL에 따라 해당 데이터를 가져와 표시합니다.
1단계: S3 호환 객체 스토리지 준비
코드를 시작하기 전에 업로드된 이미지를 저장할 공간이 필요합니다. 서버 디스크에 직접 이미지를 저장하는 것도 한 가지 방법이지만, 최신 애플리케이션에서는 고가용성, 쉬운 확장성, 저렴한 비용 등의 장점 때문에 객체 스토리지 서비스(AWS S3 등)를 사용하는 것이 더 권장됩니다.
편의를 위해 데이터베이스 및 백엔드 호스팅뿐만 아니라 S3 호환 객체 스토리지 서비스도 제공하는 Leapcell을 계속 사용하겠습니다.
- 버킷 생성: Leapcell 콘솔에 로그인하여 "객체 스토리지" 페이지로 이동한 후 "버킷 생성"을 클릭합니다. 전역적으로 고유한 버킷 이름(예:
my-nest-blog-images
)을 입력하고 리전을 선택합니다. - 액세스 자격 증명 가져오기: 버킷 상세 페이지 또는 계정 설정에서 액세스 키 ID 및 비밀 액세스 키를 찾습니다. 또한 엔드포인트 주소도 기록해 둡니다.
이 정보(버킷 이름, 엔드포인트, 액세스 키, 비밀 키)는 중요하며, 나중에 백엔드 설정에 사용될 것입니다.
2단계: 백엔드에 이미지 업로드 API 구현
이제 파일 업로드를 처리하는 Nest.js 백엔드를 구축해 보겠습니다.
1. 종속성 설치
S3 호환 스토리지 서비스와 상호 작용하기 위해 aws-sdk
(최신 버전은 @aws-sdk/client-s3
)가 필요하며, multipart/form-data
요청을 처리하기 위해 @nestjs/platform-multer
가 필요합니다.
npm install @aws-sdk/client-s3 npm install @nestjs/platform-multer
2. 환경 변수 구성
보안상의 이유로 S3 자격 증명을 코드에 직접 하드코딩하지 마십시오. 프로젝트 루트 디렉토리에 .env
파일(아직 없는 경우)을 만들고 다음 내용을 추가합니다:
# .env S3_ENDPOINT=https://objects.leapcell.io S3_ACCESS_KEY_ID=YOUR_ACCESS_KEY_ID S3_SECRET_ACCESS_KEY=YOUR_SECRET_ACCESS_KEY S3_BUCKET_NAME=my-nest-blog-images
위 값들을 실제 정보로 교체해야 합니다.
Nest.js가 .env
파일을 읽을 수 있도록 하려면 config 모듈을 설치해야 합니다:
npm install @nestjs/config
그런 다음 app.module.ts
에서 ConfigModule
을 가져옵니다:
// src/app.module.ts import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; // ... 기타 가져오기 @Module({ imports: [ ConfigModule.forRoot({ isGlobal: true }), // 전역 모듈로 설정 // ... 기타 모듈 ], // ... }) export class AppModule {}
3. 업로드 모듈 생성
파일 업로드 기능을 위한 별도의 모듈을 생성합니다.
nest generate module uploads nest generate controller uploads nest generate service uploads
uploads.service.ts
작성
이 서비스는 S3와의 통신 핵심 로직을 담당합니다.
// src/uploads/uploads.service.ts import { Injectable } from '@nestjs/common'; import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import { ConfigService } from '@nestjs/config'; import { v4 as uuidv4 } from 'uuid'; @Injectable() export class UploadsService { private readonly s3: S3Client; constructor(private readonly configService: ConfigService) { this.s3 = new S3Client({ endpoint: this.configService.get<string>('S3_ENDPOINT'), region: 'us-east-1', // S3 호환 스토리지의 경우, 리전은 종종 형식적인 값입니다 credentials: { accessKeyId: this.configService.get<string>('S3_ACCESS_KEY_ID'), secretAccessKey: this.configService.get<string>('S3_SECRET_ACCESS_KEY'), }, }); } async uploadFile(file: Express.Multer.File): Promise<string> { const bucket = this.configService.get<string>('S3_BUCKET_NAME'); const endpoint = this.configService.get<string>('S3_ENDPOINT'); const uniqueFileName = `${uuidv4()}-${file.originalname}`; const command = new PutObjectCommand({ Bucket: bucket, Key: uniqueFileName, Body: file.buffer, ContentType: file.mimetype, ACL: 'public-read', // 파일을 공개적으로 읽을 수 있도록 설정 }); try { await this.s3.send(command); // 파일의 공개 URL 반환 return `${endpoint}/${bucket}/${uniqueFileName}`; } catch (error) { console.error('S3 업로드 오류:', error); throw new Error('파일 업로드 실패.'); } } }
uploads.controller.ts
작성
이 컨트롤러는 API 라우트를 정의하고 FileInterceptor
를 사용하여 업로드된 파일을 받습니다.
// src/uploads/uploads.controller.ts import { Controller, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { UploadsService } from './uploads.service'; import { AuthenticatedGuard } from '../auth/authenticated.guard'; @Controller('uploads') export class UploadsController { constructor(private readonly uploadsService: UploadsService) {} @UseGuards(AuthenticatedGuard) // 로그인한 사용자만 업로드 가능 @Post('image') @UseInterceptors(FileInterceptor('file')) // 'file'은 폼의 파일 필드 이름입니다 async uploadImage(@UploadedFile() file: Express.Multer.File) { const url = await this.uploadsService.uploadFile(file); return { url }; } }
마지막으로 app.module.ts
에 UploadsModule
을 가져와서 애플리케이션이 이 새 모듈을 인식하도록 합니다.
3단계: FilePicker API와 프런트엔드 연동
이제 백엔드가 준비되었으니, 프런트엔드 페이지 new-post.ejs
를 수정하여 업로드 기능을 추가해 보겠습니다.
FilePicker API 대 기존 <input type="file">
시작하기 전에 두 가지 프런트엔드 파일 선택 방식의 차이점을 간략하게 비교해 보겠습니다:
- **기존 방식: `<input type=