+
한장현
han41858@gmail.com
han41858.tistory.com
1
• 전 삼성SDS 선임
• TV플랫폼 JavaScript 어플리케이션 구현
• 리테일 솔루션 서버 & 프론트엔드 구현
• 프리랜서 개발자
• han41858.tistory.com 블로그 운영
• Angular 2 번역서 집필중
• GDG Korea Web Tech 운영진
한장현 (Janghyun Han)
2
3
ngular
1.6.0-rc.2 safety-insurance
4
• 마술 같은 2-way binding
• HTML 표준을 기반으로 기능 확장
• 체계적인 컴포넌트 구성, Web Component로 가는 길
• Front-end 전체를 커버하는 프레임워크
• Google 에서 관리, 개선
• 풍부한 사용자 삽질 경험
Angular를 쓰는 이유
5
Class, inheritance
import/export
Arrow function
Promise
ECMAScript 2015
6
• 간단해지는 코드, syntax sugar
• 클래스, 상속, 모듈 구성 ⇒ 아키텍처 진화
• 브라우저들 지원 계속
• 결국엔 표준, transpiler는 거쳐갈 뿐
ES6를 쓰는 이유
7
8
ES6ES5
script src
<script src="bundle.js"></script>
"devDependencies": {
"angular": "^1.5.8",
"babel-cli": "^6.18.0",
"babel-loader": "^6.2.7",
"babel-preset-es2015": "^6.18.0",
"webpack": "^1.13.3"
}
<script src="angular.js"></script>
<script src="index.js"></script>
"devDependencies": {
"angular": "^1.5.8"
}
9
index.js
ES6ES5
(function () {
var ngApp = angular.module('angular1es5', []);
})();
import angular from 'angular';
(() => {
const ngApp = angular.module('angular1es6', []);
})();
10
Webpack
// this is ES5
var webpack = require('webpack');
module.exports = {
entry : [
'./index.js'
],
output : {
filename : 'build/bundle.js',
sourceMapFilename: '[name].map'
},
module : {
loaders : [
{
test : /.js$/,
loader : 'babel?presets[]=es2015
exclude : /node_modules/
},
{
test : /.pug$/,
loader : 'pug-loader',
exclude : /node_modules/
}
]
},
plugins : [
// new webpack.optimize.UglifyJsPlugin({minimize: true})
]
};
webpack.config.js
"devDependencies": {
"angular": "^1.5.8",
"angular-route": "^1.5.8",
"babel-cli": "^6.18.0",
"babel-loader": "^6.2.7",
"babel-preset-es2015": "^6.18.0",
"file-loader": "^0.9.0",
"pug": "^2.0.0-beta6",
"pug-loader": "^2.3.0",
"webpack": "^1.13.3",
"webpack-dev-server": "^1.16.2"
}
package.json
11
export default class HomeCtrl {
constructor () {
console.log('HomeCtrl.constructor()');
}
}
/view/homeCtrl.js
p this is home
/view/home.pug
import angular from 'angular';
import ngRoute from 'angular-route';
import HomeCtrl from './view/homeCtrl';
const main = () => {
console.log('main()');
const ngApp = angular.module('angular1es6', ['ngRoute']);
ngApp.config(($routeProvider, $locationProvider) => {
console.log('this is angular config');
$routeProvider
.when('/', {
template : require('./view/home.pug'),
controller : 'HomeCtrl',
controllerAs : 'Ctrl'
})
.otherwise({
redirectTo : '/'
});
// need to angular.js routing
$locationProvider.html5Mode({
enabled : true,
requireBase : false
});
});
ngApp.controller('HomeCtrl', HomeCtrl);
};
main();
index.js + ngRoute
12
webpack-dev-server
13
ngApp.directive('CustomDirective', () => new CustomDirective);
ngApp.filter('groupBy', GroupBy);
ngApp.service('CustomSvc', CustomSvc);
ngApp.controller('CustomCtrl', CustomCtrl);
Angular Components
ngApp.directive('CustomDirective', CustomDirective);
ngApp.filter('groupBy', GroupBy);
ngApp.service('CustomSvc', CustomSvc);
ngApp.controller('CustomCtrl', CustomCtrl);
function
Class
new Class
14
Filter
ES6ES5
ngApp.filter('uppercase', uppercase);
function uppercase () {
return function (item) {
return item.toUpperCase();
};
}
<script src="uppercase.filter.js"></script>
const uppercase = () => {
return (input) => {
return input.toUpperCase();
};
};
export default uppercase;
import uppercase from './uppercase.filter';
ngApp.filter('uppercase', uppercase);
15
.ctrlRoot
p this is home ctrl
p {{ Ctrl.title }}
button(ng-click="Ctrl.select()") Select
export default class HomeCtrl {
constructor () {
console.log('HomeCtrl.constructor()');
this.title = 'this is title';
}
select () {
console.log('HomeCtrl.select()');
}
}
$routeProvider
.when('/', {
template : require('./view/home.pug'),
controller : 'HomeCtrl',
controllerAs : 'Ctrl'
})
ngApp.controller('HomeCtrl', HomeCtrl);
function HomeCtrl ($scope) {
console.log('home controller');
$scope.title = 'this is title';
$scope.select = function () {
console.log('HomeCtrl.select()');
}
}
Controller
ES6ES5
.ctrlRoot
p this is home ctrl
p {{ title }}
button(ng-click="select()") Select
16
Service
ES6ES5
ngApp.service('myService', myService);
<script src="./view/myService.js"></script>
function myService () {
this.testFnc = function () {
console.log('myService.testFnc()');
};
return this;
}
export default class MyService {
constructor () {
console.log('MyService');
}
testFnc () {
console.log('MyService.testFnc()');
}
}
ngApp.service('MyService', MyService);
export default class HomeCtrl {
constructor (MyService) {
this.MyService = MyService;
}
select () {
this.MyService.testFnc();
}
}
static 있어야 할 것 같지만 없어야 함
17
ngApp.directive('myDirective', () => new MyDirective);
export default class MyDirective {
constructor () {
console.log('MyDirective.constructor()');
this.restrict = 'E';
this.template = '<p>message : {{ this.msg }}</p>';
this.scope = {
msg : '@'
};
}
link (scope) {
console.log(scope.msg);
}
}
ngApp.directive('myDirective', myDirective);
function myDirective () {
return {
restrict : 'E',
template : '<p>message : {{ msg }}</p>',
scope : {
msg : '@'
},
controller : function ($scope) {
console.log('myDirective.controller()');
console.log($scope.msg);
}
}
}
<script src="myDirective.js"></script>
Directive
ES6ES5
directive 등록할 때 () => new18
Directive vs. Component
19
Directive vs. Component
const customInput = {
bindings : {
model : '='
},
template : '<input ng-model="$ctrl.model"></input>',
controller : function () {
this.$onInit = () => {
}
}
};
export default customInput;
ngApp.component('customInput', customInput);ngApp.directive('customInput', () => new customInput);
export default class customInput {
constructor () {
this.restrict = 'E';
this.scope = {
model : '='
};
this.template = '<input ng-model="model"></input>';
}
controller () {
}
}
20
static delete (param) {
const self = this;
return util.objValidate(param, {
userID : Constants.TYPE.EMAIL
}, Constants.ERROR.USER_CTRL.NO_PARAMETER, log, 'delete()')
.then(param => self.isExists(param))
.then(param => {
// delete records
return recordCtrl.deleteAll({
userID : param.userID
})
.then(() => {
log('remove records ok');
// param 자체를 다시 돌려주기 위해 Promise 필요
return Promise.resolve(param);
});
})
.then(param => {
// delete cards
return cardCtrl.deleteAll({
userID : param.userID
})
.then(() => {
log('remove cards ok');
return Promise.resolve(param);
});
})
.then(param => {
// delete assets
return assetCtrl.deleteAll({
userID : param.userID
})
.then(() => {
log('remove assets ok');
return Promise.resolve(param);
});
})
.then(param => {
// delete user
return User.remove({
userID : param.userID
})
.then(() => {
return Promise.resolve(param);
}, error => {
log(error);
return Promise.reject(new ERROR(Constants.ERROR.MONGOOSE.REMOVE_FAILED, log, 'delete()'));
});
})
.then(param => {
log(`delete ok : ${param.userID}`);
return Promise.resolve(true);
});
}
Promise
21
ngApp.config(($routeProvider, $locationProvider) => {
// include styles
require('./view/home.styl');
$routeProvider
.when('/', {
template : require('./view/home.pug'),
controller : 'HomeCtrl',
controllerAs : 'Ctrl'
})
.otherwise({
redirectTo : '/'
});
});
배포 : webpack
import가 아니므로 require 22
Angular 1 + ES6 + BDD
= Hell
23
describe('homeCtrl.test', () => {
it('module import', () => {
expect(true).to.be.true;
});
});
λ karma start
(node:7564) DeprecationWarning: Using Buffer without `new` will soon stop working. Use `new
Buffer()`, or preferably `Buffer.from()`, `Buffer.allocUnsafe()` or `Buffer.alloc()` instead.
18 11 2016 02:53:45.852:INFO [framework.browserify]: bundle built
18 11 2016 02:53:45.941:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/
18 11 2016 02:53:45.941:INFO [launcher]: Launching browser Chrome with unlimited concurrency
18 11 2016 02:53:45.952:INFO [launcher]: Starting browser Chrome
18 11 2016 02:53:47.283:INFO [Chrome 54.0.2840 (Windows 10 0.0.0)]: Connected on socket
/#AjAqCwTlrwmVmV_sAAAA with id 16857313
Chrome 54.0.2840 (Windows 10 0.0.0): Executed 1 of 1 SUCCESS (0.005 secs / 0.001 secs)
BDD 시작
24
λ karma start
(node:12196) DeprecationWarning: Using Buffer without `new` will soon stop working. Use `new Buffer()`, or preferably
`Buffer.from()`, `Buffer.allocUnsafe()` or `Buffer.alloc()` instead.
18 11 2016 02:19:10.237:INFO [framework.browserify]: bundle built
18 11 2016 02:19:10.343:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/
18 11 2016 02:19:10.343:INFO [launcher]: Launching browser Chrome with unlimited concurrency
18 11 2016 02:19:10.353:INFO [launcher]: Starting browser Chrome
18 11 2016 02:19:11.676:INFO [Chrome 54.0.2840 (Windows 10 0.0.0)]: Connected on socket /#sBpP4RL0XFZAwPtxAAAA with id 52822107
Chrome 54.0.2840 (Windows 10 0.0.0) ERROR
Uncaught SyntaxError: Unexpected token import
at test/homeCtrl.test.js:1
import HomeCtrl from '../view/homeCtrl';
describe('homeCtrl.test', () => {
it('module import', () => {
console.log(HomeCtrl);
expect(true).to.be.true;
expect(HomeCtrl).to.be.ok;
});
});
λ karma start
(node:11580) DeprecationWarning: Using Buffer without `new` will soon stop working. Use `new Buffer()`, or preferably
`Buffer.from()`, `Buffer.allocUnsafe()` or `Buffer.alloc()` instead.
18 11 2016 02:31:24.248:INFO [framework.browserify]: bundle built
18 11 2016 02:31:24.339:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/
18 11 2016 02:31:24.339:INFO [launcher]: Launching browser Chrome with unlimited concurrency
18 11 2016 02:31:24.349:INFO [launcher]: Starting browser Chrome
18 11 2016 02:31:25.657:INFO [Chrome 54.0.2840 (Windows 10 0.0.0)]: Connected on socket /#pvpyGrXqq2TZPTgmAAAA with id 30236974
LOG: class HomeCtrl { ... }
Chrome 54.0.2840 (Windows 10 0.0.0): Executed 1 of 1 SUCCESS (0.007 secs / 0.002 secs)
babel : node6
λ karma start
(node:13196) DeprecationWarning: Using Buffer without `new` will soon stop working. Use `new Buffer()`, or preferably
`Buffer.from()`, `Buffer.allocUnsafe()` or `Buffer.alloc()` instead.
18 11 2016 02:58:38.638:INFO [framework.browserify]: bundle built
18 11 2016 02:58:38.728:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/
18 11 2016 02:58:38.729:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
18 11 2016 02:58:38.738:INFO [launcher]: Starting browser PhantomJS
18 11 2016 02:58:40.301:INFO [PhantomJS 2.1.1 (Windows 8 0.0.0)]: Connected on socket /#JeRwavdozVZCC8HJAAAA with id 75347319
LOG: function HomeCtrl(MyService) { ... }
PhantomJS 2.1.1 (Windows 8 0.0.0): Executed 1 of 1 SUCCESS (0.008 secs / 0.001 secs)
babel : es2015
ES6가 돌지 않는다…
+
25
var $httpBackend;
beforeEach(function () {
module('angular1es5');
inject(function (_$httpBackend_) {
$httpBackend = _$httpBackend_;
});
});
module 선언, injection 불가
ES6ES5
const $injector, $httpBackend;
beforeEach(() => {
$injector = angular.injector(['angular1es6']);
$httpBackend = $injector.get('$httpBackend');
});
Object is not a constructor (evaluating 'module('angular1es6')')
r:/temp/test/homeCtrl.test.js:15:9 <-
R:/temp/3871fde1c6cf6c302eeae7add18a3b02.browserify:22:9
26
let ngApp = angular.module('angular1es6', ['ngMock']);
ngMock vs. ngMockE2E
The ngMock module provides support to inject
and mock Angular services into unit tests. In
addition, ngMock also extends various core ng
services such that they can be inspected and
controlled in a synchronous manner within test
code.
The ngMockE2E is an angular module which
contains mocks suitable for end-to-end testing.
Currently there is only one mock present in this
module - the e2e $httpBackend mock.
ngMock ngMockE2E
Fake HTTP backend implementation suitable for end-to-
end testing or backend-less development of applications
that use the $http service.
Fake HTTP backend implementation suitable for unit
testing applications that use the $http service.
.when()
.expect()
.flush()
.verifyNoOutstandingExpectation()
.verifyNoOutstandingRequest()
.resetExpectations()
.when()
ngMockE2E에는 $location 없음 27
Promise + http.flush()
Chrome 54.0.2840 (Windows 10 0.0.0) homeCtrl.test http test ok FAILED
Error: No pending request to flush !
at Function.$httpBackend.flush (node_modules/angular-mocks/angular-mocks.js:1799:34)
at r:/temp/test/homeCtrl.test.js:36:17
promise 함수로 부르면 동작 안함
flush() 타이밍 달라짐
flush는 Util에서 수행
const $injector = angular.injector(['angular1es6']);
const $httpBackend = $injector.get('$httpBackend');
const $http = $injector.get('$http');
beforeEach(() => {
$httpBackend.expectGET('/test')
.respond(['this', 'is', 'GET', 'test', 'data']);
});
it('ok', () => {
return new Promise(resolve => {
$http.get('/test').then(result => {
console.log('get().then()');
console.log(result);
console.log(result.data);
resolve(true);
});
$httpBackend.flush();
});
});
동작하는 코드
static post (uri, param, config) {
// use new promise for flush()
return new Promise((resolve, reject) => {
this.http.post(uri, param, config)
.then(result => {
resolve(result);
}, error => {
console.error(error);
reject(false);
});
if (this.httpBackend && this.httpBackend.flush) {
this.httpBackend.flush();
}
});
}
it('ok', () => {
return new Promise(resolve => {
Promise.resolve()
.then(() => {
$http.get('/test').then(result => {
console.log('get().then()');
console.log(result);
console.log(result.data);
resolve(true);
});
});
$httpBackend.flush();
});
});
28
const $httpBackend = $injector.get('$httpBackend');
const $http = $injector.get('$http');
ClientUtil.http = $http;
ClientUtil.httpBackend = $httpBackend;
여러 test 파일 동시 실행
const $injector = angular.injector(['angular1es6']);
const $httpBackend = $injector.get('$httpBackend');
const $http = $injector.get('$http');
Chrome 54.0.2840 (Windows 10 0.0.0) SignInCtrl test logic submit() with ajax error response
response error test FAILED
AssertionError: expected [Error: Unexpected request: POST /api/user/signIn
No more request expected] to be an instance of ERROR
Chrome 54.0.2840 (Windows 10 0.0.0) SignUpCtrl test logic submit() error response response
error test FAILED
AssertionError: expected [Error: [$rootScope:inprog] $digest already in progress
const ngApp = angular.module(appName, ['ngMock']);
var $injector = angular.injector(['MoneyBook']);
const $httpBackend = $injector.get('$httpBackend');
const $http = $injector.get('$http');
ClientUtil.http = $http;
ClientUtil.httpBackend = $httpBackend;
29
생성자 안에서 Promise
export default class HomeCtrl {
constructor () {
console.log('HomeCtrl.constructor()');
Promise.resolve(() => {
// do something
});
}
}
describe('constructor()', () => {
it('ok', () => {
// expect what...?
});
});
export default class HomeCtrl {
constructor () {
console.log('HomeCtrl.constructor()');
this.somethingPromise();
}
somethingPromise(){
return Promise.resolve()
.then() => {
// do something
});
}
}
30
Promise + $scope.$apply()
export default class HomeCtrl {
constructor () {
console.log('HomeCtrl.constructor()');
this.count = 0;
}
select () {
console.log('HomeCtrl.select()');
return Promise.resolve()
.then(() => {
this.count++;
});
}
}
export default class HomeCtrl {
constructor ($scope) {
console.log('HomeCtrl.constructor()');
this.$scope = $scope;
this.count = 0;
}
select () {
console.log('HomeCtrl.select()');
return Promise.resolve()
.then(() => {
this.count++;
this.$scope.$apply();
});
}
}
31
+ =
32
33
https://github.com/han41858/angular1-es6
boilerplate
Q & A
34
감사합니다.
han41858@gmail.com
han41858.tistory.com
35

Angular 1 + es6

  • 1.
  • 2.
    • 전 삼성SDS선임 • TV플랫폼 JavaScript 어플리케이션 구현 • 리테일 솔루션 서버 & 프론트엔드 구현 • 프리랜서 개발자 • han41858.tistory.com 블로그 운영 • Angular 2 번역서 집필중 • GDG Korea Web Tech 운영진 한장현 (Janghyun Han) 2
  • 3.
  • 4.
  • 5.
    • 마술 같은2-way binding • HTML 표준을 기반으로 기능 확장 • 체계적인 컴포넌트 구성, Web Component로 가는 길 • Front-end 전체를 커버하는 프레임워크 • Google 에서 관리, 개선 • 풍부한 사용자 삽질 경험 Angular를 쓰는 이유 5
  • 6.
  • 7.
    • 간단해지는 코드,syntax sugar • 클래스, 상속, 모듈 구성 ⇒ 아키텍처 진화 • 브라우저들 지원 계속 • 결국엔 표준, transpiler는 거쳐갈 뿐 ES6를 쓰는 이유 7
  • 8.
  • 9.
    ES6ES5 script src <script src="bundle.js"></script> "devDependencies":{ "angular": "^1.5.8", "babel-cli": "^6.18.0", "babel-loader": "^6.2.7", "babel-preset-es2015": "^6.18.0", "webpack": "^1.13.3" } <script src="angular.js"></script> <script src="index.js"></script> "devDependencies": { "angular": "^1.5.8" } 9
  • 10.
    index.js ES6ES5 (function () { varngApp = angular.module('angular1es5', []); })(); import angular from 'angular'; (() => { const ngApp = angular.module('angular1es6', []); })(); 10
  • 11.
    Webpack // this isES5 var webpack = require('webpack'); module.exports = { entry : [ './index.js' ], output : { filename : 'build/bundle.js', sourceMapFilename: '[name].map' }, module : { loaders : [ { test : /.js$/, loader : 'babel?presets[]=es2015 exclude : /node_modules/ }, { test : /.pug$/, loader : 'pug-loader', exclude : /node_modules/ } ] }, plugins : [ // new webpack.optimize.UglifyJsPlugin({minimize: true}) ] }; webpack.config.js "devDependencies": { "angular": "^1.5.8", "angular-route": "^1.5.8", "babel-cli": "^6.18.0", "babel-loader": "^6.2.7", "babel-preset-es2015": "^6.18.0", "file-loader": "^0.9.0", "pug": "^2.0.0-beta6", "pug-loader": "^2.3.0", "webpack": "^1.13.3", "webpack-dev-server": "^1.16.2" } package.json 11
  • 12.
    export default classHomeCtrl { constructor () { console.log('HomeCtrl.constructor()'); } } /view/homeCtrl.js p this is home /view/home.pug import angular from 'angular'; import ngRoute from 'angular-route'; import HomeCtrl from './view/homeCtrl'; const main = () => { console.log('main()'); const ngApp = angular.module('angular1es6', ['ngRoute']); ngApp.config(($routeProvider, $locationProvider) => { console.log('this is angular config'); $routeProvider .when('/', { template : require('./view/home.pug'), controller : 'HomeCtrl', controllerAs : 'Ctrl' }) .otherwise({ redirectTo : '/' }); // need to angular.js routing $locationProvider.html5Mode({ enabled : true, requireBase : false }); }); ngApp.controller('HomeCtrl', HomeCtrl); }; main(); index.js + ngRoute 12
  • 13.
  • 14.
    ngApp.directive('CustomDirective', () =>new CustomDirective); ngApp.filter('groupBy', GroupBy); ngApp.service('CustomSvc', CustomSvc); ngApp.controller('CustomCtrl', CustomCtrl); Angular Components ngApp.directive('CustomDirective', CustomDirective); ngApp.filter('groupBy', GroupBy); ngApp.service('CustomSvc', CustomSvc); ngApp.controller('CustomCtrl', CustomCtrl); function Class new Class 14
  • 15.
    Filter ES6ES5 ngApp.filter('uppercase', uppercase); function uppercase() { return function (item) { return item.toUpperCase(); }; } <script src="uppercase.filter.js"></script> const uppercase = () => { return (input) => { return input.toUpperCase(); }; }; export default uppercase; import uppercase from './uppercase.filter'; ngApp.filter('uppercase', uppercase); 15
  • 16.
    .ctrlRoot p this ishome ctrl p {{ Ctrl.title }} button(ng-click="Ctrl.select()") Select export default class HomeCtrl { constructor () { console.log('HomeCtrl.constructor()'); this.title = 'this is title'; } select () { console.log('HomeCtrl.select()'); } } $routeProvider .when('/', { template : require('./view/home.pug'), controller : 'HomeCtrl', controllerAs : 'Ctrl' }) ngApp.controller('HomeCtrl', HomeCtrl); function HomeCtrl ($scope) { console.log('home controller'); $scope.title = 'this is title'; $scope.select = function () { console.log('HomeCtrl.select()'); } } Controller ES6ES5 .ctrlRoot p this is home ctrl p {{ title }} button(ng-click="select()") Select 16
  • 17.
    Service ES6ES5 ngApp.service('myService', myService); <script src="./view/myService.js"></script> functionmyService () { this.testFnc = function () { console.log('myService.testFnc()'); }; return this; } export default class MyService { constructor () { console.log('MyService'); } testFnc () { console.log('MyService.testFnc()'); } } ngApp.service('MyService', MyService); export default class HomeCtrl { constructor (MyService) { this.MyService = MyService; } select () { this.MyService.testFnc(); } } static 있어야 할 것 같지만 없어야 함 17
  • 18.
    ngApp.directive('myDirective', () =>new MyDirective); export default class MyDirective { constructor () { console.log('MyDirective.constructor()'); this.restrict = 'E'; this.template = '<p>message : {{ this.msg }}</p>'; this.scope = { msg : '@' }; } link (scope) { console.log(scope.msg); } } ngApp.directive('myDirective', myDirective); function myDirective () { return { restrict : 'E', template : '<p>message : {{ msg }}</p>', scope : { msg : '@' }, controller : function ($scope) { console.log('myDirective.controller()'); console.log($scope.msg); } } } <script src="myDirective.js"></script> Directive ES6ES5 directive 등록할 때 () => new18
  • 19.
  • 20.
    Directive vs. Component constcustomInput = { bindings : { model : '=' }, template : '<input ng-model="$ctrl.model"></input>', controller : function () { this.$onInit = () => { } } }; export default customInput; ngApp.component('customInput', customInput);ngApp.directive('customInput', () => new customInput); export default class customInput { constructor () { this.restrict = 'E'; this.scope = { model : '=' }; this.template = '<input ng-model="model"></input>'; } controller () { } } 20
  • 21.
    static delete (param){ const self = this; return util.objValidate(param, { userID : Constants.TYPE.EMAIL }, Constants.ERROR.USER_CTRL.NO_PARAMETER, log, 'delete()') .then(param => self.isExists(param)) .then(param => { // delete records return recordCtrl.deleteAll({ userID : param.userID }) .then(() => { log('remove records ok'); // param 자체를 다시 돌려주기 위해 Promise 필요 return Promise.resolve(param); }); }) .then(param => { // delete cards return cardCtrl.deleteAll({ userID : param.userID }) .then(() => { log('remove cards ok'); return Promise.resolve(param); }); }) .then(param => { // delete assets return assetCtrl.deleteAll({ userID : param.userID }) .then(() => { log('remove assets ok'); return Promise.resolve(param); }); }) .then(param => { // delete user return User.remove({ userID : param.userID }) .then(() => { return Promise.resolve(param); }, error => { log(error); return Promise.reject(new ERROR(Constants.ERROR.MONGOOSE.REMOVE_FAILED, log, 'delete()')); }); }) .then(param => { log(`delete ok : ${param.userID}`); return Promise.resolve(true); }); } Promise 21
  • 22.
    ngApp.config(($routeProvider, $locationProvider) =>{ // include styles require('./view/home.styl'); $routeProvider .when('/', { template : require('./view/home.pug'), controller : 'HomeCtrl', controllerAs : 'Ctrl' }) .otherwise({ redirectTo : '/' }); }); 배포 : webpack import가 아니므로 require 22
  • 23.
    Angular 1 +ES6 + BDD = Hell 23
  • 24.
    describe('homeCtrl.test', () =>{ it('module import', () => { expect(true).to.be.true; }); }); λ karma start (node:7564) DeprecationWarning: Using Buffer without `new` will soon stop working. Use `new Buffer()`, or preferably `Buffer.from()`, `Buffer.allocUnsafe()` or `Buffer.alloc()` instead. 18 11 2016 02:53:45.852:INFO [framework.browserify]: bundle built 18 11 2016 02:53:45.941:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/ 18 11 2016 02:53:45.941:INFO [launcher]: Launching browser Chrome with unlimited concurrency 18 11 2016 02:53:45.952:INFO [launcher]: Starting browser Chrome 18 11 2016 02:53:47.283:INFO [Chrome 54.0.2840 (Windows 10 0.0.0)]: Connected on socket /#AjAqCwTlrwmVmV_sAAAA with id 16857313 Chrome 54.0.2840 (Windows 10 0.0.0): Executed 1 of 1 SUCCESS (0.005 secs / 0.001 secs) BDD 시작 24
  • 25.
    λ karma start (node:12196)DeprecationWarning: Using Buffer without `new` will soon stop working. Use `new Buffer()`, or preferably `Buffer.from()`, `Buffer.allocUnsafe()` or `Buffer.alloc()` instead. 18 11 2016 02:19:10.237:INFO [framework.browserify]: bundle built 18 11 2016 02:19:10.343:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/ 18 11 2016 02:19:10.343:INFO [launcher]: Launching browser Chrome with unlimited concurrency 18 11 2016 02:19:10.353:INFO [launcher]: Starting browser Chrome 18 11 2016 02:19:11.676:INFO [Chrome 54.0.2840 (Windows 10 0.0.0)]: Connected on socket /#sBpP4RL0XFZAwPtxAAAA with id 52822107 Chrome 54.0.2840 (Windows 10 0.0.0) ERROR Uncaught SyntaxError: Unexpected token import at test/homeCtrl.test.js:1 import HomeCtrl from '../view/homeCtrl'; describe('homeCtrl.test', () => { it('module import', () => { console.log(HomeCtrl); expect(true).to.be.true; expect(HomeCtrl).to.be.ok; }); }); λ karma start (node:11580) DeprecationWarning: Using Buffer without `new` will soon stop working. Use `new Buffer()`, or preferably `Buffer.from()`, `Buffer.allocUnsafe()` or `Buffer.alloc()` instead. 18 11 2016 02:31:24.248:INFO [framework.browserify]: bundle built 18 11 2016 02:31:24.339:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/ 18 11 2016 02:31:24.339:INFO [launcher]: Launching browser Chrome with unlimited concurrency 18 11 2016 02:31:24.349:INFO [launcher]: Starting browser Chrome 18 11 2016 02:31:25.657:INFO [Chrome 54.0.2840 (Windows 10 0.0.0)]: Connected on socket /#pvpyGrXqq2TZPTgmAAAA with id 30236974 LOG: class HomeCtrl { ... } Chrome 54.0.2840 (Windows 10 0.0.0): Executed 1 of 1 SUCCESS (0.007 secs / 0.002 secs) babel : node6 λ karma start (node:13196) DeprecationWarning: Using Buffer without `new` will soon stop working. Use `new Buffer()`, or preferably `Buffer.from()`, `Buffer.allocUnsafe()` or `Buffer.alloc()` instead. 18 11 2016 02:58:38.638:INFO [framework.browserify]: bundle built 18 11 2016 02:58:38.728:INFO [karma]: Karma v1.3.0 server started at http://localhost:9876/ 18 11 2016 02:58:38.729:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency 18 11 2016 02:58:38.738:INFO [launcher]: Starting browser PhantomJS 18 11 2016 02:58:40.301:INFO [PhantomJS 2.1.1 (Windows 8 0.0.0)]: Connected on socket /#JeRwavdozVZCC8HJAAAA with id 75347319 LOG: function HomeCtrl(MyService) { ... } PhantomJS 2.1.1 (Windows 8 0.0.0): Executed 1 of 1 SUCCESS (0.008 secs / 0.001 secs) babel : es2015 ES6가 돌지 않는다… + 25
  • 26.
    var $httpBackend; beforeEach(function (){ module('angular1es5'); inject(function (_$httpBackend_) { $httpBackend = _$httpBackend_; }); }); module 선언, injection 불가 ES6ES5 const $injector, $httpBackend; beforeEach(() => { $injector = angular.injector(['angular1es6']); $httpBackend = $injector.get('$httpBackend'); }); Object is not a constructor (evaluating 'module('angular1es6')') r:/temp/test/homeCtrl.test.js:15:9 <- R:/temp/3871fde1c6cf6c302eeae7add18a3b02.browserify:22:9 26 let ngApp = angular.module('angular1es6', ['ngMock']);
  • 27.
    ngMock vs. ngMockE2E ThengMock module provides support to inject and mock Angular services into unit tests. In addition, ngMock also extends various core ng services such that they can be inspected and controlled in a synchronous manner within test code. The ngMockE2E is an angular module which contains mocks suitable for end-to-end testing. Currently there is only one mock present in this module - the e2e $httpBackend mock. ngMock ngMockE2E Fake HTTP backend implementation suitable for end-to- end testing or backend-less development of applications that use the $http service. Fake HTTP backend implementation suitable for unit testing applications that use the $http service. .when() .expect() .flush() .verifyNoOutstandingExpectation() .verifyNoOutstandingRequest() .resetExpectations() .when() ngMockE2E에는 $location 없음 27
  • 28.
    Promise + http.flush() Chrome54.0.2840 (Windows 10 0.0.0) homeCtrl.test http test ok FAILED Error: No pending request to flush ! at Function.$httpBackend.flush (node_modules/angular-mocks/angular-mocks.js:1799:34) at r:/temp/test/homeCtrl.test.js:36:17 promise 함수로 부르면 동작 안함 flush() 타이밍 달라짐 flush는 Util에서 수행 const $injector = angular.injector(['angular1es6']); const $httpBackend = $injector.get('$httpBackend'); const $http = $injector.get('$http'); beforeEach(() => { $httpBackend.expectGET('/test') .respond(['this', 'is', 'GET', 'test', 'data']); }); it('ok', () => { return new Promise(resolve => { $http.get('/test').then(result => { console.log('get().then()'); console.log(result); console.log(result.data); resolve(true); }); $httpBackend.flush(); }); }); 동작하는 코드 static post (uri, param, config) { // use new promise for flush() return new Promise((resolve, reject) => { this.http.post(uri, param, config) .then(result => { resolve(result); }, error => { console.error(error); reject(false); }); if (this.httpBackend && this.httpBackend.flush) { this.httpBackend.flush(); } }); } it('ok', () => { return new Promise(resolve => { Promise.resolve() .then(() => { $http.get('/test').then(result => { console.log('get().then()'); console.log(result); console.log(result.data); resolve(true); }); }); $httpBackend.flush(); }); }); 28 const $httpBackend = $injector.get('$httpBackend'); const $http = $injector.get('$http'); ClientUtil.http = $http; ClientUtil.httpBackend = $httpBackend;
  • 29.
    여러 test 파일동시 실행 const $injector = angular.injector(['angular1es6']); const $httpBackend = $injector.get('$httpBackend'); const $http = $injector.get('$http'); Chrome 54.0.2840 (Windows 10 0.0.0) SignInCtrl test logic submit() with ajax error response response error test FAILED AssertionError: expected [Error: Unexpected request: POST /api/user/signIn No more request expected] to be an instance of ERROR Chrome 54.0.2840 (Windows 10 0.0.0) SignUpCtrl test logic submit() error response response error test FAILED AssertionError: expected [Error: [$rootScope:inprog] $digest already in progress const ngApp = angular.module(appName, ['ngMock']); var $injector = angular.injector(['MoneyBook']); const $httpBackend = $injector.get('$httpBackend'); const $http = $injector.get('$http'); ClientUtil.http = $http; ClientUtil.httpBackend = $httpBackend; 29
  • 30.
    생성자 안에서 Promise exportdefault class HomeCtrl { constructor () { console.log('HomeCtrl.constructor()'); Promise.resolve(() => { // do something }); } } describe('constructor()', () => { it('ok', () => { // expect what...? }); }); export default class HomeCtrl { constructor () { console.log('HomeCtrl.constructor()'); this.somethingPromise(); } somethingPromise(){ return Promise.resolve() .then() => { // do something }); } } 30
  • 31.
    Promise + $scope.$apply() exportdefault class HomeCtrl { constructor () { console.log('HomeCtrl.constructor()'); this.count = 0; } select () { console.log('HomeCtrl.select()'); return Promise.resolve() .then(() => { this.count++; }); } } export default class HomeCtrl { constructor ($scope) { console.log('HomeCtrl.constructor()'); this.$scope = $scope; this.count = 0; } select () { console.log('HomeCtrl.select()'); return Promise.resolve() .then(() => { this.count++; this.$scope.$apply(); }); } } 31
  • 32.
  • 33.
  • 34.
  • 35.

Editor's Notes

  • #4 오늘 언급할 라이브러리들
  • #5 https://github.com/angular/angular.js/blob/master/CHANGELOG.md 1.6.0-rc.2 safety-insurance (2016-11-24)
  • #6 지속적인 성능 개선, 버그 수정 two-way binding is magic 체계적인 앱 프론트엔드 구성 사용자 삽질 경험 풍부, 웬만한 문제는 해결 가능 Angular 2는 아직..
  • #7 http://www.ecma-international.org/ecma-262/6.0/ ECMAScript 2015 2015-6 발표 2016-6-20 개정 ECMAScript 2016, ES7 2016-6 Exponentiation operator ** Array.prototype.includes
  • #8 transpiler들의 지원 babel babelify browserify traceur
  • #10 ES6에서 angular.js 포함 없음 webpack 쓸거니까
  • #11 webpack bundling을 위해 html이 아니라 js에서 추가 import 들… 코드에서 직접 쓰지는 않더라도 의존성을 위해 추가해야 함(script src 대신) ex) import ngRoute 없으면 angular module injection failed html script 나열이 아니라서 webpack dependency를 찾아가는 것이 편하기도 함
  • #13 webpack을 쓰니까 pug 를 require로 사용 controllerAs 없으면 스코프 접근 불가
  • #15 모두 class로 export filter, directive 등록에는 new 필요, 없으면 에러 service, controller 등록에는 안 필요
  • #17 controller 대체, controllerAs 사용하지 않으면 접근 불가 $scope 대신 this 사용, private function 불가
  • #18 angular.service 와 ES6 class static은 연관되지만… static 있으면 안돌아감. 없어야 prototype chain으로 연결됨 ngApp.service() 필요 없음, babel 때문에
  • #19 생성자의 재발견 상속이면 super() 필요 ES5 directive의 return보다 보기 편함 controller 대체 $scope 대신 this 사용, private function 불가
  • #20 after Angular 1.5
  • #21 componen는 directiv와 사용방법이 다름 component 쓸때는 객체를 반환 directive는 클래스를 반환, new 선언 componen에서는 element, attr 등 접근 어려움 component는 Element만 사용 가능, 어차피 directive 필요 .directive()는 쓰자
  • #22 어차피 표준, 가독성 엄청 향상 catch로 끝내지 않도록 주의, 콘솔이 안 이쁘잖아.. callback hell, pyramid of doom
  • #23 template, style 로드 간편 서버와 공통 로직 그대로 사용(ex. validation)
  • #24 BDD 작성 시작 헬 시작 참고 자료 찾기 어려움
  • #25 Chrome에서는 돈다. PhantomJS에서는 돌지 않음
  • #26 브라우저에서 ES6지원하지 않으면 karma 돌지 않음… ES6니까 node6, es2015 babel preset 필요 Chrome에서도 import는 안됨 import 없이 arrow function은 동작함 webpack → karma : bundling, 오류 찾기 힘들다 karma-browerify → karma-babelify → karma : O
  • #27 import 구문이 아니라서 찾을 수 없음 browserify, babelify를 거치니까.. karma.conf.js에 angular 추가, 수동 injector 사용
  • #28 http 이후 $location 테스트하면서 문제 발생 같은 파일이지만 angular.module dependency 선언에서 ngMock과 ngMockE2E로 갈림 동시에 사용 불가, ngMockE2E에는 $location이 없음 ngMock : fake response for unit test ngMockE2E : fake response for end-to-end test
  • #29 promise 쓰니까 http.flush() 타이밍 맞지 않음 유틸 객체에 프로퍼티로 지정
  • #30 한 번에 돌리면 에러 http injection 문제, 전역에서 한 번만 지정
  • #31 생성자 안에서 Promise 쓰는 경우 결과 확인 불가 함수 분리. Promise 완료를 기다릴 수 없음
  • #32 Promise 안에서 클래스 프로퍼티 값을 바꾼 경우 한 cycle 늦게 view에 적용됨 $scope.$apply() 호출 필요
  • #33 할 만 하다 어차피 ES6는 표준 가독성이 향상되는 코드가 많음
  • #35 Angular usage in Google (etc : https://builtwith.angularjs.org/) Double Click For Advertiser YouTube for PS3 Google Cloud Developers Console Google Cast