동기적으로 이루어지지 않는 이미지 업로드에 대한 고민

YLYLYL·2022년 8월 28일
0
post-thumbnail

만들고자 하는 것

  1. 데이터를 들고 controller의 createClub()에 접근한다
  2. 데이터중 image를 storage와 db에 생성 및 업로드하고, 생성된 객체의 id들을 돌려준다.
  3. 2번에서 받은 imageIds와 나머지 데이터는 club DB에 생성한다. (연결하기 위해)

발생한 문제

1-2-3번이 동기적으로 실행되어야하는데,
작업이 조금 걸리는 2번이 어째서인지 동기적으로 실행하지 못하여 3번이 받는 2번의 리턴값은 빈 배열이다.

코드

//controller.ts
  @Post()
  async createClub(
    @UploadedFiles() files: File[],
    @Body() createClubDto: CreateClubDto,
  ) {
    let imageIds = [];
    if (files.length > 0) {
      imageIds = await this.imageService.uploadImageOnClub(files);
    }
    console.log('[2] imageIds' + imageIds);
    const createdClub = await this.clubService.createClub(
      createClubDto,
      imageIds,
    );
    return await this.findClubById(createdClub.id);
  }
 // service.ts
  async uploadImageOnClub(images: File[], clubId?: number): Promise<number[]> {
    let imageIds: number[] = [];
    if (images.length !== 0) {
      //현 이미지 업로드
      images.map(async (image) => {
        const file = await this.cloudStorageService.uploadFile(image, '');

        const createdImage = await this.prisma.image.create({
          data: {
            imageUrl: file.publicUrl,
            imageName: file.name,
            type: 'CLUB',
          },
        });
        console.log('[0] createdImage', createdImage.id);
        imageIds.push(createdImage.id);
      });
    } else {
      throw new BadRequestException(`Image did not transferred`);
    }
    console.log('[1] imageIds' + imageIds);
    return imageIds;
  }

내가 원하는 결과는 0 > 1 > 2 순서대로 출력되는 것을 기대하였다

  • 0 : storage와 image DB에 이미지들이 Create 된 후 해당 각각의 id 출력
  • 1 : 위 저장된 이미지들의 id 배열 출력
  • 2 : id배열이 잘 받아졌는지 확인

하지만 실제 결과는 위 사진과 같이 1 > 2 > 0 으로 출력되어 [1]과 [2]의 배열에는 숫자25가 들어가지 않은! 빈 배열이 들어가게 되어 결과에 영향을 끼친다.

왜 그럴까?

  • 왜 빈 배열이 출력될까?
    • 동기적으로 실행되지 않아 이미지 업로드 되는 동안 빈 결과값을 그대로 반환하였다.
  • 왜 동기적으로 실행되지 않았을까? 의심되는 부분은 어디인가?
    • 여기서 약간 막혔다. 모두 async/await을 적용해주었는데 왜 그럴까?
      1) imageIds 에 빈 배열을 선언해주어서? ▶ 하지만 이 변수는 동기적인 흐름과는 전혀 관계가 없다..
      2) map() 이 동기적인 역할을 하지 못해서? ▶ 잘 모르겠지만 알아 볼 필요가 있다.

문제 접근

  • 그렇다면 map() 을 쓴 이유는 무엇인가?
    • 받은 이미지들을 하나씩 받아서 저장소에 업로드하기 위함이다.
  • for 문도 위의 역할이 가능한데 왜 map을 사용하였는가?
    • for과 map이 같은 역할을 하고 있다고 생각하여 for문은 고안하고 있지 않았다. ▶ 이 대답은 정말 치명적인 문제인 것 같다.
  • map() 의 역할은 무엇인가? for 과의 차이점은?
    • 두 메소드 모두 반복문 기능을 가진다.
      for문은 반환값이 없고, map새로운 배열값을 반환(return) 한다
  • 그렇다면 createdData의 id값을 배열로 받으면 되지 않는가?
    • 해보자
    //현 이미지 업로드
       const imageId = images.map(async (image) : Promise<number>=> {
        ...
        const data = await this.prisma.image.create({
          data: {
           ...
          },
        });
        return data.id;
      });
      console.log('i',imageId)
결과값은 오른쪽과 같았다.

Promise 자체가 리턴된다.
왜 그런가, 찾아봤더니 map 안에서 async/await 함수가 들어갈 경우, Promise의 배열을 반환한다고 한다.

즉 for문을 사용하던가, map을 쓰겠다면 then으로 받아주던가..


지나가도 되는 for문, promise 테스트 해보기

그렇다면 for과 Promise 에 초점을 잡고, 테스트를 해보았다.

promise에 대한 글은 이 포스팅에서 이해하는데 도움을 많이 받았다.

1. promise 비동기 객체(성공,실패) 만들기

const generatePromise = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`generate Promise`)
    }, 1000)
  })
}

function generateError() {
  return new Promise((resolve, reject) => {
    setTimeout(() => { reject("[X] Error promise") }, 1000);
  })
}

generatePromise().then((x) => console.log(x));
console.log("[1] start");

결과는 내 예상대로 1 > promise 출력되었다

2. for문으로 promise 객체 받기

하나씩 돌리면서 promise 객체를 받아보자 (시간도 같이 재기)
for문 안에서 도는 것 또한 동기적으로 하나씩 돌고 있는데, all을 통해 promise 작업을 병렬적으로 받을 수 있다.

3. Promise.all로 한번에 promise 객체 받기

  • 생성 객체만 받기
    all을 이용해 한번에 돌리니까 속도도 줄었다.
  • 에러객체도 받기
    단, 에러가 하나라도 발생한다면 그 즉시 catch로 빠진다.

4. Promise.allSettled

allSettled는 실패한 작업이 있어도 무조건 실행하고, 구분된 반환값을 내뱉는다.


해결 방법

1. for of

위 테스트한 내용을 기반으로 map 대신 for..of를 사용했다.


  async uploadImageOnClub(clubId: number , images: File[]){
    const imageIds: number[] = []; 
      //현 이미지 업로드
      for(let image of images){ //map 대신 for..of
        const file = await this.cloudStorageService.uploadFile(image, '');

        const data = await this.prisma.image.create({
          data: {
            imageUrl: file.publicUrl,
            imageName: file.name,
            type: 'CLUB',
            ClubImage: {
              create: {
                clubId
              }
            }
          },
        });
        console.log('[0] createdImage', data.id);
        imageIds.push(data.id);
      }
      console.log('[1] imageIds' + imageIds);
      return imageIds;

  }

결과는!!! 0 > 1 순서대로 성공!!!

그런데 시간이 꽤 걸린다

2. Promise.allSettled

이번엔 allSettled를 활용하여 코드를 작성해보았다.


  async uploadImageOnClub(clubId: number , images: File[]){
     //현 이미지 업로드
     const upload: number[] = await this.saveClubImage(clubId, images); //하단
     const result = await Promise.allSettled(upload)
     const imageIds = result.filter((f) => f.status === 'fulfilled');
    
     return imageIds;

  }

  async saveClubImage(clubId: number, images: File[]) {
    const imageIds: number[] = [];
    for (const image of images) {
      const file = await this.cloudStorageService.uploadFile(image, '');

      const data = await this.prisma.image.create({
        data: {
          imageUrl: file.publicUrl,
          imageName: file.name,
          type: 'CLUB',
          ClubImage: {
            create: {
              clubId,
            },
          },
        },
      });
      imageIds.push(data.id);
      return imageIds;
    }
  }


1번과 동일한 파일을 업로드하였는데, 1번의 상황보다는 시간이 1/7보다도 적게 소모된다!!!!!!!

회고

이 문제를 맞닥뜨리고 원인을 찾았을때, 나의 부족함도 그대로 마주했다..
js의 기본문법을 제대로 이해하고 있었다면, map안에서 async를 사용할 경우 Promise를 반환해 준다는 사실은 금방 알 수 있었을 것 같은 문제였다.

또한, 이 글은 2-3일 정도 혼자 끙끙 앓다가 선배에게 물어보며 힌트를 얻고,
인프콘 강연 및 여러 게시글들을 보며 에러를 해결하기 위해 생각해야할 것, 에러 해결 후 글 쓰는 법을 조금은 터득한 후 처음 쓴 글이었다.
이 전 블로그에서는 단순히 에러발생 현상과 결과만 작성하였는데
왜?라는 질문을 하며 짚어보니 혼자 끙끙 앓던 2-3일보다 훨씬 쉽게 원인에 닿을 수 있었다.

하.. 이 문제 해결 후에 도서관에서 자바스크립트 코딩의 기술 책을 빌려왔다.

0개의 댓글