day27

Antipiebse·2022년 4월 21일
0

TIL

목록 보기
17/17

오늘의 목표
: 이미지를 google cloud storage 사이즈 별로 잘라서 썸네일 이미지 만들고 storage 안에 저장하기


이미지 저장과정

프론트엔드에서 저장할 이미지를 받아와 cloud컴퓨터 안에 스토리지 서비스(디스크가 엄청 큰 가상 컴퓨터)를 이용하여 이미지를 저장한다. 이때 저장하는 이미지는 url형식(단지 문자열)으로 저장한다. 이러한 파일을 string뿐만 아니라 BLOB(Binary Large Object)을 이용해 이진형태로 파일을 저장할 순 있다.

우리는 UploadFileAPI를 만들어볼 예정이다.

Cloud Service란?

인터넷 기반의 컴퓨팅으로 인터넷 상의 가상화된 서버에 프로그램을 두고 필요할때마다 불러와 사용하는 서비스이다. 인터넷 통신망 어딘가에서 가상의 컴퓨터를 사용하는 것이라고 이해하면 편하다. 이러한 가상 컴퓨터들을 제공하는 회사들이 여럿 존재하는데 대표적으로 아마존의 AWS, MS의 MS Azure, 그리고 우리가 이번에 사용한 google의 gcp가 있다. gcp의 cloud storage를 이용할 것이다.

Google Cloud Storage

구글 클라우드 스토리지(Google Cloud Storage)는 구글 클라우드 플랫폼 인프라스트럭처에서 데이터 저장 및 접근을 위한 REST형 온라인 파일 스토리지 웹 서비스이다. 이 서비스는 구글의 클라우드의 성능과 스케일러빌리티를 고급 보안, 공유 기능과 결합한다.

Google Cloud Function

google에서 돈을 받고 제공하는 cloud Storage안에 데이터를 추출해 가공한 후 다른 곳으로 보내거나 저장하는 도중에 데이터를 가공하여 저장하는 등 다양한 기능을 직접 구현할 수 있다.(어렵다.)그럼 우리가 사용할 gcp에 대해 아아아ㅏ아아주 간단하게 알아보았다. 그럼 이렇게 데이터를 저장할 때 사용할 문법인 promise와 promise.all에 대해 알아보자.

이미지 저장과 이미지 업로드를 기다리기 위한 promise

Cloud Storage안에 데이터를 저장하는 과정은 데이터 크기에 따라 다르겠지만 저장하는데 일정 시간이 필요하다. 이를 async await만으로 처리한다고 가정했을 때 a_file은 저장하는데 3초, b_file은 2초, c_file은 1초가 걸린다고 하자.

//async await promise만을 이용
a_file =====저장 중===> 3초 경과
그 다음 b_file 저장 시작
b_file =====저장 중===> 2초 경과
그 다음 c_file 저장 시작
c_file =====저장 중===> 1초 경과
>총 6초 경과

함수로 구현해보자.

/// async로 개별 promise!
const fetchData = async () =>{
  console.time("=== 개별 Promise 각각 ===")
  const result1 = await new Promise((resolve, reject)=>{
    // 특정한 작업(api보내기 등등)
    setTimeout(()=>{
        resolve("성공시 받는 데이터")
    },2000)
  })
  const result2 = await new Promise((resolve, reject)=>{
    // 특정한 작업(api보내기 등등)
    setTimeout(()=>{
        resolve("성공시 받는 데이터")
    },3000)
  })
  const result3 = await new Promise((resolve, reject)=>{
    // 특정한 작업(api보내기 등등)
    setTimeout(()=>{
        resolve("성공시 받는 데이터")
    },1000)
  })
  console.timeEnd("=== 개별 Promise 각각 ===")
}

그럼 이렇게 하나 저장될 때까지 기다리고 그 다음 파일 저장될 때까지 기다리고 이런 방식 밖에 없는 걸까?

promise.all을 통해 각각의 요청을 동시에 보내 이러한 시간을 줄일 수 있다.

//async await promise promise.all 이용
a_file =====저장 중===> 3초 경과
b_file =====저장 중===> 2초 경과
c_file =====저장 중===> 1초 경과
>총 3초 경과

함수로 구현해보자

const fetchData2 = async ()=>{
  console.time("=== 한방에 Promise.all ===")

  await Promise.all([
    new Promise((resolve, reject)=>{
      setTimeout(()=>{
          resolve("성공시 받는 데이터")
      },2000)
    }),
    new Promise((resolve, reject)=>{
    setTimeout(()=>{
        resolve("성공시 받는 데이터")
    },3000)
    }),
    new Promise((resolve, reject)=>{
    setTimeout(()=>{
        resolve("성공시 받는 데이터")
    },1000)
    })
  ])//배열 속의 promise들을 한 번에 실행시킨다. axios요청도 여러개 한 번에 보내기 가능!
  console.timeEnd("=== 한방에 Promise.all ===")
}

여러 번 요청을 보내 각각의 시간을 소비하는 것보단 한 번에 요청을 보내놓고 기다리는 시간이 더 짧으므로 promise.all을 적절히 사용해보자!!

gcp bucket?

cloud storage를 사용하기 위해선 버킷을 만들어야한다. 친절하게 설명이 되어있는 블로그를 참조한다.

우선 gcp bucket안에 이미지를 업로드!

bucket안에 이미지를 넣기 위한 함수를 작성해보자. bucket을 만들고 서비스 계정을 만들면 우리의 계정 정보가 담긴 json파일을 다운 받을 수 있다.

이 json 파일은 절대로 github에 업로드 하지말자

왜냐하면 이를 통해 비트코인 채굴을 하거나 개인 정보가 다른 곳으로 도용될 수 있기 때문이다. 꼭 gitignore설정을 하자.

그럼 설정을 끝내고 소스코드를 nest패턴에 맞춰 만들어보자.

// file.module.ts
import { Module } from '@nestjs/common';
import { FileResolver } from './file.resolver';
import { FileService } from './file.service';
@Module({
  providers:[
    FileResolver,
    FileService,
  ]
})
export class FileModule{}
// file.resolver.ts
import { FileService,  } from './file.service';
import { Args, Resolver , Mutation} from "@nestjs/graphql";
// import {FileUpload, GraphQLUpload} from 'graphql-upload'
import {FileUpload, GraphQLUpload} from 'graphql-upload'
@Resolver()
export class FileResolver{
  constructor(
    private readonly fileService:FileService
  ){

  }
  //단순 이미지 업로드
  // 브라우저에서 백엔드 api로 전송할 땐 graphqlUpload, 
  // 백엔드 안에선 fileUpload형식으로 사용!
  @Mutation(()=>[String])
   async uploadFile(
    @Args({name:'files', type:()=>[GraphQLUpload]}) files: FileUpload[],
  ) {
    return await this.fileService.upload({files})
  }
}
// file.service.ts
import { FileUpload } from 'graphql-upload';
import { Injectable } from "@nestjs/common";
import { Storage } from '@google-cloud/storage'

//타입스크립트에서 타입을 지정해주기 위해 사용!
interface IFile{
  files: FileUpload[]
}

@Injectable()
export class FileService{
  async upload({files}:IFile){
    const storage = new Storage({
      projectId: 	"back01-347705",//우리의 프로젝트 id
      keyFilename:"back01-347705-14996fc5f866.json", //keyfilename
    }).bucket("codecamp-file-storage-sungmin")//저장할 장소
    
    // 일단 먼저 프론트엔드로 부터 저장할 데이터 다 받아오기
    const waitedFiles = await Promise.all(files)
    
    //여러 장 보낼때
    const results = await Promise.all(waitedFiles.map((el)=>{
      return new Promise((resolve, reject)=>{
        el.createReadStream()//파일을 읽어드리는 함수, 실행시켜야 파일이 읽어짐!
          .pipe(storage.file(el.filename).createWriteStream()) //파일을 읽고 어떤 작업을 할 지 정함(업로드, 사이즈 변경 등등)
          .on("finish", ()=> resolve(`codecamp-file-storage-sungmin/${el.filename}`)) //성공시
          .on("error", (error: Error)=> reject(error))//에러발생시 
     })
    }))
    //배열 안에 url이 들어가 있는 상태로 리턴!
    return results
  }
}

사진을 업로드할 준비는 완료됐다.

그럼 gcp function은 어떤 식으로 작성해야할까?

위 사진에 잘 보면 event type이 완료/생성을 선택하여 함수를 만들었다. 뜻은 한 번 함수 내에서 어떠한 로직이 완료되거나 생성될 경우 계속해서 이벤트를 실행하는 것인데 이를 통해 여러 번 함수를 실행할 수 있다. 그러나 트리거(함수)가 무한 반복하는 일이 발생할 수도 있는데 이를 잘 처리하기 위해 재귀함수를 사용하듯이 종료 조건을 잘 명시해주어야한다.

/**
 * Triggered from a change to a Cloud Storage bucket.
 *
 * @param {!Object} event Event payload.
 * @param {!Object} context Metadata for the event.
 */
 const sharp = require('sharp');//이유는 잘 모르지만 import가 잘 안 먹는다.
 const {Storage} = require('@google-cloud/storage')
 
 exports.thumbnailGCS = async (event, context) => {
   const storage = new Storage().bucket(event.bucket)
   const Eventname = event.name
   const url = storage.file(Eventname)
   const size = [["s",320], ["m",640],["l",1280]]
 
   //만약 썸네일 폴더 안에 이미지가 존재 한다면? 정지
   if(Eventname.includes('thumb/')) return 
 
   await Promise.all(size.map(([folder,withSize])=>{
     new Promise((resolve, reject)=>{
       url
       .createReadStream()//파일을 읽어드리는 함수, 실행시켜야 파일이 읽어짐!
       .pipe(sharp().resize(withSize))//sharp라는 라이브러리를 통해 이미지 자르기
       .pipe(storage.file(`thumb/${folder}/${Eventname}`).createWriteStream())//자른 파일을 사이즈별로 나눠 저장
       .on("finish", ()=> resolve(`썸네일 이미지 생성 완료!!`)) //성공시
       .on("error", (error)=> reject("에러가 발생했습니다:", error))//에러발생시 
     })
   }))
 } 

사진 업로드 및 저장한 이미지 접근하는 방법

우리는 따로 프론트엔드 부분을 만들지 않았으므로 파일을 실행해 postman으로 확인해야한다.

포스트맨에서 요청을 보낸다.

GCP에 접근하여 아까 만든 버킷에 들어간다.

요청후 응답된 파일의 이름을 긁어와 storage.googleapis.com + 파일명의 형식으로 접근한다.
https://storage.googleapis.com/codecamp-file-storage-sungmin/%EB%AC%B4%EC%8B%A0%EC%82%AC%20%EC%82%AC%EC%A7%84.png
위 링크로 들어가면 업로드된 결과 중 한 사진을 확인할 수 있다.


마치며

와 지ㅏㄴ짜 어렵다... ㅋㅊㅋㅌ

profile
백엔드 주니어 개발자

0개의 댓글