4주 프로젝트 DAY 13

  • es6 map, set
  • koa 앱을 serverless framework를 통해 serverless 아키텍쳐로 마이그레이션하자.
  • serverless offline을 jest로 테스트해봅시다.(http get,post)
  • serverless framework를 사용할때 로컬일때 배포할때 환경세팅을 다르게 해야겠죠?

es6 map, set

출처 : 제로초 - ES2015(ES6) Map, Set, WeakMap, WeakSet

map왜 써용?

내가 map을 쓰는이유? C++의 경우는
문자열을 인덱스로 쓰고 싶을때~ 썻었다.
key, value 쌍으로 값을 저장하는 데이터 창고이다.
map["문자열도인덱스가 될 수 있다."] = value;

사실 자바스크립트에서는 Object가 있기 때문에..크게 의미가 있지는 않다.

map 사용법

Map 생성 + 초기화까지

var map = new Map([['zero', 'ZeroCho']]);

Map에 값을 넣기

map.set('hero', 'Hero');

Map에서 값을 받기

map.get('zero'); // 'ZeroCho'

Map에 저장된 데이터량

map.size; // 2

Map에 데이터가 있냐? 없냐? c++경우 map.count(key) 썼었다.

map.has('hero'); // true
map.has('nero'); // false

값을 순회할때도 쓸 수 있다.

map.entries(); // {['zero', 'ZeroCho'], ['hero', 'Hero']}
map.keys(); // {'zero', 'hero'}
map.values(); // {'ZeroCho', 'Hero'}

//예를 들어 key가 string, value가 callback의 배열인 경우

for ( const fn of map.get(key).values()) {
    fn.apply(null, args);
}

Map에서 데이터 제거하기 (key로 제거한다.)

map.delete('hero');

Map안에 데이터 싹다 지우기

map.clear();

Set왜 써용?

저는 set를 중복없이 데이터를 저장할때 사용해요.

중복이 없다는건 균형 이진 트리로 저장한다고 생각을 할 수 있겠죠.
중복이 없다는 걸 결국 Set도 logN의 시간복잡도로 찾아낼 수 있고,
물론 지식이 짧기 때문에 ㅎㅎ 더 신박한 자료구조와 알고리즘으로 상수 시간복잡도로 할 수 있을 수도 있을 것 같다.

koa 앱을 serverless framework를 통해 serverless 아키텍쳐로 마이그레이션하자.

자 그러면 라이브러리를 이용해서 serverless 아키텍쳐로 바꿔봅시다.

다행히 ㅎㅎ koa앱을 다시 serverless 아키텍쳐로 일일이 바꾸지 않아도 돼요.

npm i serverless-http

출처 : y0c - serverless-koa-bilerplate

위 레퍼런스를 통해 serverless.yml 생성해서 정보 넣고,
serverless.ts 파일 만들어서 app을 handler로 빼고

npm i serverless-plugin-typescript

serverless.yml에 플러그인아래 추가해주고

npm install serverless-offline --save-dev

serverless.yml에 플러그인아래 추가해주고

serverless.yml 설정 뜻을 몰라..
여기보면 다 나옴
serverless 공홈

npm run sls-offline

에러 발생!

TypeError: Cannot read property 'getLineAndCharacterOfPosition' of undefined

구글링!

에러 코드 구글링
image.png

stackoverflow 발견
image.png

tsconfig.json 이 문제라네요.
image.png

다시 우리 에러메시지 보면, tsconfig에 대한 이야기가 있어요. incremental은 파일 하나로 묶을때만 사용가능하대요.

image.png

제거 해줬어요.
image.png

serverless-offline 동작...

image.png

요약
TypeError: Cannot read property 'getLineAndCharacterOfPosition' of undefined
-> tsconfig.json 에서 "incremental": true,를 제거해주면 됍니다.


그런데 test를 돌렸는데 요청이 제대로 안가요.

image.png

Not Found... 요청을 보냈는데~ 해당 router가 없었다.
그럼 예측을 해볼 수 있죠.
라우팅이 안됐다..
라우팅은 어디서 건들죠?
serverless.yml에 등록을 다음과 같이 해놨는데

image.png

이렇게 하면 안되나봐요. 구체적으로 바꿔볼게요.
구글링 해서 따라했어요.

serverless.yml for http 으로 구글링~

출처: dougmoscrop - serverless-http

functions:
  app:
    handler: src/serverless.handler 
    events:
      - http:
          path: /fileMetadataList
          method: get
      - http:
          path: /uploadFile
          method: post
      - http:
          path: /downloadFile
          method: get                
    cors:
      origin: '*'   

또 에러 발생
다른 에러가 발생했어요.
라우터는 찾아가는데 /.build/src/uploads 저런게 뜨네용...
.build로 디폴트로 저장되는 위치가 있나봐요. 저거 없애주고 싶어요.

구글링

Serverless: POST /uploadFile (λ: app)
server is listening to port 4002

  Error --------------------------------------------------

  Error: ENOENT: no such file or directory, open '/Volumes/Samsung_T5/codestates/CreativeStorage/server/.build/src/uploads/upload_a57fe6e537cd317ad37993ff88d27298'

sls offline 이랑 serverless offline 하고 똑같아요.

image.png

다음과 같이 .build가 생성이 돼요.
그리고... src 폴더에 uploads 폴더를 생성해주니 에러가 해결됐어요.

또 다른 에러 발생

server is listening to port 4002

Serverless: GET /fileMetadataList (λ: app)

  Error --------------------------------------------------

  Error: listen EADDRINUSE: address already in use :::4002

바보였었음..
in index.ts

app.listen(PORT, () => {
  console.log(`server is listening to port ${PORT}`);
});

export default app;

listen을 하고 export하다니...
listen()을 server.ts 생성해서 다음과 같이 만들어 놨음.

import app from "./index";
const PORT: number = process.env.NODE_ENV === "production" ? 4001 : 4002;

app.listen(PORT, () => {
  console.log(`server is listening to port ${PORT}`);
});

image.png

serverless 아키텍쳐로 잘 동작하게 되었다.

에러가 또 발생함...

기존 CreativeStorage V0.1 koa app에서
파일을 업로드, 다운로드, 리스트를 반환하는 API를 제공해줬습니다.

그래서 파일을 formData로 파일을 업로드했었는데,
파일을 업로드하고, 다운로드 했는데 이 둘이 같은지 체크하는 테스트 코드인데

in FileApiRouter.test.ts

describe(`upload and download and listFileMetadata test as FileApiRouter`, () => {

  it(`should upload file and get FileMetadatalist from server and check it in downloads files`, async () => {

    const imageInBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==";
    const imageBuffer = Buffer.from(imageInBase64, "base64");
    const filename = uuid();

    await uploadFile(filename, imageBuffer);

    const fileMetadataList = await getFileMetadataList();
    const filenameFromApi = fileMetadataList.map((fileMetadata) => {
      return fileMetadata.filename;
    });

    const downloadFiles: string[] = await Promise.all(filenameFromApi.map(async (filenameToBeDownloaded: string) => {
      const downloadedFile = await downloadFile(filenameToBeDownloaded);
      const downloadedFileInBase64 = downloadedFile.toString("base64");
      return downloadedFileInBase64;
    }));

    expect(downloadFiles).toEqual(expect.arrayContaining([imageInBase64]));
  });
});

image.png

테스트 코드도 http 요청을 하고 응답을 받는 형식으로 바꿔야겠다.

배포하기 전에 serverless.yml 필드에 값을 넣어놓자.

serverless.yml은 들여쓰기로 필드를 구분합니다.
indentation이 공식홈페이지나 레퍼런스에 나오듯이 써야합니다.

aws region list는? region은 한국으로...

in serverless.yml

provider: 
  name: aws
  runtime: nodejs10.x
  region : ap-northeast-2
  stage: ${opt:stage, 'dev'}    

${opt:stage}는 뭐지?
레퍼런스에 나오듯이 cli(터미널)에서 옵션을 줄 수 있다.

예를 들어 다음과 같이 사용한다면

serverless offline --stage prod

serverless.yml에서 이렇게 사용된다.

${self:provider.stage} <- prod 값이 들어가게 된다.

${self:provider.stage} 이게 뭔말인지? 어디서 봐야하나?
출처 : serverless 공홈

이러지말고 custom에 변수를 만들어 사용해보자.

in serverless.yml

service: new-service
provider:
  name: aws
  stage: dev
custom:
  myStage: ${opt:stage, self:provider.stage} 
  myRegion: ${opt:region, 'us-west-1'}

functions:
  hello:
    handler: handler.hello

myStage: ${opt:stage, self:provider.stage}
이 의미는 stage 옵션을 두었다면 그 스테이지가 myStage로 들어간다.
stage 옵션을 두지 않았다면 provider.stage인 dev가 들어간다.

  • 기존 serverless.yml과 custom 을 사용한 serverless.yml 비교

기존 serverless.yml

provider: 
  name: aws
  runtime: nodejs10.x
  region : ap-northeast-2
  stage: ${opt:stage, 'dev'}    

custom:
  serverless-offline:
    port: ${file(./src/config/${self:provider.stage}.json):PORT}

custom변수를 이용한 serverless.yml

provider: 
  name: aws
  runtime: nodejs10.x
  region : ap-northeast-2
  stage: dev

custom:
  myStage: ${opt:stage, self:provider.stage}
  myRegion: ${opt:region, self:provider.region}
  serverless-offline:
    port: ${file(./src/config/${self:custom.myStage}.json):PORT}    

serverless offline을 jest로 테스트해봅시다.(http get,post)

레퍼런스
붙이는데 실패했음.
https://niradler.com/serverless-integration-tests-with-offline-plugin/

에러 발생

timeout이 발생한다... timeout 전에 비동키 콜백이 돌지 않는다..

image.png

레퍼런스
https://dev.to/didil/serverless-testing-strategies-4g92
테스트 시작할때 serverless offline 실행했다가 테스트 끝날때 serverless offline 중지시키기

레퍼런스
실패했지만
https://codeburst.io/how-i-do-integration-test-for-service-powered-by-serverless-dynamodb-using-jest-e94f0710d28f
spawn을 통해 npm run script를 쓸 수 있다는 정보를 얻음.
shell을 쓰는데.. 난 못했음..

아직 해결못함.

해결함. 아래에 정리해놓음.

cross-env? nodejs process 환경변수로 이용해서 프로그램을 하고싶을때 써요.
process.env.NODE_ENV=development 이렇게 변수를 나눠서 개발 할 수 있는데요.
저는 맥을 쓰는데요. 윈도우는 방식이 달라요.
그래서 운영체제 상관없이 NODE_ENV를 바꿀 때 cross-env를 쓴대요.
https://thebook.io/006982/ch15/01/03-01/

npm scripts에서 윈도우에서도 NODE_ENV를 세팅하고 싶다면~
"start": "cross-env NODE_ENV=production node app",

node.js 로 자식 프로세스를 만들어서 프로그램을 돌릴 수 있다고?
출처 : www.freecodecamp.org
방법도 4개나 됌. 위에 레퍼런스 자세히 나와있음.

[node.js] 외부 프로세스를 생성하고 제어하기
출처 : 몽상가

node.js node.js 에서 spawn 과 exec 의 차이
출처 : 꿀벌개발일지

serverless offline jest 테스트 코드

현재 프로젝트 디렉토리

- server 
    - src
        - serverlessUtil.ts
    - test
        - serverless.test.ts

serverless offline을 자식프로세스로 실행시킬 util 프로그램

in serverlessUtil.ts

import { spawn, ChildProcessWithoutNullStreams } from "child_process";

const isWindows: boolean = /^win/.test(process.platform);
const npmCommand: string = isWindows ? "npm.cmd" : "npm";

let slsProcess: ChildProcessWithoutNullStreams;
export function start(): Promise<string | Error> {
  return new Promise((resolve, reject) => {
    slsProcess = spawn(npmCommand, ["run", "sls-offline", "--", "--noTimeout"]);
    slsProcess.stdout.on("data", (data) => {
      const log: string = (data as Buffer).toString("utf-8");
      if (log.includes("listening on")) {
        resolve(log);
      }
    });
    slsProcess.stderr.on("data", (data) => {
      const log: string = (data as Buffer).toString("utf-8");
      reject(log);
    });
    slsProcess.on("error", (data) => {
      reject(data);
    });
  });
}

export function stop(): void {
  slsProcess.kill();
}

in serverless.test.ts

import { start, stop } from "../ServerlessUtil";
import fetch from "node-fetch";

export async function getTest(testUrl: string): Promise<boolean> {
  const response = await fetch(testUrl);

  if (!response.ok) {
    throw new Error(response.statusText);
  }
  return response.ok;
}

async function healthCheck(): Promise<boolean> {
  const url = "http://localhost:4002/";

  const response = await fetch(url);
  let status: boolean = false;

  if (!response.ok) {
    throw new Error(response.statusText);
  }

  status = true;

  return status;
}

describe("serverelss", () => {

  beforeEach(async () => {
    jest.setTimeout(30000);
    await start();
  });

  afterEach(() => {
    stop();
  });

  it("/filemetadatalist api get request response test", async () => {
    const serverStatus = await healthCheck();
    if (serverStatus) {
      const status: boolean = await getTest("http://localhost:4002/filemetadatalist");
      expect(status).toEqual(true);
    } else {
      expect(true).toEqual(false);
    }
  });

  it("/uploadfileurl api get request response test", async () => {
    const serverStatus = await healthCheck();
    if (serverStatus) {
      const status: boolean = await getTest("http://localhost:4002/uploadfileurl");
      expect(status).toEqual(true);
    } else {
      expect(true).toEqual(false);
    }
  });

  it("/downloadfileurl api get request response test", async () => {
    const serverStatus = await healthCheck();
    if (serverStatus) {
      const status: boolean = await getTest("http://localhost:4002/downloadfileurl");
      expect(status).toEqual(true);
    } else {
      expect(true).toEqual(false);
    }
  });

  it("/deletefileurl api get request response test", async () => {
    const serverStatus = await healthCheck();
    if (serverStatus) {
      const status: boolean = await getTest("http://localhost:4002/deletefileurl");
      expect(status).toEqual(true);
    } else {
      expect(true).toEqual(false);
    }
  });

});

serverless framework를 사용할때 로컬일때 배포할때 환경세팅을 다르게 해야겠죠?

image.png
위와 같이 serverlessConfig.json 파일을 하나 만들었습니다.

image.png
그 안에는 미리 stage에 따라 다르게 나눌 프로퍼티들을 설정해둡니다.

image.png
serverless.yml 에서는
${file(경로):stage.port} 이런 형식으로 써줘야 합니다. stage앞에는 : 를 붙여줘야 합니다.

아래는 참조한 레퍼런스들입니다.

레퍼런스 1
환경에 따라 다르게 serverless.yml 설정을 다르게 하고싶다.
env.yml을 만들어서 사용한 레퍼런스

레퍼런스 2
환경에 따라 다르게 serverless.yml 설정을 다르게 하고싶다.
serverless-dotenv-plugin 사용

레퍼런스 3
환경에 따라 다르게 serverless.yml 설정을 다르게 하고싶다.
Recursively reference properties
여기보면 --stage 옵션을 통해 여러 환경을 미리 세팅해놓고 사용할 수 있음.

레퍼런스 4
serverless.yml에 나오는 키워드?필드?프로퍼티? 이게 무슨뜻인지 모르겟다 ㅠ
구체적으로 serverless.yml에 대한 내용이 나옴.

레퍼런스 5

공식홈페이지에 나온 방법을 좀 더 자세히 설명해준 레퍼런스

레퍼런스 6

json 파일을 읽어서 사용하는 방식

serverless.yml

custom:
 STAGE: ${self:provider.stage} # 현재 스테이지 별로 데이터베이스 접속 정보를 달리하기 위함
 DB_CONFIG: ${file(./config/config.js):DB_CONFIG} # config.js 에서 가져올 데이터 베이스 접속정보

config.js 은 다음과 같이 쓸 수 있다.

module.exports.DATABASE_CONFIG = (serverless) => ({
 dev: {
   DB_HOST: 'localhost',
   DB_USER: 'scott',
   DB_PASSWORD: 'tiger'
 },
 prod: {
   DB_HOST: 'fake.database.com',
   DB_USER: 'scott2',
   DB_PASSWORD: 'tiger2'
 }
});

레퍼런스 7

다른 Nuxt 프로젝트를 배포하는 것이긴 하지만
현재 serverless deploy 작업만 남아있기 때문에 참조했음.
특히 serverless.yml 에서 package 부분을 참조했음.