잊지 않겠습니다.

Webpack을 이용한 angularJS 1 application 개발

기존 grunt를 이용한 angularjs application을 webpack을 사용하도록 변경하고자 합니다. 일단 webpack을 사용하게 되면 다음과 같은 장점을 가질 수 있습니다.

  1. npm을 이용한 외부 liberary의 관리
  2. es6 문법의 사용
  3. import/require의 사용 가능
  4. nodejs의 server code와 client code의 완벽 호환 가능

위 장점 이외에, 최근의 웹 개발 양상은 module화입니다. 모든 liberary들이 CommonJS 방식으로 module화가 되어가고 있는 시점에서, 추세에 따라가지 못하면 도태가 되어버리는 것 같은 압박감을 느끼기도 하지요.

환경

사용되는 application의 주요 library들은 다음과 같습니다.

  • jquerey
  • angularjs
  • lodash
  • restangular
  • kendo ui Pro

구조

기본적으로 webpack의 폴더구조는 maven의 구조와 비슷하게 가는 경향들이 있습니다. 다음과 같은 folder구조를 가지고 갈 것입니다.

.
├── dist
├── gulpfile.js
├── libs
│   └── kendo
├── LICENSE
├── node_modules
├── package.json
├── README.md
├── src
│   ├── app
│   └── public
├── webpack.config.js
├── webpack-test.sublime-project
└── webpack-test.sublime-workspace
  • dist: build된 file들이 위치할 folder입니다.
  • libs: bower/npm이 지원되지 않는 외부 library들이 위치한 folder입니다. 상기 application에서는 kendo가 포함됩니다.
  • src: 소스가 위치합니다.
    • public: public에 배포될 css, image, html 들이 위치할 folder입니다.
    • app: angularJs application이 위치할 folder입니다. app.js를 시작접으로 application이 구동될 것입니다.

여기서 app folder의 경우 다음과 같은 구조로 구성됩니다. (기본적으로 yo angular와 동일한 구성을 가지고 갑니다.)

├── controllers
│   ├── home.js
│   └── user.js
├── directives
│   └── nameButton.js
├── services
│   ├── BaseService.js
│   └── CodeService.js
├── app.js
└── routes.js

사용자 directive, controller, service를 모두 사용합니다.

packages.js

webpack은 기본적으로 bower를 사용하지 않습니다. 모든 js library들은 npm을 통해 설치되며 관리됩니다.

basic angularJS application (STEP01)

가장 기본적인 angularjs application을 작성해보도록 합니다. 다음과 같은 특징을 갖습니다.

  • /home, /user 라는 두개의 route를 갖습니다.

추후에 npm을 이용해서 module을 관리할 예정이기 때문에 처음 시작은 모두 cdn을 이용해서 처리하도록 합니다.

public안에 다음과 같이 구성합니다.

├── js
│   ├── controllers
│   │   ├── home.js
│   │   └── user.js
│   └── app.js
├── home.html
├── index.html
└── user.html

그리고, /home, /user의 url router를 다음과 같이 구성합니다.

// app.js
'use strict';

angular
  .module('app', ['ui.router'])
  .config(function ($stateProvider, $urlRouterProvider) {
    $stateProvider.state('home', {
      url: '/home',
      templateUrl: 'home.html',
      controller: 'HomeController'
    })
    .state('user', {
      url: '/user',
      templateUrl: 'user.html',
      controller: 'UserController'
    });
  });

index.html

<!doctype html>
<html ng-app="app" lang="en">
<head>
  <meta charset="UTF-8">
  <title>Angular App</title>
</head>
<body>
  <button ui-sref="home">Home</button>
  <button ui-sref="user">User</button>
  <ui-view></ui-view>
</body>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.9/angular.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/angular-ui-router/1.0.0-alpha.4/angular-ui-router.min.js"></script>
  <script src="/js/app.js"></script>
  <script src="/js/controllers/home.js"></script>
  <script src="/js/controllers/user.js"></script>
</html>

home.html

<p>this is home.html</p>

user.html

<p>This is user.html</p>

이제 public folder내에서 http-server를 실행해보면 간단한 angularjs webapp 이 생성됩니다.

basic angularJS application + webpack (STEP02)

이제 webpack을 이용해서 angularjs application을 구성해보도록 하겠습니다.
먼저, public/js 안에 있는 모든 js 파일들을 모두 app folder로 옮깁니다.

.
├── app
│   ├── controllers
│   │   ├── home.js
│   │   └── user.js
│   └── app.js
└── public
    ├── home.html
    ├── index.html
    └── user.html

이런 구성을 가지게 됩니다.
이제 webpack.config.js 파일을 생성합니다.

'use strict';

const config = {
  entry: './src/app/app.js',
  output: {
    path: __dirname + '/dist',
    filename: 'app.bundle.js'
  },
  plugins: []
};
module.exports = config;

매우 기초적인 config 파일입니다. app.js 파일을 entry point로 지정하고, compile된 folder를 지정합니다. 이제 bundle된 파일을 사용하도록 index.html을 다음과 같이 수정합니다.

<!doctype html>
<html ng-app="app" lang="en">
<head>
  <meta charset="UTF-8">
  <title>Angular App</title>
</head>
<body>
  <button ui-sref="home">Home</button>
  <button ui-sref="user">User</button>
  <ui-view></ui-view>
  <script src="/app.bundle.js"></script>
</body>
</html>

그리고, webpack에서 각 controller, angular-ui-router의 import를 위해 app.js를 다음과 같이 수정합니다.

var angular = require('angular');
require('angular-ui-router');

angular
  .module('app', [ 'ui.router' ])
  .config(function ($stateProvider, $urlRouterProvider) {
    $stateProvider.state('home', {
      url: '/home',
      templateUrl: 'home.html',
      controller: 'HomeController'
    })
    .state('user', {
      url: '/user',
      templateUrl: 'user.html',
      controller: 'UserController'
    });
  });

require('./controllers/home');
require('./controllers/user');

여기서 중요한 것은 require의 위치입니다. controller들의 등록은 최초 app의 등록이 마쳐진 이후에 등록되어야지 됩니다.

angularJS application (ES2015) + webpack (STEP03)

이제, angularJs application을 es2015 문법으로 구현해보도록 하겠습니다. 개인적으로 가장 욕심이 나는 부분입니다.

es2015 문법을 사용해서 compile 하기 위해서는 babel을 이용해야지 됩니다. babel loader와 babel 2015 loader를 npm을 이용해서 설치합니다.

npm install babel-loader --save-dev
npm install babel-preset-es2015 --save-dev

이제 webpack.config에 babel loader를 등록시켜줍니다. js파일은 모두 babel loader에 의해서 처리가 되도록 설정합니다.

'use strict';
const config = {
  entry: './src/app/app.js',
  output: {
    path: __dirname + '/dist',
    publicPath: '/',
    filename: 'app.bundle.js'
  },
  module: {
    loaders: [{
      test: /\.js$/,
      loader: 'babel',
      query: {
        presets: ['es2015']
      },
      exclude: /node_modules/
    }]
  },
  devtool: 'inline-source-map',
  plugins: []
};
module.exports = config;

이제 app.js, home.js, user.js 파일을 es2015 문법에 맞게 수정을 합니다.

home.js

class HomeController {
  constructor() {
    this.name = 'HomeCtrl';
    console.log('HomeController.init');
  }

  getName() {
    return this.name;
  }

  changeName(name) {
    this.name = name;
    return name;
  }
}

module.exports = HomeController;

user.js

class UserController {
  constructor() {
    this.name = 'UserCtrl';
    console.log('UserController.init');
  }

  getName() {
    return this.name;
  }

  changeName(name) {
    this.name = name;
    return name;
  }
}

module.exports = UserController;

app.js

var angular = require('angular');
require('angular-ui-router');

angular
  .module('app', [ 'ui.router' ])
  .config(function ($stateProvider, $urlRouterProvider) {
    $stateProvider.state('home', {
      url: '/home',
      templateUrl: 'home.html',
      controller: 'HomeController'
    })
    .state('user', {
      url: '/user',
      templateUrl: 'user.html',
      controller: 'UserController'
    });
  })
  .controller('HomeController', require('./controllers/home'))
  .controller('UserController', require('./controllers/user'));

app.js에서 angular에 대한 dependency만을 갖고, 각 controller들은 dependency에서 angular에서 자유로워지는 것을 볼 수 있습니다. 이제 webpack 명령어를 실행해보면 다음과 같은 결과를 볼 수 있습니다.

Hash: f32a9fcb0a0a1f4a0d08
Version: webpack 1.12.14
Time: 1498ms
        Asset     Size  Chunks             Chunk Names
app.bundle.js  3.88 MB       0  [emitted]  main
    + 6 hidden modules

외부 Library 들의 연결 (STEP04)

angularJs application을 개발하면 당연히 기존 library 들과 같이 사용해야지 되는 경우가 많습니다. 먼저 angular-ui-router의 경우에도 마찬가지지요.

대표적인 것들이 일단 lodash와 같은 utility library들이 있을것이고, angularjs에서 rest api 를 call 할때 사용하는 restangular 역시 주로 사용되는 library 입니다.

먼저 loadash에 대해서 알아보도록 하겠습니다. 저는 lodash에 대한 의존이 매우 높은 관계로 전역으로 등록시켜서 사용하고자 합니다.

npm을 통해서 lodash를 설치합니다.

npm install lodash --save

lodash의 경우에는 다른 기타 library들의 dependency가 심한편입니다. 또한 모든 controller에서 전역으로 사용하고싶은 욕심이 있습니다. 그렇다면 webpack에서 미리 등록을 시켜두면 좀 더 사용하기 편할 것 같습니다. browser 환경의 전역 scope로 미리 등록시켜두기 위해서는 webpack에서 제공하는 ProvidePlugin을 이용해서 등록을 시킵니다.

webpack.config.js를 다음과 같이 수정합니다.

const webpack = require('webpack');

const config = {
  entry: './src/app/app.js',
  output: {
    path: __dirname + '/dist',
    publicPath: '/',
    filename: 'app.bundle.js'
  },
  module: {
    loaders: [{
      test: /\.js$/,
      loader: 'babel',
      query: {
        presets: ['es2015']
      },
      exclude: /node_modules/
    }]
  },
  devtool: 'inline-source-map',
  plugins: [
    new webpack.ProvidePlugin({
      'window._': 'lodash',
      '_': 'lodash'
    })
  ]
};
module.exports = config;

lodashwindow 전역과 local 모두에 등록을 시켰습니다. 이제 controller에서는 lodashrequire필요 없이 사용 가능합니다.

home.js

class HomeController {
  constructor() {
    this.name = 'HomeCtrl';
    console.log('HomeController.init');
    console.log(_.random(0, 5));
  }

  getName() {
    return this.name;
  }

  changeName(name) {
    this.name = name;
    return name;
  }
}

module.exports = HomeController;

이제 restangular를 추가할 차례입니다. npm을 이용해서 restangular를 설치하는 일련의 과정은 lodash와 동일합니다. 다만, restangular의 경우에는 angularjs module에 등록을 시켜줘야지 됩니다. app.js는 다음과 같이 변경되어야지 됩니다.

app.js

var angular = require('angular');
require('angular-ui-router');
require('restangular');

angular
  .module('app', [ 'ui.router', 'restangular' ])
  .config(($stateProvider, $urlRouterProvider) => {
    $stateProvider.state('home', {
      url: '/home',
      templateUrl: 'home.html',
      controller: 'HomeController'
    })
    .state('user', {
      url: '/user',
      templateUrl: 'user.html',
      controller: 'UserController'
    });
  })
  .config(RestangularProvider => {
    console.log(RestangularProvider);
  })
  .controller('HomeController', require('./controllers/home'))
  .controller('UserController', require('./controllers/user'));

이제 controller에서는 생성자에서 Restangular를 inject해서 사용가능합니다.

home.js

class HomeController {
  constructor(Restangular) {
    this.Restangular = Restangular;
    this.name = 'HomeCtrl';
    console.log('HomeController.init');
    console.log(_.random(0, 5));

    this.Restangular.all('').get('data.json').then(result => {
      console.log(result);
    });
  }

  getName() {
    return this.name;
  }

  changeName(name) {
    this.name = name;
    return name;
  }
}

module.exports = HomeController;

Service, Directive, filter 추가 (Step 05)

angularJs의 주요 컨셉중 하나인 service, directive, filter의 경우, 기존 controller추가와 동일합니다. 다만 es2015 형식으로 코딩을 하는 경우, class를 이용해서 작성하는 것만 좀 유의해주면 될 것 같습니다.

userservice.js - service

class UserService {
  constructor(Restangular) {
    this.Restangular = Restangular;
  }

  getUserName() {
    return 'username';
  }

  getUserInfo() {
    this.Restangular.all('').get('info.json').then(info => {
      console.log(info);
    });
  }
}
module.exports = UserService;

userfilter.js - filter

function filteringUser () {
  return function (users) {
    return _.filter(users, function (user) {
      return user.name.indexOf('7') >= 0;
    });
  };
}
module.exports = filteringUser;

namedbutton.js - directive

function postLink(scope, element, attrs) {
  console.log(attrs);
  scope.ok = function () {
    alert(scope.name);
  };
}

function template(element, attrs) {
  console.log(attrs);
  return '<button ng-click="ok()">This is named button 확인</button>';
}

module.exports = function NameButtonDef() {
  return {
    restrict: 'E',
    scope: {
      name: '='
    },
    link: postLink,
    template: template
  };
};

app.js

var angular = require('angular');
require('angular-ui-router');
require('restangular');

angular
  .module('app', [ 'ui.router', 'restangular' ])
  .config(($stateProvider, $urlRouterProvider) => {
    $stateProvider.state('home', {
      url: '/home',
      templateUrl: 'home.html',
      controller: 'HomeController',
      controllerAs: 'model'
    })
    .state('user', {
      url: '/user',
      templateUrl: 'user.html',
      controller: 'UserController',
      controllerAs: 'model'
    });
  })
  .config(RestangularProvider => {
    console.log(RestangularProvider);
  })
  .controller('HomeController', require('./controllers/home'))
  .controller('UserController', require('./controllers/user'))
  .service('UserService', require('./services/userservice'))
  .filter('user', require('./filters/userfilter'))
  .directive('namedButton', require('./directives/namedbutton'));

controller와 별 차이가 없는 코드 패턴을 보입니다. 개인적으로는 최고의 장점이 service, directive, filter 코드 자체에 angular dependency가 없어지는 것이 장점인것 같습니다.

jqLite 대신 jquery 사용하기 (step06)

기본적으로 angular.element의 경우, jquery가 없는 경우에는 angularjs내부에 include 되어 있는 jqlite를 이용해서 element handling을 지원합니다. 그런데 angular.element의 경우 jquery selector보다 매우 기능이 빈약합니다. bootstrap도 사용할 가능성도 높고, 그냥 기본적으로 jquery를 포함해서 사용하게 되는 경우가 더 일반적입니다.

그럼 angular.element에서 jquery를 사용하도록 할려면 어떻게 해야지 될까요? 예전 방법대로라면 다음과 같이 해주면 됩니다.

<script src="jquery.js"></script>
<script src="angular.js"></script>

네. angular보다 jquery를 먼저 선언해주면 모든것이 해결되었습니다. 그럼 비슷한 구문으로 실행해보도록 하겠습니다.

    require('jquery');
    const angular = require('angular');
    console.log(angular.element('div'));

이러면 다음과 같은 에러가 발생합니다.

angular.js:13424 Error: [jqLite:nosel] Looking up elements via selectors is not supported by jqLite! See: http://docs.angularjs.org/api/angular.element
http://errors.angularjs.org/1.5.3/jqLite/nosel

네. jqLite를 사용하고 있지, jquery를 사용하고 있지 못하게 됩니다.

이런 경우에는 expose-loaderimport-loader를 이용해서 각 module에서 jquery등을 자동으로 내부내에서 import 시켜줘야지 됩니다.

npm을 이용해서 다음 module들을 설치합니다.

npm install imports-loader --dev-save
npm install expose-loader --dev-save

그리고 webpack.config.js의 module 부분을 다음과 같이 변경합니다.

  module: {
    loaders: [{
      test: /\/angular\.js$/,
      loader: 'imports?jQuery=jquery'
    }, {
      test: /\/jquery.js$/,
      loader: 'expose?jQuery'
    }, {
      test: /\.js$/,
      loader: 'babel',
      query: {
        presets: ['es2015']
      },
      exclude: /node_modules/
    }]
  },

코드 해석은 다음과 같습니다. jquery가 요청되는 경우 window.jQuery로 등록을 시키는 구문이 두번쩨 expose 구문입니다. 그리고 angular.js가 로드될 때, jQuery의 경우 jquery를 import 해서 사용하도록 강제하는 것입니다.

위의 두 구문은 매우 중요합니다. jquery 기반의 module들을 사용할 때, 안되는 이유의 대부분이 window.jQuery, window.$, window.jquery가 등록이 되지 않을 때 주로 나타나게 됩니다.

이제 저 구문을 넣어주고 webpack 을 실행해주면 다음과 같은 결과를 볼 수 있습니다.

[div#window-resizer-tooltip, div#feedly-mini, prevObject: jQuery.fn.init[1], context: document, selector: "div"]0: div#window-resizer-tooltip1: div#feedly-minicontext: documentlength: 2prevObject: jQuery.fn.init[1]selector: "div"__proto__: Object[0]

Summary

angularjs2가 나오는 이때에 유행에 어울리지 않는 angularjs와 최신 유행인 webpack간의 결합에 대해서 알아봤습니다.

위 소스는 모두 github에 올려두었고, step별로 tag를 달아두었습니다.
(https://github.com/xyzlast/webpack-angular1-step)

  1. angularjs1 application을 webpack으로 묶는 과정에 대해서 알아봤습니다.
  2. jquery 기반의 library들을 어떻게 결합할 수 있는지에 대해서 알아봤습니다.

css-loader등을 잘 설명하신 글이 있으니, 참고하셔서 보시면 좀 더 쉬울 것 같습니다.
(http://hyunseob.github.io/2016/04/03/webpack-practical-guide/index.html)


저작자 표시 동일 조건 변경 허락
신고
Posted by xyzlast Y2K

Express에서의 Session 사용법

기초중의 기초이지만, 간혹 사용법이 헛갈릴때가 많아서 정리하였습니다.

요구사항

  1. ES6 문법을 기초로 이용
  2. Session을 이용한 로그인 관리
  3. Cluster를 이용가능 (pm2 등을 이용)
  4. login을 통해 얻어진 사용자 이름을 session에 저장.
  5. view는 모두 json을 이용

개발 환경 구성

yo generator중에 generator-express 설치

npm install -g generator-express

특정 directory에서 yo express 실행하면 기본적인 express application이 구성됩니다. 구성된 폴더안에서 gulp default를 실행하면 기본 port 3000번으로 application이 구동됩니다.

Application이 구동되지 않을때.

port 35729가 사용중일때 : gulp.liveload를 이용한 다른 application이 구동되고 있을 때, 발생되는 error. 두개의 application을 동시에 실행하고 싶은 경우에는 gulpfile.js에서 liveload method에 port 번호를 명시. livereload.listen(60000);

config.root undefined가 될때. config/config.js가 정상적으로 로드되지 않은 경우. NODE_ENVdevelopment, production 등으로 지정하지 않은 경우에 발생. 자신의 NODE_ENV를 확인후, NODE_ENV에 맞게 config.js를 수정

추가 npm - express-session, filestore

Session을 이용하기 위해서 express-session 설치합니다. 구 버젼을 기반으로한 책이나 blog에서는 이 과정이 나오지 않습니다. express의 middleware들은 대부분 npm module로 따로 분리가 되어 사용됩니다. express에 기본으로 들어가 있는 경우는 얼마 없습니다.

npm install express-session --save
babel-express-app@0.0.1 /home/ykyoon/dev/code/babel-express-app
└─┬ express-session@1.13.0 
  ├── crc@3.4.0 
  └─┬ uid-safe@2.0.0 
    └── base64-url@1.2.1 

npm WARN optional Skipping failed optional dependency /chokidar/fsevents:
npm WARN notsup Not compatible with your operating system or architecture: fsevents@1.0.7

express.js파일에 express-session을 middleware로 등록합니다.

const session = require('express-session');
app.use(session({
    secret: 'keyboard cat',
    resave: false,
    saveUninitialized: false,
    cookie: { secure: false }
}));

지정되는 parameter는 다음 의미를 갖습니다.

  • secret: session의 암호화에 사용되는 key값입니다.
  • resave: requrest가 요청되었을때, 기존의 session이 존재하는 경우 다시 저장할 필요가 있는지를 확인하는 option입니다. express-session에서 제공되는 touch를 구현하고 있는지를 확인해보면 됩니다. 기본적인 MemoryStore의 경우, touch가 구현되어 있기 때문에 설정할 필요가 없습니다.
  • saveUninitialized: 설정하지 않는 경우, Session이 저장되지 않습니다. 일반적인 로그인을 구성할 때, false로 지정해줘야지 됩니다. 기본값은 true이며 모든 초기화되지 않은 session은 저장되게 됩니다.
  • cookie.secure: https로 호출되는 경우에만 session cookie를 생성하는 option입니다. 실 production에 유용한 option으로 테스트나 개인 공부를 할 때는 false로 지정해주시면 됩니다.
  • cookie.maxAge: cookie가 만료되는 시간을 설정합니다.

login url 구현

DB를 사용하지 않는 최소한의 application이기 때문에 사용자가 입력한 username을 그대로 session의 username으로 넣어주는 code를 구성합니다.

'use strict';

const express = require('express'),
  router = express.Router();

router.get('/login', (req, res) => {
  const session = req.session;
  session.username = req.query.username;
  return res.json(session);
});

module.exports = (app) => {
  app.use('/user', router);
};

원래는 login의 경우, post로 구현해야지 되는 것이 원칙이지만, 테스트를 위한 application이고, browser에서 그대로 구현하기 위해서이니 그냥 get으로 구현하였습니다. 이제 저장된 username을 얻어내야지 됩니다. 이제 browser에서는 저장된 username이 계속 유지가 되어야지 됩니다.

router.get('/info', (req, res) => {
  return res.json(req.session);
});

이제 url을 두개를 연달아 browser에서 실행하면 다음과 같은 결과를 볼 수 있습니다.

{
    "cookie":{"originalMaxAge":null,"expires":null,"secure":false,"httpOnly":true,"path":"/"},
    "username":"abcdef"
}

Session store의 변경

고성능, 고가용성을 구현하기 위해서는 기본적으로 nodejs는 cpu의 core 숫자와 동일한 process숫자로 실행되어야지 됩니다. 이 사실은 지금 application의 경우 심각한 문제를 가지고 있는 것을 의미합니다. process간의 memory 공유는 원칙적으로 되지 않는거니까요. (hacking이나 memory editor의 경우 다르겠지만, web application의 기본 기능과는 거리가 있는 상황이니.)

그래서, nodejs application에서는 공용으로 사용되어야지 되는 정보는 file이나 redis, db와 같은 공용 IO에 저장해서 모든 process가 공유하는 형식으로 사용하게 됩니다. 또한, nodejs application은 application을 종료했다가 다시 올리는 경우가 왕왕 있습니다. express-session의 기본값인 MemoryStore는 기본적으로 production에 올릴수 없는 설정입니다.

성능은 안좋고, multiple instance에서는 절대로 사용할 수 없는 방법이지만, File에 session정보를 저장하기 위해서 FileStore를 이용해보도록 하겠습니다. FileStore와 Session간의 interface를 하는 session-file-store를 설치하면 FileStore가 같이 설치됩니다.

npm install session-file-store --save
babel-express-app@0.0.1 /home/ykyoon/dev/code/babel-express-app
├── filestore@0.1.3 
└─┬ session-file-store@0.0.24 
  ├── bagpipe@0.3.5 
  ├─┬ fs-extra@0.26.5 
  │ ├── jsonfile@2.2.3 
  │ ├── klaw@1.1.3 
  │ └─┬ rimraf@2.5.2 
  │   └── glob@7.0.0 
  └── retry@0.8.0

FileStore를 Session에 설정합니다. 코드는 다음과 같습니다.

const session = require('express-session');
const FileStore = require('session-file-store')(session);
app.use(session({
  secret: 'keyboard cat',
  resave: false,
  saveUninitialized: false,
  cookie: { secure: false },
  store: new FileStore()
}));

설치후, folder를 보면 session라는 folder가 생성되어 있는 것을 볼 수 있습니다. 이제 위에서 실행했던 URL을 다시 실행해보면 File이 하나 생성되어 있는 것을 볼 수 있습니다. 파일을 열어보면, session의 정보가 그대로 저장되어 있는 것을 볼 수 있습니다.

정리

express에서 session을 구성하는 방법에 대해서 간단히 알아봤습니다. 또한 MemoryStore가 아닌 다른 Store를 이용하는 방법에 대해서도 간단히 알아 보았습니다. 실무에서는 대부분 redis를 이용하는 경우가 일반적이긴 하지만, 개발단에서 편하게 알아보기 위해서, File을 사용하는 경우도 꼭 나쁜 것은 아닙니다.


저작자 표시 동일 조건 변경 허락
신고
Posted by xyzlast Y2K

mocha를 이용한 express application test 방법

yo-express를 이용한 application을 test 하는 방법을 한번 알아보고자합니다.

npm module 설치

web에 대한 test는 supertest를 사용하는 것이 좋습니다. 직관적인 사용에도 좋고, mocha에서 사용하기에도 매우 편합니다.

npm install supertest --save-dev

root에 test folder 생성

app.test.js 파일 생성

express application을 먼저 구동하는 것이 필요합니다. 이는 원 app.js에서 구성되는 express start와 sequelize model의 초기화 과정에 대한 process를 실행해야지 되는것을 의미합니다. test라는 상대경로로 옮겨왔기 때문에 경로의 depth가 달라지는 것에 주의할 필요가 있습니다. 그리고 test의 시작전에 항시 실행되어야지 되는 global before이기 때문에 before로 구성되어야지 됩니다.

'use strict';

before(function () {
  var express = require('express'),
    config = require('../config/config'),
    db = require('../app/models');
  var agent = require('supertest');

  var app = express();

  require('../config/express')(app, config);

  db.sequelize
    .sync()
    .then(function () {
      app.listen(config.port, function () {
        console.log('Express server listening on port ' + config.port);
      });
    }).catch(function (e) {
      throw new Error(e);
    });
  console.log('before');
  global.app = app;
  global.agent = agent(app);
});

mocha로 test running

이제 mocha를 이용해서 다음 command로 실행이 가능합니다.

mocha test/app.test.js test/**/*.test.js

mocha.opts 설정

좀더 편한 test 환경을 만들기 위해서 mocha.optsapp.test.js를 넣어두면 편합니다.

--timeout 5000
--full-trace
test/app.test.js

이 때, mocha.opts파일의 위치는 다음과 같이 구성합니다.

└── test
    ├── app.test.js
    ├── controllers
    ├── mocha.opts
    └── models

controller test code 작성

controller test code는 다음과 같이 구성될 수 있습니다. app.test.js에서 supertest의 request를 global.agent로 지정했기 때문에, test에서는 언제나 agent를 접근 가능해서 test code를 작성하기 좀 더 편해집니다.

'use strict';

var assert = require('assert');

describe('func01', function () {
  it('test', function () {
    console.log('abc');
  });

  it('call /', function (done) {
    agent
      .get('/')
      .expect(200)
      .end(function (err, res) {
        // console.log(res);
        done(err);
      });
  });

  it('call / - 2', function (done) {
    agent
      .get('/')
      .expect(500)
      .end(function (err, res) {
        done();
      });
  });
});

Summary

nodejs는 compile 언어가 아니기 때문에, 실행되기 전까지 정상적인 code를 작성했는지를 헛갈릴 수 있습니다. 개인적으로는 원론적으로 test를 거치지 않으면 compile 언어 역시 마찬가지로 문제가 있다고 생각합니다만. test를 잘 만들어주는 것과 test를 잘 할 수 있도록 해주는 것은 자신 뿐 아니라 다른 개발자들에게도 매우 좋은 일들입니다. 이건 정말 잘 할 필요가 있다고 생각합니다.

mocha를 이용한 test는 매우 유용합니다. 만약에 nodejs를 이용한 개발을 하고 있다면, test를 어떻게 할지에 대한 고민을 꼭 해보시길 바랍니다.

저작자 표시 동일 조건 변경 허락
신고
Posted by xyzlast Y2K


티스토리 툴바