ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • AWS 를 사용한 서버리스 아키텍쳐 적용기(삽질기) (6) - serverless 프레임워크 2
    Web Study/Server 2019. 5. 22. 11:27

    저번 글에서 서버리스 프레임워크를 간단하게 사용해봤습니다.

    이번 글에선 좀 더 많은 기능을 테스트해보겠습니다.

     

    0. 새로운 프로젝트 생성

    sls create -t aws-nodejs -p <프로젝트명> 

    명령어를 통해 다시 프로젝트를 생성하겠습니다.

     

    생성한 다음 해당 폴더에 들어가서 

    yarn init -y

    명령어를 통해 package.json을 만들어줍니다.

    또한 이번에는 좀 더 편하게 사용할 수 있도록 serverless-http, serverless-webpack 등을 사용할 예정입니다.

     

    * serverless-http? 기존 node 기반 백엔드 서버 구축에 사용되는 express, koa 등의 프레임워크로 람다 함수를 wrapping 해서 사용할 수 있게 만들어주는 일종의 툴.

    * serverless-webpack? serverless용 웹팩이다. 웹팩은 검색해보면 관련된 좋은 설명들이 너무 많다. 많은 번들러 중에 굳이 왜 웹팩을 쓰느냐? 제일 좋아서 쓴다. 

    * serverless-offline? serverless는 코드를 수정하고 테스트를 해보고 싶으면 배포를 해야 테스트를 해볼 수 있다. 배포는 s3를 통해 배포되므로 곧 비용과 직결되는 문제이다. serverless-offline을 사용하면 로컬에서도 테스트를 해볼 수 있다!! 

     

    1. 본격적인 준비.

    1-1. serverless.yml

    우선 serverless.yml 파일을 수정해줘야 합니다.

    serverless-http는 따로 설정 안 해주시고 npm 또는 yarn을 통해 install만 해주시면 됩니다. serverless-webpack과 serverless-offline은 yml에 설정해줘야 사용할 수 있습니다.

    service: sls-assistant # 서비스 명.
    
    plugins:
      - serverless-webpack # webpack이 먼저 와야됨.
      - serverless-offline
    
    custom:
      webpack: # serverless-webpack 관련 설정
        webpackConfig: 'webpack.config.js'
        includeModules: true
        packager: 'yarn' # npm을 사용한다면, npm 으로 설정.
    
    provider:
      name: aws
      runtime: nodejs8.10
      stage: dev
      region: ap-northeast-2 # 한국으로 설정.
    
    functions:
      app:
        handler: src/app.handler
        events:
          - http: ANY /
          - http: 'ANY {proxy+}'

    1-2. webpack.config.js

    명시된 것과 같이 webpack.config.js 파일을 통해 webpack 설정을 해줘야 합니다.

    webpack.config.js 파일은 다음과 같이 작성해주시면 되겠습니다.

    const path = require('path');
    const serverlessWebpack = require('serverless-webpack');
    const nodeExternals = require('webpack-node-externals');
    
    module.exports = {
      // entry를 따로 설정하지 않아도 됨
      entry: serverlessWebpack.lib.entries,
      target: 'node',
      mode: serverlessWebpack.lib.webpack.isLocal ? 'development' : 'production',
      // webpack의 critical warning 메시지를 피하기 위한 용도
      externals: [nodeExternals()],
      resolve: {
        modules: [path.resolve('./src'), 'node_modules'] // src 디렉토리 내부에서 absolute import를 사용하기 위한 용도
      },
      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
              {
                loader: 'babel-loader'
              }
            ]
          }
        ]
      },
      output: {
        libraryTarget: 'commonjs',
        path: path.join(__dirname, '.webpack'),
        filename: '[name].js'
      }
    };

    코드를 보면 loader는 바벨을 쓰는 것을 확인할 수 있습니다.

    따라서 install을 통해 바벨도 설치해주고 여러 가지 설치를 해줘야 됩니다.

     

    1-3. package.json

    그럼 어떤 것들이 설치되어야 하는지 package.json 파일을 잠시 보겠습니다.

    {
      "name": "sls-assistant",
      "version": "1.0.0",
      "main": "index.js",
      "license": "MIT",
      "dependencies": {
        "axios": "^0.18.0",
        "express": "^4.16.4",
        "serverless-http": "^2.0.1"
      },
      "devDependencies": {
        "@babel/core": "^7.4.4",
        "@babel/preset-env": "^7.4.4",
        "babel-loader": "^8.0.6",
        "serverless-offline": "^4.10.0",
        "serverless-webpack": "^5.3.0",
        "webpack": "^4.31.0",
        "webpack-node-externals": "^1.7.2"
      }
    }
    

    바벨 관련된 패키지, serverless-offline, serverless-webpack 등등은 전부 개발할 때 빌드용으로 사용할 것이므로

    devDependencies에 추가해주시면 되겠습니다.

    dependencies 에는 저 같은 경우 람다에서 post, get 등의 http request를 보낼 것이므로 axios, body-parser, express 등을 사용합니다.

    모두 yarn 또는 npm을 이용해서 옵션을 주고 install 해주시면 됩니다.

     

    2. 구조

    위에 webpack.config.js 파일에서 './src' 경로를 절대 경로로 설정해주었습니다.

    그럼 폴더 구조가 어떻게 되어있는지 간단하게 보겠습니다.

    디렉토리 구조

    순서대로 보면

    .serverless -> 어느 시점에 생긴 건지 정확히 확인하지 못했습니다...ㅜㅜ  대충 유추해보면 deploy를 할 때 생기는 폴더 같습니다. cloudformation 관련 설정 파일들과 람다에 올라가는 압축파일이 있습니다.

    .webpack -> 위의 webpack.config.js 파일의 output을 보면 path가 .webpack 으로 설정해놓은 것을 볼 수 있습니다. 프로젝트가 빌드될 때 생성된 빌드 output 폴더입니다.

    node_modules -> 패스

    src -> 실질적으로 모든 코드가 있는 폴더입니다. 저 같은 경우 components 폴더 안에 express로 전달되는 각 경로에 따라 폴더를 따로 생성해서 알아보기 쉽도록 하였습니다.

     

    3. 코드 실행 흐름

    3-1. app.js

    단순합니다. server.js를 가져와서 handler로 export 해줍니다.

    import Server from './server';
    
    const server = new Server();
    
    // serverless-http 의 entry point
    export const { handler } = server;

    이를 통해 serverless.yml 파일에서 handler: src/app.handler로 설정해놓은 entry point를 server.js로 연결시킵니다.

     

    3-2. server.js

    사실상 모든 코드가 시작되는 부분입니다.

    간단하게 구성해봤습니다.

    import serverless from 'serverless-http';
    import express from 'express';
    import axios from 'axios';
    import beginning from 'components/apart-evaluation/beginning';
    
    export default class Server {
      constructor() {
        this.app = express();
        // this.app.use(express.urlencoded({extended: false}));
        this.app.use(express.json());
    
        this.app.get('/', (req, res) => { //get
          res.json({ hello: "world"});
        });
    
        this.app.post('/apart_evaluation/beginning', (req, res) => {
          beginning(req.body).then((data) => {
            console.log(JSON.stringify(data));
            res.json(data);
          });
        });
      }
    
      get handler() {
        // serverless-http 관련 설정
        return serverless(this.app);
      }
    }

    평범한 node의 express 프레임워크입니다.

    beginning이라는 함수를 components 폴더 안의 어딘가에서 가져와서 사용합니다. 이때, Promise를 이용해서 결과 값을 전송해줍니다.

    http request에는 항상 필수적으로 동기/비동기 제어가 중요합니다. (계속 써도 계속 헷갈리긴 하는데...ㅋㅋㅋㅋㅋ)

    당연히 beginning 함수의 리턴에서 return new Promise 형태로 값을 리턴해줘야 되겠죠?

     

    3-3. beginning.js

    import axios from 'axios';
    
    const Beginning = async (data) => {
      // console.log(data);
      let blockId = data.userRequest.block.id;
      let botUserId = data.userRequest.user.id;
      let plusFriendUserId = data.userRequest.user.properties.plusfriendUserKey;
      let key, msg, label;
      let webUrl = "http://kakao.chatbot/api/test/";
      let returnView;
    
      await axios.get("http://kakao.chatbot/api/test",{
        data : {
          block_id: blockId,
          user_id: botUserId,
          plus_friend_id: blockId
        }
      }).then(response => {
        if(response.data.status == "success") {
         ...
          returnView = {
            "version": "2.0",
            "template": {
             ...
            }
          };
        } else {
          msg = response.data.msg;
          returnView = {
            "version": "2.0",
            "template": {
              "outputs": [
                {
                  "simpleText": {
                    "text": msg
                  }
                }
              ]
            }
          };
        }
      }).catch(error => {
        console.log(error);
        returnView = {
          "version": "2.0",
          "template": {
            "outputs": [
              {
                "simpleText": {
                  "text": "error"
                }
              }
            ]
          }
        };
      });
      // promise returnView
      return new Promise(resolve => {
        console.log(returnView);
        resolve(returnView);
      });
    };
    
    export default Beginning;

    저 같은 경우에는 카카오 챗봇에 연결했기 때문에 챗봇에 맞는 템플릿을 연결해줬습니다.

    async & await 그리고 promise를 사용해서 비동기의 늪에서 헤어 나올 수 있었습니다ㅠㅠ (맨날 해도 맨날 헷갈리는 게 함정)

    마지막 라인 쪽을 보시면 return new Promise 구문을 이용해 위의 axios를 이용한 api콜이 끝나면 그 결과를 리턴해줍니다.

    리턴 받은 결과값을 위의 server.js 에서 최종적으로 처리합니다. 

    사실 server.js에서 다 처리해도 됩니다. 하지만 api콜이 여러 개 붙을수록 server.js 코드가 복잡해지겠죵?

    그렇기 때문에 굳이 폴더를 나눠서 처리했습니다. (저는 하나의 함수는 하나의 기능만 수행하는 것이 모든 면에서 좋다고 생각합니다.)

    추후에 api endpoint url 같은 경우도 따로 빼고 중요한 부분은 람다 함수의 환경변수로 모두 빼서 보안도 챙기고 나중에 수정할 때 편하게 수정할 수 있습니다.

     

    4. 실행

    그럼 이제 대충 코드가 완성되었으니 배포해야겠죠?

    아 배포하기 전에

    sls offline start

    명령어를 통해 테스트를 해보고 에러처리를 한 뒤에 배포해줍시다!

    sls deploy

    명령어를 치게 되면 자동으로 파일들이 빌드되고 aws에 배포됩니다.

    배포한 뒤 테스트를 해보면 성공적으로 돌아가는 것을 확인할 수 있습니다.

     

     

    5. 결론

    serverless 프레임워크가 정말진짜레알완전 편리한 것을 느낄 수 있었습니다.

    기존의 정말 복잡한 여러 가지 세팅들을 할 필요 없이 바로 뚝딱뚝딱 간단하게 프로젝트에 적용할 수 있습니다.

    물론 조금 더 세밀하게 구축하고 보안적인 부분도 신경 쓰려면 세팅을 조금씩 바꿔줘야 됩니다.

    다음 글은 이렇게 만든 람다 함수를 실제로 적용하려면 어떠한 추가 작업들을 거쳐야 하는지 알아보겠습니다.

    (공부를 많이 해야 될 것 같아서 오래 걸릴 듯....? 그냥 넘어갈수도?ㅋㅋㅋ)

     

     

    p.s 카카오 챗봇 관련해서는 나중에 기회가 되면 포스팅...?

     

ZER01NE