Today I learned
기존의 이미지를 업로드하는 방식은 단일 이미지만 업로드 할 수 있었다
우리 프로젝트는 사진과 장소가 중요하다 그렇다면 단일 이미지로만 만족할 수 없다
그래서 여러장의 이미지를 업로드 할 수 있게 작업을 진행한다
작업 중 문제가 발생했다 지금 이미지를 업로드를 할 수 있게 사용하는 패키지는 nestjs-form-data 패키지를 사용하고 있다
DTO에 vaildation도 할 수 있고 아주 유용하게 사용했다
하지만 여러가지 파일을 받으려고 하니까 쉽게 풀리지 않았다 파일들이 배열이 아닌 객체로 겹쳐서 들어온다거나 파일을 못찾는다거나 문제가 발생했다
그래서 방법은 패키지를 바꾸자! 나는 기존에 사용했던 multer를 사용하기로 했다
npm install --save-dev @types/multer
multer는 express에서 자주 사용하던 이미지를 업로드할 때 사용하던 패키지다
이 패키지는 nestjs에서 기본 패키지로 사용할 수 있는데 사용하려면 types/multer를 깔아줘야한다
multer를 이용해서 이미지를 받아오는건 성공을 했다
하지만 파일을 validation하고 싶었다 validation 때문에 nestjs-form-data를 사용했는데 multer은 DTO에 vaildation 하는 방법을 찾지 못했다 나는 vaildation 방법으로 나는 Pipe를 사용하기로 했다
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';
import * as _ from 'lodash'
@Injectable()
export class FileVaildationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
const imageType = ['IMAGE/PNG', 'IMAGE/JPEG', 'IMAGE/JPG'];
if (_.isNil(value)) {
throw new BadRequestException('이미지 파일을 입력하셔야 합니다.');
}
if (value.length > 5) {
throw new BadRequestException('이미지 파일은 최대 5장만 가능합니다.');
}
value.forEach((val: any) => {
if (!imageType.includes(val.mimetype.toUpperCase())) {
throw new BadRequestException('이미지 파일만 업로드 할 수 있습니다.');
}
})
return value;
}
}
이런식으로 파일의 타입과 최대 이미지 개수 그리고 파일이 들어있는지를 체크하게 만들었다
컨트롤러로 가보자
@Put('/:photospotId')
@UserGuard
@UseInterceptors(FilesInterceptor('images'))
async modifyPhotospot(
@UploadedFile(FileVaildationPipe) images: Express.Multer.File[],
@Body() modifyPhotospot: ModifyPhotospotDto,
@Param('photospotId') photospotId: number,
@InjectUser('id') userId: number
): Promise<void> {
await this.photospotService.modifyPhotospot(modifyPhotospot, photospotId, userId);
}
그럼 이제 여러장의 이미지를 photospot과 관계가 설정되어있는 photo 테이블에 넣어주기만 하면 된다
서비스로 가보자
try {
const { title, description, latitude, longitude }: CreatePhotospotDto = createPhtospotDto;
const photospot = await queryRunner.manager
.getRepository(Photospot)
.insert({ title, description, latitude, longitude, userId, collectionId });
for (const file of files) {
try {
const image = await this.s3Service.putObject(file);
await queryRunner.manager.getRepository(Photo).insert({ image, userId, photospotId: photospot.identifiers[0].id });
} catch (error) {
console.log(error);
throw new Error('Photo 입력 실패.');
}
}
이런식으로 photo에 데이터를 입력할 때 photospot의 아이디가 필요해서 id를 반환해주는 insert를 이용해서 id를 반환받아서 이미지의 개수만큼 for문을 돌려서 photo에 데이터를 입력하는 방식으로 진행했다
이 방법에서는 하다가 문제가 발생했다
혹시 photospot은 입력이 되었는데 photo에서 에러가 발생하면 photospot은 존재하지만 photo는 존재하지 않게 되어버린다 데이터 무결성이 망가져 버린다고 생각했다 그래서 트랜잭션을 적용했다
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const { title, description, latitude, longitude }: CreatePhotospotDto = createPhtospotDto;
const photospot = await queryRunner.manager
.getRepository(Photospot)
.insert({ title, description, latitude, longitude, userId, collectionId });
for (const file of files) {
try {
const image = await this.s3Service.putObject(file);
await queryRunner.manager.getRepository(Photo).insert({ image, userId, photospotId: photospot.identifiers[0].id });
} catch (error) {
console.log(error);
throw new Error('Photo 입력 실패.');
}
}
await queryRunner.commitTransaction();
} catch (error) {
console.log(error);
await queryRunner.rollbackTransaction();
throw new BadRequestException('요청이 올바르지 않습니다.');
} finally {
await queryRunner.release();
}
이런식으로 적용하니 둘 다 정상적으로 작동해야지 테이블에 정상적으로 입력이 되도록 진행했다
이 코드도 계속 보다 보니까 불만족 스러웠다 typeorm을 너무 활용안하는 느낌이 나고 Sequelize를 사용할 때 분명 관계를 생성해뒀으면 관계가 걸려있는 참조 테이블 하나만 사용하더라도 데이터를 잘 넣을 수 있었다
typeorm도 같은 ORM이다 분명 방법이 있을거라고 생각했다
방법을 찾고 코드를 수정해보자
const { title, description, latitude, longitude }: CreatePhotospotDto = createPhtospotDto;
const images = await files.map(async(file) => await this.s3Service.putObject(file));
await this.photospotRepository.insert({ title, description, latitude, longitude, userId, collectionId, photos: images.map((image) => ({
userId,
image,
})), });
이 방식으로 보면 엔티티에 작성했던 photos에다가 Photo[]의 형식으로 넣어주는 형식으로 진행하려고 했는데 문제가 생겼다 images가 Promise<string[]>로 반환이 되어서 문제가 생겼다
문제를 해결해보자
const { title, description, latitude, longitude }: CreatePhotospotDto = createPhtospotDto;
const images = await Promise.all(files.map((file) => this.s3Service.putObject(file)));
await this.photospotRepository.insert({ title, description, latitude, longitude, userId, collectionId, photos: images.map((image) => ({
userId,
image,
})), });
Promise.all을 사용하면 [promise1, promise2, promise3]배열안의 프로미스들은 비동기 적으로 처리한다 그리고 반환되는 값들은 await를 거치면서 Promise를 빠져나와서 Promise<string[]>가 string[]으로 반환이 된다 그러면 해당 코드가 정상적으로 동작한다!
오늘 배운 점은 Promise.all로 Promise들을 비동기 처리할 수 있는 방법과 typeorm의 관계가 형성되어있는 테이블들을 다루는법 그리고 트랜잭션을 배웠고 여러 이미지를 저장하는 법을 배웠다
참고자료
'과거공부모음' 카테고리의 다른 글
20230324 TIL - 사진 리사이징 (0) | 2023.03.25 |
---|---|
20230321 TIL - 구글 비전을 이용한 이미지 라벨링 (0) | 2023.03.21 |
20230308 TIL - seed data만들기 (0) | 2023.03.09 |
20230307 TIL - github에 민감한 파일 올렸다 (0) | 2023.03.07 |
20230306 TIL - S3 파일 업로드 문제 (0) | 2023.03.06 |