포시코딩

[Nest.js] FormData 전달받기 with multer 본문

Node.js

[Nest.js] FormData 전달받기 with multer

포시 2023. 2. 22. 04:16
728x90

개요

string, number type의 데이터와 이미지 파일이 같이 담긴 FormData에 대해

Nest.js 상에서 어떻게 해야 

multer를 통해 이미지 파일을 받고

나머지 데이터들에 대해 DTO를 적용해 type을 가려 받을 수 있는지에 대해

내 시행착오를 정리해보았다.

 

발단

 

Client Server

const formData = new FormData();
formData.append('title', title);
formData.append('content', content);
formData.append('writerId', writerId);
formData.append('joinLimit', joinLimit);
formData.append('file', file);
axios.post('http://localhost:8080/boards', formData)
// ...생략

이런 형태로 Nest.js 서버에 POST 요청하는 코드가 있었는데

 

Controller

  @Post()
  createBoard(
    @Body() boardData,
  ) {
    console.log('boardData: ');
    console.log(boardData);
    return;
  }

처음에는 이런 형태로 받으려고 하니

아예 제대로 받지 못하는 상황이 생겼다.

 

시행착오

nestjs-form-data

일단 FormData를 받는 게 우선순위였기에

먼저 Nest.js에서 FormData를 정상적으로 받아 위에서 나온 {}가 아닌

Object 형태로 받게끔 하는 방법을 찾아보았다.

 

그 과정에서 nestjs-form-data 라는 패키지를 발견해 사용해 보았다.

사용과정 하단 참고

https://4sii.tistory.com/430

 

2월21일 - nestjs-form-data

개요 React에서 Nest.js 서버로 axios를 통해 글쓰기 요청을 보내는 과정에서 이미지 파일도 같이 보낼걸 생각해 원래 axios .post('http://localhost:8080/boards', { title, content, }) 이렇게 보냈다면 formData를 써서

4sii.tistory.com

 

사용 결과

사용 결과 겉보기에는 아주 성공적이었다. 

 

Controller

  @Post()
  @FormDataRequest()
  createBoard(
    @Body() boardData: CreateBoardDto,
  ) {
    console.log('boardData: ');
    console.log(boardData);
    return;
  }

다만 위처럼 Body에 DTO를 type으로 적용 시 받지 못하는 문제가 생겼고

DTO에  file 타입을 넣기도 애매하고 추후 붙이게 될 S3라던지

multer를 사용해 활용하고 싶은 마음이 컸다.

 

그래서 일단 nestjs-form-data 패키지는 아쉽지만 안 쓰는 방향으로 생각해 보기로 했다.

 

multer 사용

코드

multer.options.factory.ts

import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
import { memoryStorage } from 'multer';

export const multerOptionsFactory = (): MulterOptions => {
  return {
    storage: memoryStorage(),
    limits: { fileSize: 10 * 1024 * 1024 }, // 10MB로 크기를 제한
  };
};

Module

// ...import 생략
@Module({
  imports: [
    MulterModule.registerAsync({
      useFactory: multerOptionsFactory,
    }),
  ],
  controllers: [BoardsController],
  providers: [BoardsService]
})
export class BoardsModule {}

Controller

  @Post()
  @UseInterceptors(FileInterceptor('file'))
  createBoard(
    @Body() boardData,
    @UploadedFile() file: Express.Multer.File,
  ) {
    console.log('boardData: ');
    console.log(boardData);
    console.log('file: ');
    console.log(file);
    return;
  }

 

사용 결과

사용 결과는 대성공이었다.

해당 결과까지의 과정이 순탄치는 않았지만 정리할 거리도 없기에 생략

아마 multer 사용 방법이 다른 블로그 글들과 좀 다를 텐데

service에 데이터를 넘겨 저장하다 실패가 나도 이미지는 multer로 인해 저장 상태 그대로 남게 되는 경우가 있어

아예 service에서 성공적으로 데이터 저장 시 이미지를 저장하게끔 하기 위해 이렇게 만들었다.

 

하지만 문제가 없는 건 아니었다.

writerId, joinLimit은 number type으로 받아야 되는 값들인데

FormData에 담겨 넘어와서 그런지 string으로 변환되어 있었다.

 

위 코드는 일단 multer를 사용했을 때 데이터가 잘 넘어오는지 확인하느라

boardData에 대해 dto를 type으로 지정해놓지 않은 상태였고

지정하면 타입이 맞지 않아 튕겨내고 있는 상황.

 

class-transformer를 사용하고 있었지만 FormData 내부의 값들까지 변환해주지는 못하는 모양이었다.

여기서 또 한참을 헤매고 있었다.

 

시행착오 2

CreateBoardDto

// ...import 생략
export class CreateBoardDto {
  @IsString()
  readonly title: string;
  
  @IsString()
  readonly content: string;
  
  @IsNumber()
  readonly writerId: number;

  @IsNumber()
  readonly joinLimit: number;
}

원래 사용하고 있던 dto의 코드다. 

controller에서 해당 dto를 적용하면 writerId, joinLimit은 number여야 하기 때문에 튕겨내는데

 

https://github.com/typestack/class-validator/blob/develop/README.md#validation-decorators

 

GitHub - typestack/class-validator: Decorator-based property validation for classes.

Decorator-based property validation for classes. Contribute to typestack/class-validator development by creating an account on GitHub.

github.com

공식 문서에서 @IsNumber() 대신

문자열의 형태가 숫자인지 확인하는 @IsNumberString()을 사용하면 될 것 같다는 힌트를 얻었고

 

적용 결과

controller에서 dto를 적용해도 데이터를 잘 넘겨받을 수 있었다.

 

이제 넘겨받긴 했으니 boardData를 가공해 쓰면 되는 일이었다.

 

문제 발생 

하지만 여기서 두 가지 문제가 발생했는데

  1. writerId는 dto에서 readonly로 선언됐기 때문에 변경할 수 없다.
  2. @IsNumberString()의 영향인지 wrtierId는 number로 취급받고 있음

2번은 내가 번역을 잘못한 거일 수도 있는 게 boardData를 출력해 보면 여전히 string 형태로 나온다.

 

어쨌든 @IsNumberString()으로도 제대로 해결할 수 없는 상황임이 분명했다.

 

해결 방법

이번엔 @IsNumberString()을 키워드 삼아 또 열심히 구글링 했는데

다행스럽게도 GitHub nestjs의 issue에 비슷한 고민에 대해 질문한 사람이 있어

답을 얻을 수 있었다.

https://github.com/nestjs/nest/issues/1331

 

Using class validator's IsNumberString along with number validators · Issue #1331 · nestjs/nest

I'm submitting a... [ ] Regression [ ] Bug report [ ] Feature request [X] Documentation issue or request [X] Support request => Please do not submit support request here, instead post your q...

github.com

 

 

dto

// ...import 생략
export class CreateBoardDto {
  @IsString()
  readonly title: string;
  
  @IsString()
  readonly content: string;
  
  @Type(() => Number)
  @IsNumber()
  readonly writerId: number;

  @Type(() => Number)
  @IsNumber()
  readonly joinLimit: number;
}

@IsNumberString() 대신 @Type()을 통해 타입 변환을 해준 뒤 @IsNumber()를 쓰는 방법이었는데

적용 결과

controller에서 원하는 형태로 데이터를 받는 모습을 확인할 수 있었다.

 

정리

express에선 아무 생각 없이 받아 썼던 부분들이

TypeScript의 사용으로 복잡해지고 어려워진 부분이 없지 않았지만 

이런 과정 덕분에 Back-End로써 좀 더 깐깐하게 요청받는 데이터들에 대해 확인할 수 있다고 생각한다.

 

그리고 처음 배울 땐 dto에서 @IsString(), @IsNumber() 말고는 안 알려줘 이 두 개만 써왔는데

이렇게 직접 부딪히며 필요성을 느끼면서 배우니 금방 잊지도 않을 거 같고 성취감도 느낄 수 있었다.

 

이제 얻은 boardData와 file에 대해서 service로 넘겨 

해당 게시글을 저장하는 일이 남았는데

이 포스팅의 주제를 벗어나는 부분이라 여기까지 하고 마치겠다.

 

 

multer 사용 관련은 아래 팀원의 코드에서 도움을 받았다.

https://github.com/muja-code/nestjs-board-app

 

GitHub - muja-code/nestjs-board-app: nestjs-board-app

nestjs-board-app. Contribute to muja-code/nestjs-board-app development by creating an account on GitHub.

github.com

728x90