GCP, Cloud functions으로 Trigger 만들기

·2022년 4월 20일
4

TIL

목록 보기
17/36
post-thumbnail

코드캠프를 6주간 하는 동안, 지금까지는 어려운게 없었다고 생각을 했고
과제보다는 내가 해보고 싶은 것을 구현하는 것에 목적을 두면서 수업을 따라가고 있었다.

그러던 와중 이미지를 업로드하는 것에 대한 것을 배우게 되었고, 그것의 과제로
한개의 사진을 올렸을 경우 여러가지의 크기로 변환하는 작업을 하는 것을 받게 됐는데

아, 더럽게 고생했고 정말 고통스러웠기 때문에 미래의 나를 위해서라도 올려두는 글이다.

웹사이트에 사진이 없는게 말이 된다고 생각해?

모두가 같은 생각을 할 것이다.
그렇다는 것은 기본적인 시스템 이라는 것이고, 자료 또한 엄청 많다고 생각해서
과제를 받고 금방 하겠지. 라는 생각을 했다.

근데 웬걸, 자료가 더럽게 없었다.

나 혼자만 못했으면 내 실력이겠거니 했는데
모든 인원이 다 찾는데도 안나오는 것을 보면 아마도..... (진짜 별로 안쓰나?)

일단 AWS 쪽의 자료가 엄청 많이 쏟아졌고, google이지만 firebase를 이용한 것들이 무작위로 쏟아져나왔다.

내가 필요한 것은 sharp를 써서 만드는 것이였는데
이상하게 sharp도 공식문서가 제대로 없고(.....) 뭔가 설명이 엄청 빈약해서 고생이란 고생은 다 한 것 같다.

아무튼 그래서 파일 업로드란?

일단 파일이라는게 뭔지부터 생각을 해보자.
우리는 지금까지 숫자, 문자같은 것을 담아서 작업을 했지만
파일(이미지)같은 것들은 분명 그것도 0101010101인건 달라지지 않겠지만 특정한 형태를 가지고 있을 것이라는건데

그게 바로 buffer 라는 곳에 담겨져서 다른 곳으로 이동을 하게 된다.

요즘은 찾아보기 힘들지만 버퍼링 이 걸린다는 말이 바로 데이터의 이동 시간이 지연되서 발생되는 문제라는 것이다.

그럼 이제 그것은 알았다, 파일(사진)을 올리려면 버퍼라는 곳에 담아서 보내야 한다는 것을
그렇다면 gcp(Google Cloud Platform)에서는 어떻게 하는 것일까?

구글에서는 다양한 언어를 지원하고 있는데, 나는 Javascript, Typescript를 공부하고 있어서 이쪽에 초점이 맞춰지는 점 양해 바란다.

일단 Nodejs 쪽에서는 @google-cloud/storage 라는 라이브러리를 제공해주고 있다.

ts의 경우에는 파일의 인터페이스가 존재하는데, 이러한 형태로 만들어져있다.


그리고 사진을 발송하면 콘솔창에 이렇게 찍히게 된다.

여기서 중요하게 확인을 해야하는 함수가 존재하는데 createReadStrieam()이라는 함수다.

내가 지금까지 확인한 것으로는 파일을 전송하기 위해 buffer의 형태가 되어있는 파일을
Stream이라는 형태로 읽을 수 있게 만들어주는 것이다! 라고 알고 있는 상태이다.

그리고 중요한 것은 위에 사진을 보면 Promise의 형태로 되어있는데,
무조건 async await을 걸어주지 않으면 와장창 당한다는 것이다.

다시 코드로 넘어가면

새로운 저장소를 만들고, (이것으로 자신의 저장소를 정의한다고 생각하면 된다. 프로젝트 만드는 튜토리얼 따라가면 알게 된다.)

promise이기 때문에 전부 다 받을때까지 기다려야해서 Promise.all (보통 사진은 여러장 올린다고 가정하는 것이다.)

현재 waitedFiles에는 Promise의 형태를 가진 buffer배열에 여러개 담아져있는 상태다.
사진의 갯수가 많을 수 있기 때문에, 병렬처리를 위하여 Promise.all의 내부에서
배열을 한개한개 접근하여, 본인의 GCP 저장소에 올리는 것인데, 여기서 중요한 부분이 나오게 된다.

  1. el.createReadStream() <- stream 형태로 만들어줘
  2. .pipe(storage.file(el.filename).createWriteStream() <- 위에서 정의해놓은 자신의 storage에 el의 이름으로 생성된 Stream을 생성(Write)해줘
    2.1 pipe는 파이프라고 부르는데, 쉽게 생각해서 어떤 것이든 작업을 할 수 있게 해주는 공간이라고 생각하면 편하다.
    2.2 필요에 따라서 pipe는 여러개를 붙여서 이어쓸 수도 있다.
  3. on~~ 이부분은 필요가 없다. 과제를 다 해놓고서 보니까 resolve단에 있는 저 내용은.....쓰레기값이다.

만약 최상단에 storage를 정의할 때 bucket의 정보가 붙어있지 않는다면,
pipe쪽에 재정의가 필요하지만, 붙어놨기 때문에 코드가 조금 짧아질 수 있다.

그래서 파일 업로드의 흐름은?

  1. 파일을 업로드 하는 순간 buffer의 객체 형태로 변환하게 된다.
  2. 그것을 createReadStream -> createWriteStream을 거쳐 Stream의 형태로 변환한 후 storge에 입력되게 된다.

이렇게 2step으로 진행이 되는 것 같다.

다양한 속성이 존재하는데, 아직은...아직은 잘 모르겠다ㅠㅠ 파일(이미지)업로드는 정말 웹사이트에서는 없어서는 안되는 개념이라 일단 공부를 더 해야할 것 같다.


그럼 이제 진짜 목표, 썸네일 만들기

이 글을 볼 정도면 아마 cloud function을 만들면서 화를 내고 있는 사람들일 가능성이 높다.
그래도 어느정도 설명은 해야하니까....

cloud function은 다양한 것을 제공해주고 있는데
특정 지점을 잡아서 사용하는 것도 존재한다, 이것을 Trigger 포인트를 잡아서 사용한다. 라고도 이야기를 하는 것 같았다.

내가 사용하는 이유는 사진이 storage에 업로드가 될 경우, 그것의 크기를 변환시켜서 새로 저장을 해주는 것이였다.
그럼 여기서 Trigger 위치를 잡아줘야하는데, 정확한 포인트가 명확하게 보인다.
바로 Storage에 파일이 업로드가 됐을 때

그리고 그 과정에서 다양한 방법을 시도했으나, 다 틀려먹었다.

일단 buffer와 stream의 개념이 상당히 모자랐고, 쓰라고 수업에 알려줬던 메소드들을 등한시한 댓가를 철저하게 치뤘다....
시도하고 실패한 내용을 여기 적기 시작하면 오늘 밤새도 다 적지 못할 것 같아서 완성된 코드로 넘어가려고 한다.


아래는 사용한 코드다.

index.js

const { Storage } = require("@google-cloud/storage");
const sharp = require("sharp");
//
// import를 사용하기 위해서는 package.json에 모듈화를 허용해야하는데
// 왜그런지는 모르겠는데, 지원이 되지 않았다. 분명 ES6 지원된다고 적혀있는 것 같은데...
// 그래서 require로 불러와서 사용을 하게 됐다.
// 사진을 자르기 위해 sharp 라이브러리를 사용하게 된 상태다.
//
exports.ThumbnailTrigger = async (event, context) => {
  // event에는 stream였지만, 풀어져있는 객체의 형태로 속에 어떤 데이터들이 존재하고 있는지 확인을 할 수 있다.
  // 오늘 여기에 콘솔을 몇번을 찍었는지 다부셔졌겠다 진짜
  //
  if (event.name.includes("thumb/")) return;
  // if (event.name.includes("thumb/s")) return;
  // if (event.name.includes("thumb/m/")) return;
  // if (event.name.includes("thumb/l/")) return;
  // 여기 위에 3개의 조건문은 사진이 반복되서 생성되는 것을 방지해준다.
  //
  // 어떤 원리로 돌아가냐면, 아래 map을 반복하면서 파일이 생성되었을 때 그 파일의 이름이 폴더의 위치 또한 포함되어있다.
  // 그래서 이 코드같은 경우는 thumb/s/image.jpg <- 이런 형태라 thumb이 존재하면 멈춰! 여서 중복생성되는 것을 방지해준다.
  //
  // 일단 맨 위에 주석처리가 되어있지 않은 1개만 써도 상관없지만, 주석처리가 된 3개를 써줄 경우 코드의 실행이 더 짧다.
  const option = [
    [320, "s"],
    [640, "m"],
    [1280, "l"],
  ];
  //  width만 적으면 알아서 비율을 맞춰주는데 여러번 칠까 하다가 그냥.... 그냥 배열에 담아놨다.
  // s,m,l은 사이즈다 스몰 미디움 라지
  //  
  const name = event.name;
  //
  // 여기 속에는 파일의 이름이 들어가있다, 맨 처음 파일 업로드를 image.jpg로 했다면 
  // image.jpg 가 담겨져있는 형식
  //
  const storage = new Storage().bucket(event.bucket);
  //
  // 이것은 Stroage를 초기화해주는 형식인데, 아까 맨 위의 코드를 보면 이런식으로 storage의 정보를 넣는 것을 볼 수 있었다.
  // 또한 event.bucket에는 이미지를 업로드 할 때 올라간 bucket의 정보가 들어있다.
  //
  // 즉 이제부터 storage는 사진을 업로드했을 때 적어놨던 bucket 내부라고 보면 된다.
  //
  await Promise.all(
    option.map(([size, dir]) => {
    //
    // 이중배열이라 요소를 한번에 다루려면 이런식으로 써야한다.
    //
      return new Promise((resolve, reject) => {
      //
      // 아래는 일반 메소드를 쓰는 것처럼 1개씩 쌓여나간다고 생각하면 편하다.
      //
        storage
        //
        // 내가 업로드한 버켓의 저장소에서
        //
          .file(name)
          //
          // 올렸던 파일 이름을 ex image.jpg
          //
          .createReadStream()
          //
          // 읽어서 Stream형태로 만들어줘
          //
          .pipe(sharp().resize({ width: size }))
          //
          // 그것을 width 320,640,1280의 사이즈로 바꿔서 
          //
          .pipe(storage.file(`thumb/${dir}/${name}`).createWriteStream())
          //
          // 업로드한 저장소에, thumb/s,m,l/의 폴더에
          // ${name} ex image.jpg 라는 이름으로
          // Stream을 저장해줘 (Write)
          //
          .on("finish", () => resolve())
          .on("error", () => reject());
      });
    })
  );
};

package.json

{
  "name": "sample-cloud-storage",
  "version": "0.0.1",
   "description": "Create Thumbnail of uploaded image",
  "dependencies": {
    "@google-cloud/storage": "^5.0.0", 
       "sharp": "^0.30.1"
  }
}

나중되면 더 설명을 붙이겠지만, 오늘은 이정도면 된 것 같다!
이제는 장바구니 어제 완성한거 포스팅이나 하면 될 것 같네....

profile
물류 서비스 Backend Software Developer

0개의 댓글