본문 바로가기
NestJS

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

by Programmer.Junny 2025. 3. 7.

선 업로드 방식이란

  • 이미지를 선택할때마다 이미지는 먼저 업로드된다.
  • 업로드된 이미지들은 '임시' 폴더에 잠시 저장해둔다. (/public/temp)
  • 이미지 업로드를 한 후 응답받은 이미지의 경로만 저장해둔 후 포스트를 업로드할 때 이미지의 경로만 추가해준다.
  • POST /posts 엔드포인트에 이미지 경로를 함께 보낼 경우 해당 이미지를 임시폴더(/public/temp)에서 포스트폴더(/public/posts)로 이동시킨다.
  • PostsModel의 image 필드에 경로를 추가해준다.
  • S3 presigned url을 사용하면 많이 사용되는 방식이다.
  기존 방식 신규 방식(선 업로드)
체감 속도 모든 이미지가 업로드되는 시간을 사용자가 처음부터 끝까지 느껴야하므로 사용자는 서비스가 느리다고 생각하게 된다. 이미지를 선택하는 순간 서버에 업로드가 진행되기 때문에 사용자는 서비스가 빠르다고 느낌
서버 과부하 포스팅(업로드) 버튼을 누를 때만 요청이 보내지기 때문에 과부하가 적음 이미지 선택마다 요청이 보내지기 때문에 과부하가 많음
엔드포인트 관리 파일을 업로드하는 엔드포인트가 생길때마다 업로드관련 multer를 세팅해줘야한다. 공통된 이미지 업로드 엔드포인트 하나를 만들어서 모든 이미지 업로드를 관리한다.
파일 관리 포스팅될 때의 이미지들만 업로드되기 때문에 잉여파일 생길 가능성이 적다. 이미지를 선택하면 임시폴더가 생성되고 업로드되기 때문에 잉여파일이 생성될 수 있다.

common.module.ts 로 multer 세팅 옮기기

import { BadRequestException, Module } from '@nestjs/common';
import { CommonService } from './common.service';
import { CommonController } from './common.controller';
import { MulterModule } from '@nestjs/platform-express';
import { extname } from 'path';
import * as multer from 'multer';
import {v4 as uuid} from 'uuid';
import { existsSync, mkdirSync } from 'fs';
import { TEMP_FOLDER_PATH } from './const/path.const';
import { AuthModule } from 'src/auth/auth.module';
import { UsersModule } from 'src/users/users.module';

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

@Module({
  imports: [
    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, TEMP_FOLDER_PATH);
        },
        filename: function(req, file, cb) {
          cb(null, `${uuid()}${extname(file.originalname)}`)
        }
      }),
    }),
    AuthModule,
    UsersModule,
  ],
  controllers: [CommonController],
  providers: [CommonService],
  exports: [CommonService],
})
export class CommonModule {}

기존 posts.module.ts 에 있던 multer 세팅common.module.ts 의 imports로 이동한다. 추가로 AccessGuard를 사용하여야 하기 때문에 AuthModuleUsersModule도 같이 imports에 넣어준다.

common.controller.ts 에 image 업로드하는 Controller 만들기

import { Controller, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common';
import { CommonService } from './common.service';
import { FileInterceptor } from '@nestjs/platform-express';
import { AccessTokenGuard } from 'src/auth/guard/bearer-token.guard';

@Controller('common')
export class CommonController {
  constructor(private readonly commonService: CommonService) {}

  @Post('image')
  @UseGuards(AccessTokenGuard)
  @UseInterceptors(FileInterceptor('image'))
  postImage(
    @UploadedFile() file: Express.Multer.File,
  ) {
    return {
      fileName: file.filename,
    }
  }
}

위의 컨트롤러를 Postman에서 요청을 보내면 잘 실행되는 것을 볼 수 있다.

POST posts 엔드포인트 변경하기

dto에서 image 경로를 받고, /public/temp에 있는 이미지를 /public/posts 로 옮기는 과정이 필요하다.

CreatePostDto 수정하기

import { PickType } from "@nestjs/mapped-types";
import { PostsModel } from "../entities/posts.entity";
import { IsOptional, IsString } from "class-validator";

export class CreatePostDto extends PickType(PostsModel, ['title', 'content']) {
    @IsString()
    @IsOptional()
    image?: string;
}

엔티티가 생성될때 임시 폴더로부터 이미지 파일 이동시키기

async createPostImage(dto: CreatePostDto) {
    // dto의 이미지 이름을 기반으로 파일의 경로를 생성한다.
    const tempFilePath = join(
      TEMP_FOLDER_PATH,
      dto.image!,
    );

    try {
      // 파일이 존재하는지 확인. 파일이 없는 경우 에러발생.
      await promises.access(tempFilePath);
    } catch (error) {
      throw new BadRequestException(`${error}: 존재하지 않는 파일입니다.`);
    }

    // 파일 이름만 가져오기
    const fileName = basename(tempFilePath);

    // 새로 이동할 포스트 폴더 이름 + 이미지 이름
    const newPath = join(
      POST_IMAGE_PATH,
      fileName,
    );

    // 파일 옮기기
    await promises.rename(tempFilePath, newPath);

    return true;
  }

createPostImage는 클라이언트로부터 받은 이미지가 있는지 검사하고, 있는 경우 파일을 옮기는 역할을 진행한다.

postPosts 수정하기

@Post()
@UseGuards(AccessTokenGuard)
async postPosts(
@User('id') userId: number,
@Body() body: CreatePostDto,
// @Body('title') title: string,
// @Body('content') content: string,
) {
await this.postsService.createPostImage(body);

return this.postsService.createPost(userId, body);
}

포스트를 호출하는 컨트롤러에서 createPostImage를 먼저 호출하면 파일을 다 옮기고 createPost를 호출하게 된다.

/common/image로 클라이언트에서 서버로 이미지 전송
posts 시 image 필드를 넣고 호출하면 이미지가 잘 등록된다.
get 요청에서도 역시 잘 반영된 것을 볼 수 있다.
서버에서 temp -> posts로 이미지가 잘 옮겨진 것을 볼 수 있다.

'NestJS' 카테고리의 다른 글

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

최근댓글

최근글

skin by © 2024 ttuttak