본문 바로가기
NestJS

[NestJS] 파일 업로드 (클래식 방식)

by Programmer.Junny 2025. 3. 7.

패키지 설치

yarn add multer @types/multer uuid @types/uuid

npm i multer @types/multer uuid @types/uuid

파일 및 폴더 경로 정리

path.const.ts 파일 생성

import { join } from "path";

// 서버 프로젝트의 루트 폴더
export const PROJECT_ROOT_PATH = process.cwd();
// 외부에서 접근 가능한 파일들을 모아둔 폴더 이름
export const PUBLIC_FOLDER_NAME = 'public';
// 포스트 이미지들을 저장할 폴더 이름
export const POSTS_FOLDER_NAME = 'posts';

// 실제 공개폴더의 절대경로
// /{프로젝트의 위치}/public
export const PUBLIC_FOLDER_PATH = join(
    PROJECT_ROOT_PATH,
    PUBLIC_FOLDER_NAME,
)

// 포스트 이미지를 저장할 경로
export const POST_IMAGE_PATH = join(
    PUBLIC_FOLDER_PATH,
    POSTS_FOLDER_NAME,
)

// /public/posts/xxx.jpg
export const POST_PUBLIC_IMAGE_PATH = join(
    PUBLIC_FOLDER_NAME,
    POSTS_FOLDER_NAME,
)

클라이언트에서 받은 이미지를 어느 경로에 저장할지, 어느 경로의 값으로 클라이언트에 반환할지 등 해당 경로들을 한데모아 작성한 파일이다.

PostsModel에 image 컬럼 생성

posts.entity.ts

@Column({
    nullable: true,
  })
  image: string;

PostsModel에 클라이언트에서 이미지를 받은 후 해당 이미지의 경로를 저장image 컬럼을 작성한다. 그냥 글만(Post) 있을 수 있기 때문에 nullabletrue로 한다.

posts.module.ts 에 Multer 모듈 세팅하기

import { BadRequestException, Module } from '@nestjs/common';
import { PostsService } from './posts.service';
import { PostsController } from './posts.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PostsModel } from './entities/posts.entity';
import { AuthModule } from 'src/auth/auth.module';
import { UsersModule } from 'src/users/users.module';
import { CommonModule } from 'src/common/common.module';
import { MulterModule } from '@nestjs/platform-express';
import { extname } from 'path';
import * as multer from 'multer';
import { POST_IMAGE_PATH } from 'src/common/const/path.const';
import {v4 as uuid} from 'uuid';
import { existsSync, mkdirSync } from 'fs';

// 업로드 경로가 존재하지 않으면 생성 (recursive 옵션으로 상위 폴더까지 생성)
if (!existsSync(POST_IMAGE_PATH)) {
  mkdirSync(POST_IMAGE_PATH, { recursive: true });
}

@Module({
  imports: [
    TypeOrmModule.forFeature([PostsModel]),
    AuthModule,
    UsersModule,
    CommonModule,
    MulterModule.register({
      limits: {
        // 바이트 단위로 입력
        fileSize: 1024 * 1024 * 10,
      },
      fileFilter: (req, file, cb) => {
        /**
         * cb(에러, boolean)
         * 
         * 첫 번째 파라미터에는 에러가 있을 경우 에러 정보를 넣어준다.
         * 두 번째 파라미터는 파일을 받을지 말지 boolean을 넣어준다.
         */

        // xxx.jpg -> jpg(확장자)만 가져옴
        const ext = extname(file.originalname);

        if(ext !== '.jpg' && ext !== '.jpeg' && ext !== '.png') {
          return cb(
            new BadRequestException('jpg/jpeg/png 파일만 업로드 가능합니다.'),
            false,
          );
        }

        return cb(null, true);
      },
      storage: multer.diskStorage({
        destination: function(req, res, cb) {
          cb(null, POST_IMAGE_PATH);
        },
        filename: function(req, file, cb) {
          cb(null, `${uuid()}${extname(file.originalname)}`)
        }
      }),
    }),
  ],
  controllers: [PostsController],
  providers: [PostsService],
  exports: [PostsService],
})
export class PostsModule {}
// 업로드 경로가 존재하지 않으면 생성 (recursive 옵션으로 상위 폴더까지 생성)
if (!existsSync(POST_IMAGE_PATH)) {
  mkdirSync(POST_IMAGE_PATH, { recursive: true });
}

해당 경로에 폴더가 없다면 생성해주도록 구현한다.

MulterModule.register({

register는 Multer가 작동할 환경설정을 해주는 것이다.

 limits: {
        // 바이트 단위로 입력
        fileSize: 1024 * 1024 * 10,
      },

limits는 파일의 사이즈를 어떻게 할지를 설정하며, 바이트 단위로 설정한다.

fileFilter: (req, file, cb) => {
        /**
         * cb(에러, boolean)
         * 
         * 첫 번째 파라미터에는 에러가 있을 경우 에러 정보를 넣어준다.
         * 두 번째 파라미터는 파일을 받을지 말지 boolean을 넣어준다.
         */

        // xxx.jpg -> jpg(확장자)만 가져옴
        const ext = extname(file.originalname);

        if(ext !== '.jpg' && ext !== '.jpeg' && ext !== '.png') {
          return cb(
            new BadRequestException('jpg/jpeg/png 파일만 업로드 가능합니다.'),
            false,
          );
        }

        return cb(null, true);
      },

fileFilter는 함수형식으로 설정하며, 매개변수로 req, file, cb를 입력할 수 있다.

 

req : 클라이언트에서 서버로 보내는 HTTP 요청 객체이다. 요청에 대한 다양한 정보(예: 헤더, 본문, URL, 쿼리 파라미터 등)를 포함하고 있다.

 

file: file은 업로드된 파일에 대한 정보를 담고 있는 객체이다. 이 객체는 Multer가 자동으로 생성하며, 주로 다음과 같은 속성을 포함한다:
• originalname: 사용자가 업로드한 원본 파일명.
• filename: 서버에 저장될 때 부여되는 파일명(설정에 따라 달라질 수 있음).
• mimetype: 파일의 MIME 타입 (예: image/jpeg).
• size: 파일의 크기.
• 용도:
이 객체를 활용해 파일의 확장자나 크기 등을 검증할 수 있다.

 

cb: cb는 콜백 함수로, 두 개의 매개변수를 받는다. 첫 번째 매개변수는 에러 객체이고, 두 번째는 파일을 수락할지 여부를 나타내는 boolean 값이다.

storage: multer.diskStorage({
        destination: function(req, res, cb) {
          cb(null, POST_IMAGE_PATH);
        },
        filename: function(req, file, cb) {
          cb(null, `${uuid()}${extname(file.originalname)}`)
        }
      }),
    }),

마지막으로 storage는 저장에 대한 설정이다.

destination은 파일이 저장될 디렉터리를 결정한다.

filename은 파일이 디스크에 저장될 때 사용할 파일명을 정의한다.

posts.controller.ts 의 postPosts 수정

  @Post()
  @UseGuards(AccessTokenGuard)
  @UseInterceptors(FileInterceptor('image'))
  postPosts(
    @User('id') userId: number,
    @Body() body: CreatePostDto,
    @UploadedFile() file?: Express.Multer.File,
    // @Body('title') title: string,
    // @Body('content') content: string,
  ) {
    return this.postsService.createPost(userId, body, file?.filename);
  }

@UseInterceptors(FileInterceptor(‘image’))

이 데코레이터는 NestJS의 인터셉터를 사용하여 HTTP 요청을 가로채고, 파일 업로드 처리를 담당한다. 여기서 FileInterceptor는 Multer를 기반으로 파일 업로드를 처리하는 인터셉터이다.

'image'

HTTP 요청의 폼 데이터(form-data)에서 파일이 들어있는 필드 이름을 지정한다. 즉, 클라이언트가 파일을 전송할 때 필드 이름으로 image를 사용해야 한다.

posts.service.ts 의 createPost 수정

동작 방식

클라이언트가 파일을 업로드하면, FileInterceptor가 해당 파일을 Multer를 통해 처리하고, 파일을 지정된 스토리지에 저장한다.

@UploadedFile() file?: Express.Multer.File

이 데코레이터는 인터셉터가 처리한 업로드된 파일을 컨트롤러 메서드의 파라미터로 주입한다.

Express.Multer.File 타입으로, 파일의 메타데이터(예: originalname, filename, mimetype, size 등)를 포함하는 객체이다.

posts.service.ts 의 createPost 수정

  async createPost(authorId: number, postDTO: CreatePostDto, image?: string) {
    // 1) create -> 저장할 객체를 생성한다.
    // 2) save -> 객체를 저장한다 (create 메서드로 생성한 객체로)

    const post = this.postsRepository.create({
      author: {
        id: authorId,
      },
      ...postDTO,
      image,
      likeCount: 0,
      commentCount: 0,
    });

    const newPost = await this.postsRepository.save(post);

    return newPost;
  }

매개변수로 image를 받았으니, 이것을 Repository가 DB에 생성할 수 있도록 수정한다.

스태틱 파일 서빙하기

스태틱 파일 서빙이란 스태틱 파일 서빙은 서버에 저장된 정적 자원(예: 이미지, CSS, JavaScript 파일 등)을 클라이언트가 HTTP 요청을 통해 쉽게 접근할 수 있도록 해주는 기능이다.

사용자가 글쓰기에서 이미지를 업로드하면, 서버는 해당 이미지를 파일 시스템이나 클라우드 스토리지 등 어딘가에 저장한다. 이후, 사용자가 혹은 다른 클라이언트가 그 이미지를 확인할 수 있도록, 서버는 해당 파일에 접근할 수 있는 고정된 URL(예: /uploads/images/abc123.jpg)을 제공한다.

패키지 파일 설치

yarn add @nestjs/serve-static

// 혹은

npm i @nestjs/serve-static

app.module.ts 에 ServeStaticModule.forRoot 추가하기

@Module({
  imports: [
    ServeStaticModule.forRoot({
      // xxx.jpg
      // rootPath만 있을 시 -> http://localhost:3000/posts/xxx.jpg
      rootPath: PUBLIC_FOLDER_PATH,
      // serveRoot를 추가 시 -> http://localhost:3000/public/posts/xxx.jpg
      serveRoot: '/public',
    }),

스태틱 파일을 서빙하기 위해선 app.module.ts의 Module에 ServerStaticModule을 등록해야한다.

Class Transformer 이용해서 URL에 prefix 추가해주기

  @Column({
    nullable: true,
  })
  @Transform(({ value }: { value: unknown }): string | undefined => {
    if (typeof value === 'string') {
      // value가 string이면 경로 문자열 반환, 그렇지 않으면 undefined 반환
      return `/${join(POST_PUBLIC_IMAGE_PATH, value)}`;
    }
    return undefined;
  })
  image: string;

@Transform은 객체의 특정 프로퍼티 값이 직렬화되거나 역직렬화될 때, 원하는 형식으로 가공할 수 있도록 도와준다. 예를 들어, 데이터베이스에서 가져온 값을 클라이언트에게 반환하기 전 URL 경로 형식으로 변경하는 경우에 유용하다.

 

예를 들어 image에 xxx.jpg라면 @Transform 데코레이터에 의해 /public/posts/xxx.jpg가 된다.

 

최종 결과

'NestJS' 카테고리의 다른 글

[NestJS] Transaction  (0) 2025.03.10
[NestJS] 파일 업로드 (선 업로드 방식)  (0) 2025.03.07
[NestJS] Config 모듈 사용하기  (0) 2025.03.05
NestJS - Pagination 심화 (일반화하기)  (0) 2025.03.05
NetJS - Pagination 기본  (0) 2025.03.02

최근댓글

최근글

skin by © 2024 ttuttak