블로그 이미지
윤영식
Frontend Application Architecter, Full Stacker, KnowHow Dispenser and Bike Rider

Publication

Statistics Graph

Recent Comment

2016.02.19 12:10 Angular/Concept

모듈단위의 파일을 만들어 모듈간에 모듈을 로딩해서 사용하는 방법을 제시하는 것이 Module Loader의 역할이고, 만들어진 모듈을 어떻게 묶어 사용할지 제시하는 것이 Module Bundler의 역할이다. 이번 글에서는 Module Bundler에 대해 알아보자. 



            





파일 번들링의 일반적인 방식 


index.html 에 <script> 태그를 이용해 첫번째는 개발자가 직접 넣는 방법, 두번째는 Grunt 또는 Gulp의 프론트앤드 툴의 도움을 받아 자동으로 넣는 방법이 있다. 하지만 응답성능에 민감한 애플리케이션에는 초기 다량의 모듈 파일 전송이라는 네트워크 성능이슈를 야기한다. 따라서 운영시에는 좀 더 컴팩하게 파일을 묶을 필요가 있다. 

  - 파일을 합치고(Concatenate)

  - 압축(Minify)하는 과정을 거친다. 


통합하고 압축하는 역할에 대한 플러그인이 Grunt/Gulp에 모두 존재한다. 하지만 CommonJS 또는 AMD나 ES2015 네이티브 로더를 사용할 때는 브라우져에 진화적인 코드로 변환해야 한다. 이때 사용하는 것이 Browserify, Webpack, JSPM 와 같은 모듈 번들러이다. 




Browserify


NodeJS에는 다양한 패키지가 존재하고 NPM(Node Package Manager)를 통해 설치한다. 모든 패키지가 CommonJS에 맞춰 모듈 패턴으로 구현을 하고 있다. 만약 이를 브라우져에서 사용하고 싶을 경우에는 Browserify 의 도움을 받으면 된다.

  - CommonJS를 번들링할 때 사용한다. 

  - AST (Abstract syntaxt tree) 에 따라 require한 모듈에서 require하고 해당 모듈이 require하고 있는 모든 하위의 모듈의 의존 파일을 자동으로 묶어준다. 

  - browserify 명령에서 entry 파일만 지정하면 된다. 의존

// app.js

var math  = require('math');

...


// console

browserify app.js -o bundle.js 




Webpack


AMD 패턴의 모듈을 번들링 할 때는 RequreJS에서 제공하는 r.js를 사용할 수 있다. 하지만 애플리케이션에서 NodeJS의 모듈도 사용하고 AMD 패턴 모듈도 사용한다면 어떻게 될까?  

  - CommonJS, AMD, ES2015 방식에 대한 번들링이 가능하다 

  - Bunlde Chunk라는 단위 묶으로 나누어서 번들링이 가능하다 


* Naver D2 Webpack 상세설명 참조




RollupJS


웹팩과 유사한 차세대 모듈 번들러로는 RollupJS가 있다. 특히 모듈안에 있는 내용중 사용하지 않는 것은 제거하는 Tree-Shaking 기술이 존재한다. 

  - maths.js에 square와 cube 펑션을 export 한다

  - cube만 사용한다. 

// maths.js 

export function square(x) {

 return x*x;


export function cube(x) {

  return x*x*x;

}


// test.js 

import { cube } from './maths.js';

console.log( cube(5) ); 


Tree Saking을 한후 square를 제거한다. 나무를 흔들면 필요없는 것은 떨어지고 필요한 것만 남는 것과 같다. (예제)

(function () {

'use strict';


// This function gets included

function cube ( x ) {

// rewrite this as `square( x ) * x`

// and see what happens!

return x * x * x;

}


console.log( cube( 5 ) ); // 125

}());




 

JSPM 


SystemJS는 모듈을 로딩하는 일관된 API를(System.config, System.import) 제공하고 패키지를 받아오고 로딩하는 역할로 JSPM을 사용할 수 있다.

  - SystemJS를 위한 패키지 메니져이다. 

  - ES6 Module Loader로 불린다

  - npm, bower, GitHub으로 부터 로딩한다. 

  - 브라우져에서 NodeJS 패키지가 browserify와 똑같이 작동한다

  - 개발시에는 개별 파일로 관리하다가 프러덕션에서는 번들링을 한다. 

  - 사용자 가이드


Browserify ==> Webpack ===> JSPM 순서로 정리 해보자. 




Angular2에서의 JSPM/SystemJS
  - JSPM을 패키지 메니져로 사용
  - SystemJS를 모듈 로더로 사용
  - TypeScript를 ES2015 자바스크립트 슈퍼셋으로 사용
  - Angular 2 를 통해 웹, 모바일, 네이티브 개발 플랫폼으로 사용


Module Loader는 CommonJS, AMD, UMD, ES2015 스팩 방식이 있고, Module Bundler는 Browserify, Webapck에서 JSPM(+SystemJS) 방식으로 수렴되고 있다. Angular2를 하게되면 Module Loader로서의 SystemJS와 Module Bundler이면서 패키지 메니져 역할을 수행하는 JSPM을 알아둘 필요가 있다.  

  - Angular 2에서 Bundler에 따른 사용 모듈 목록



<참조>

  - 모듈 번들러 설명 

  - 모듈 번들러 비교 slideshare

  - Choose ES6 Modules

  - Grunt에서 파일들을 통합/압축하는 방법

  - Browserify 와 Webpack 비교 

  - ES2015 스팩  

  - ES2015의 import/export 이야기

  


신고
posted by peter yun 윤영식
2016.02.18 18:02 Angular/Concept

프론트앤드 자바스크립트 개발이 점점 복잡해 짐에 따라 모듈 패턴으로 코드를 작성하고 단일 책임 원칙(Single Responsibility Principle)을 지키는 것이 좋다. 모듈 코드를 작성한 후 모듈을 로딩하고 배포(번들링)하는 다양한 방법들이 존재한다. 먼저 Module Loader에 대해 살펴보고 다음 글에서 Module Bundler에 대해 정리해 본다. 







모듈 패턴


모듈패턴을 사용하는 이유

  - 유지보수성(Maintainability): 단일 책임 원칙에 따라 필요한 기능을 담고 있으면서 별도 폴더와 파일로 유지하면 변경이나 확장 발생시 찾고 수정하기 쉽다. 전제 조건은 외부에 노출하는 API를 일관되게 유지하는 것이 중요하다. 

  - 이름공간(Namespacing): 자바스크립트에서 전역변수를 통한 개발을 하지 않는다. 이를 위해 즉시실행함수표현(IIFE)를 사용하여 전역변수의 오염을 방지하는데, 모듈 패턴 또한 전역변수 오명을 방지한다. 

  - 재사용성(Resuability): 모듈의 성격을 잘 나누어 놓으면 다음 프로젝트에서 그대로 사용해 쓸 수 있다. 보통 SDK나 Base Framework을 만들어 놓으면 초기 구축 비용을 최소로 할 수 있다. 


JohnPapa Angular 스타일 가이드를 보면 특별히 모듈을 지원하는 라이브러리의 도움없이 자바스크립트 모듈 패턴 방식으로 앵귤러 v1 코드를 작성토록 가이드하고 있고, 앵귤러 팀에서도 공식적으로 추천하고 있다. 

(function () {

    'use strict';


    angular

        .module('a3.common.action')

        .factory('currentAction', currentAction);


    /* @ngInject */

    function currentAction(ActionType, stateManager) {

        return {

            setDashboard: setDashboard,

            setWorkspace: setWorkspace

        };


        function setDashboard(dashboardId) {

            var action = {

                ...

            };

            stateManager.dispatch(action);

        }


        function setWorkspace(workspaceId, taskerId) {

            var action = {

                ...

            };

            stateManager.dispatch(action);

        }

    }

})();


순수 자바스크립트로 모듈 단위로 만든 후 상호 운영은 어떻게 해야할까? 일단 index.html에 설정을 하고 사용하는 순서에 index.html에 script 태그를 통해 로딩을 하는 간단한 방식을 생각해 볼 수 있다. 하지만 필요한 시점에 자바스크립트에서 로딩을 해서 사용하는 방식을 명시적으로 하려면 별도 로더의 도움이 필요하다. 




CommonJS & AMD & UMD


CommonJS는 지정한 코드를 동기적으로 로딩하는 방식으로 서버 사이드의 Node.js에서 사용한다. 

  - Object 만을 대상으로 한다.

  - module.exports 구문으로 Object를 export 한다

  - require 구문으로 Object를 import 한다. 

// 파일명: module.js 

function module() {

  this.hi = function () { return 'hi'; }

}

module.exports = module;



// 사용하는 파일: test.js

var module = require('module');

var m = new module();

m.hi();


위에서 require를 차례로 호출하면 동기적으로 하나씩 로딩을 한다. 즉, 비동기적이지 않기 때문에 로딩이 전부 되어야 수행이 된다. 브라우져에서 동기적으로 모듈 파일을 로딩하게 되면 모든 파일이 로딩된 후 화면이 실행되므로 성능 이슈를 야기할 수 있다. 따라서 CommonJS는 Node.js에서 주로 사용하고 브라우져에서는 사용하지 않는다. 


AMD(Asynchronous Module Definition)은 비동적으로 모듈을 로딩한다. 

  - Object, function, constructor, string, JSON 등 다른 타입들도 로딩이 가능한다. 

  - define 구문을 사용한다.

 define(['jquery', 'angular'], function($, angular) { 

   ...

 });


jquery, angular 파일에 대해 비동기적으로 로딩한다. AMD대표적 구현체로는 RequireJS가 있다. 


UMD(Universal Module Definition)은 AMD와 CommonJS의 기능을 둘다 지원하는 것이다. 

  - AMD, CommonJS를 고려한다 

  - 구현체로 SystemJS를 들 수 있다. SystemJS는 Universal dynamic module loader로 AMD, CommonJS뿐만 아니라 브라우져의 global scripts와 NodeJS 패키지 로딩을 하고 Traceur 또는 Babel 과 같이 작동할 수도 있다. 특히, Angular 2에서 사용한다.

  - 아래와 같이 CommonJS와 AMD를 체크하여 사용할 수도 있다. 구현 방식에 대한 다양한 예를 참조한다. 

(function (d3, jQuery) {

    'use strict';

   var Sankey2 = { ... };

   ....


    // Support AMD

    if (typeof define === 'function' && define.amd) {

        define('Sankey2', ['d3'], Sankey2);

    } 

   // Support CommonJS

   else if ('undefined' !== typeof exports && 'undefined' !== typeof module) {

        module.exports = Sankey2;

    } 

   // Support window

   else {

        window.Sankey2 = Sankey2;

    }


})(window.d3, window.$);


브라우져에서 ES6 module loader가 아닌 SystemJS (Universal module loader)를 사용할 경우 System.import 호출로 AMD, CommonJS, ES6 모듈 형식을 로딩할 수 있게 API를 제공하고  패키지 메니져로 JSPM을 사용할 수도 있다. JSPM은 무저항 브라우져용 모듈 패키지 메니져 (frictionless browser package management)로써 ES6 module loader가 작동하지 않는 곳에서 사용하는 Polyfill 이면서 AMD, CommonJS, Globals 자바스크립트 모듈 형식을 로딩할 수 있다. 



Native JS


자바스크립트 ES2015 (ES6)에서 모듈 로더를 공식지원한다. ES2015는 모듈의 importing과 exporting을 제공한다. (참조) 간결관 syntax와 비동기 로딩과 cyclic dependencies에 대해 보다 잘 지원을 한다. 

  - import, export 를 사용한다. 

// app.js 

export let count = 1;


export function hi() {

  return 'hi-' + count++;

}


// test.js

import * as app from './app';


console.log(app.hi());

console.log(app.count);



모듈로더에 대해 정리를 해보자. 자바스크립트 개발시 모듈 패턴에 입각하여 개발할 때 다양한 방식을 사용할 수 있으나

  - NodeJS 기반 서버 사이드 개발은 CommonJS 이고

  - Browser 기반 클라이언트 사이드 개발은 AMD구현체 중 하나인 RequireJS 를 사용한다. 


ppt에서 r.js는 RequireJS에서 제공하는 모듈 번들러이다. 모듈 로더에 맞는 모듈을 개발한 후에 모듈 파일을 운영 배포하기 위해 번들링 즉, 묶는 과정을 거친다. 번들링 방법은 대해 다음 글에서 살펴보자. 




<참조> 

  - 모듈 로딩 다이어그램

  - 모듈 로딩: CommonJS & AMD & UMD

  - 모듈 번들링: Browserify & Webpack 

  - CommonJS와 AMD - D2 

  - UMD 구현 예 

  - SystemJS: Universal dynamic module loader

  - Rollup.js: 차세대 Javascript module bundler

  - ES6 Module Loader Polyfill: Top Level SystemJS

  - ES6 vs CommonJS 비교

  - JavaScript Module Pattern

신고
posted by peter yun 윤영식
2016.01.15 08:16 Angular/Concept

디자인 가이드는 초기 버전의 생각이므로 현재(2015.1.15) beta 버전의 구문 형식과 틀릴 수도 있다는 것을 숙지하고 어떤 사상을 가지고 있는지 알아보는 것에 집중하자. 물고기가 아니라 물고기 잡는 그물에 집중하자는 이야기이다. 




Expressions


Angular의 expression은 Angular1의 expression 과 유사하고 몇가지 더 추가되었다. 


  - Dirty Checking과 긴밀히 통합되어 expression의 prefix 또는 constant 부분에 대한 최적화를 수행한다. 

    좀 더 자세한 사항은 dirty checking 디자인 가이드를 참조한다. 

  - One time binding을 {{::foo}} 와 같이 최초 한번만 바인딩되고 이후 바뀌지 않도록 한다. Angular1에서 사용하던 방법




Directive API


Directive는 ES6 classes를 사용해 정의하고, Annotation을 사용해 meta data를 정의한다.



Class annotation


모든 Directive는 @DecoratorDirective, @TemplateDirective, @ComponentDirective 중 하나의 annotation을 갖는 ES6 class 이다. Annotations는 일반적인 properties를 갖는다. 


  - selector: directive가 적용될 element를 찾을 css selector이다. (string)

  - events: directive가 template안에서 on-.. attribute를 사용해 발생시키는 이벤트 목록이다. (list of string)

  - visibility: element의 subtree안의 directives들이 directive 인스턴스에 접근할 수 있도록 정의한다. ['local' | 'direct-children' | 'any-children']

  - microsyntax: 특별한 properties 구문 형식과 이 properties가 다른 properties와 어떻게 맵핑하는지 정의한다 (string)


아래 예를 보면 NgShowDirective 생성을 통해 element에 ng-show attribute를 사용할 수 있게 한다. 즉, annotation은 툴이 element안에 directive를 인스턴스화하는 과정없이 그리고 코드를 실행하지 않고도 directives를 찾게한다.

import {ng} from '...';


@ng.DecoratorDirective(selector: '[ng-show]')


class NgShowDirective {}



Constructor 에 Denpendency Injection 하기


모든 directives는 Dependency Injection을 통해 contructor의 parameters 값을 갖는다. 즉, DI를 통해 다른 오브젝트 인스턴스틀 갖질 수 있는 것이다. 아래 예를 보면 @Inject annotation을 사용해 DI하고 있다. 

import {ng} from '...';


@ng.DecoratorDirective(selector: '[ng-show]')


class NgShowDirective {

  @Inject(window.HTMLElement, ng.Http, ng.SomeOtherDirective)

  constructor(element, http, someOtherDirective) { ... }

}



Directive communication


http 갖은 것은 ui 오브젝트가 아니기 때문에 element의 subtree안의 모든 directives에 접근할 수 있도록 annotation에 visibility 을 사용할 수 있다. 예로 input element에서 user.value에 대해 NgInputDirective는 input 값의 변경을 Listening하고, NgModelDirective가 validation하며, NgFormDirective은 state를 유지하는 작업을 한다. 여기서 NgModelDirective는 <form> element에 있는 NgFormDirective에 접근할 필요가 있고, NgInputDirective는 같은 element에 있는 NgModelDirective에 접근할 필요가 있다. 이를 아래와 같이 visibility를 설정한다. 

<form>

  <input type="text" ng-model="user.value" required>

</form>


import {ng} from '...';


@ng.DecoratorDirective(

  selector: 'form',

  visibility: 'subtree'

)

class NgFormDirective {

  constructor() { … }

}


@ng.DecoratorDirective(

  selector: 'input[type=text]',

  visibility: 'local'

)

class NgModelDirective {

  @Inject(NgFormDirective)

  constructor(ngForm) { … }

}


@ng.DecoratorDirective(selector: 'input[type=text]',)

class NgInputDirective {

  @Inject(NgModelDirective)

  constructor(ngModel) { … }

}



Bindable properties


Directives의 모든 properties는 데이터 바인딩을 사용할 수 있고, 이를 위해 set(write)에 대해 @ng.PropertySet annotation을 통해 환경설정을 한다. minification 후에도 property 명을 보존하고 directive의 properties에 데이터 바인딩을 쉽게 만들기 위함이다. 


  - 구문: @ng.PropertySet({trigger: ['reference' | 'collection' | 'deferred'], domOnly: [false|true] })

  - trigger

     + reference: reference가 바뀌면 reference 안에서 전달 (default)

     + collection: collection의 entry가 바뀌면 collection 안에서 전달 

     + deferred: directive가 수행할 수 있는 컴파일된 expression을 전달

  - domOnly: DOM만 변경하고 watch하고 있는 다른 property는 제외함 (default: false)

  

예 

import {ng} from '...';


@ng.DecoratorDirective {

  selector: 'dialog'

}

class Dialog {

  constructor() {

    this._content = null;

  }

  get content(content) {

    return this._content;

  }

  @ng.PropertySet({domOnly: true})

  set content(content) {...}

}


<dialog content=”{{1+2}}”>



Properties를 위한 Microsyntax


directive가 여러 값을 다룰 필요가 있을 때 microsyntax로 설정을 한다. 예로 ng-repeat을 하면 track-by expression을 해야할 때. 구문은 다음과 같다. 


  - MICROSYNTAX=(VARIABLE|FIXED|OPTIONAL)+

  - VARIABLE=$\w+

  - FIXED=[^$\[\]]+

  - OPTIONAL=\[MICROSYNTAX\]


ng-repeat을 위한 microsyntax 예를 보면 <div>에 ng-repeat-item-name, ng-repeat-collection, ng-repeat-track-by를 사용한다. 

@TemplateDirective(

  selector: '[ng-repeat]'

  microsyntax: {

   'ng-repeat': '$item-name in $collection [track by $track-by]'

  })

class NgRepeat {

  @ng.PropertySet({trigger: 'reference'})

  set ngRepeatItemName() { … }


  @ng.PropertySet({trigger: 'collection'})

  set ngRepeatCollection() { … }


  @ng.PropertySet({trigger: 'deferred'})

  set ngRepeatTrackByFn;

}



<div ng-repeat="item in items track-by item.id">

<div ng-repeat-item-name="item" ng-repeat-collection="items" ng-repeat-track-by="item.id">



Lifecycle hooks 


모든 directives는 아래 특별한 함수를 구현할 수 있다. 이유는 templates이 initialization과 finalization을 알수 있도록 허용하기 위해 lifecycle hook을 둔다. 


  - attach: templates 안에 바인딩이 된 후 한번만 호출된다. 

  - detach: directive의 element가 destroy될 때 호출된다. 


Component directive는 특별히 아래 함수도 구현할 수 있다. 


  - templateLoaded: component의 template이 로딩되어 component element에 포함될 때 호출된다. 



Events


Directives는 HTMLElement.dispatchEvent를 사용해 사용자 정의 events를 발생시킬 수 있고, HTMLElement.addEventListener를 통해 이벤트를 Listening할 수 있다. 이벤트는 tree를 타고 bubble up 된다. 설정은 아래 예와 같이 class annotation 안에 하고, event가 발생하면 dirty checking 스케쥴이 돌고 template안에 on-... attributes 를 validation한다. 

 @ng.DecoratorDirective(

  selector: 'dialog',

  events: ['close']

)

class Dialog {

  @Inject(window.HTMLElement)

  constructor(element) { ... }

  close() {

    var evt = new Event('close');

    this.element.dispatchEvent(evt);

    if (!evt.defaultPrevented()) {

      // really close the dialog...

    }

  }

}




Directive 구현 예


Decorator: ng-show

import {ng} from '...';


@ng.DecoratorDirective(

  selector: '[ng-show]'  

)

class NgShow {

  @Inject(window.HTMLElement)

  constructor(element) {

    this.element = element;

  }


  set ngShow(value) {

    this.element.style.display = value ? 'block' : 'hidden';

  }

}


Template Directive: ng-if

template 태그의 content를 사용하는 간단한 template directive 이다. 

import {ng} from '...';


@ng.TemplateDirective(

  selector: '[ng-if]'  

)

class NgIf {

  @Inject(ng.ViewHole, ng.ViewFactory, Injector)

  constructor(viewHole, viewFactory, injector) {

    this.viewFactory = viewFactory;

    this.viewHole = viewHole;

    this.injector = injector;

    this.view = null; 

  }


  set ngIf(value) {

    if (this.view) {

      this.viewHole.remove(this.view);

    }

    if (value) {

      this.view = this.viewFactory(injector);

      this.viewHole.add(this.view);

    }

  }

}


Template Direcive: ng-include

import {ng} from '...';


@ng.TemplateDirective(

  selector: '[ng-include]'

)

class NgInclude {

  @Inject(ng.ViewHole, ng.Compiler, ng.Http, ng.Directives,

          Injector)

  constructor(viewHole, compiler, http, directives, injector) {

    this.viewHole = viewHole;

    this.compiler = compiler;

    this.directives = directives;

    this.injector = injector;

    this.view = null;

    this.$http = http;

  }


  set ngInclude(value) {

    var self = this;

    this.$http(value).always(removeView).then(addView);


    function removeView() {

      if (self.view) {

        self.viewHole.remove(self.view);

        self.view = null;

      }

    }

    function addView(templateString) {

      var viewFactory = 

            this.compiler(templateString, this.directives);

      this.view = viewFactory(injector);

      this.viewHole.add(this.view);

    }

  }

}


Templat directive: ng-repeat

template directive는 새로운 execution context를 제공할 수 있고, 아래 간단한 예에서 variable을 injector안에 있는 현재 execution context에서 검증한다. 

import {ng} from '...';


@ng.TemplateDirective(

  selector: '[ng-repeat]'

  microsyntax: {

   'ng-repeat': '$item-name in $collection [track by $track-by]'

  })

class NgRepeat {

  @Inject(ng.ViewHole, ng.ViewFactory, Injector injector)

  constructor(viewHole, viewFactory, injector) {

    this.viewHole = viewHole;

    this.viewFactory = viewFactory;

    this.injector = injector;

    this.views = [];

  }

  @ng.PropertySet({trigger: 'reference'})

  set ngRepeatItemName(value) { 

    this.itemName = value; 

    this.update(); 

  }

  @ng.PropertySet({trigger: 'collection'})

  set ngRepeatCollection(arrayChangeRecord:ArrayChangeRecord) { 

    this.arrayChangeRecord = arrayChangeRecord;

    this.update(); 

  }

  @ng.PropertySet({trigger: 'deferred'})

  set ngRepeatTrackByFn(value) { 

    this.trackByFn = value; 

    this.update();

  }

  update() {

    …

    // deleting a view    

    this.viewHole.remove(deletedView);

    …

    // creating a new view

    var childContext = {

         $index: index,

         $odd: !!index%2,

         $even: !index%2 

    };

    childContext[this.itemName] = this.collection[index];

    var view = this.viewFactory(injector, childContext);

    viewHole.append(view);

   }

   ...

  }

}


Component Directive: pane

title이 있는 pane 구현 예

import {ng} from '...';


@ng.ComponentDirective(

  select: 'pane',

  template: ng.inline('<div>{{title}}</div><content/>'),

  css: ng.url('pane.css'),

)

class sample.Pane {

  constructor() {}

  set title() { … }

}




다른 Web Component 프레임워크에 component를 publishing하거나 consuming하기 



Custom Elements로 X-Tags, Polymer를 사용하는 프레임워크 


Angular는 Custom element API가 유효한지 찾고 custom element로 컴포넌트를 자동 등록하고, component directive의 properties가 custom element의 인스턴스를 접근할 수 있게 한다. 


  - custom element로 Angular component를 export 할 경우 

    + custom element API가 유효하면 Javascript 애플리케이션에서 Angular component를 DOM element 접근방식으로 event의 listen과 properties를 접근가능케 만든다.

  - Angular 애플리케이션에서 custom element 사용할 경우 

    + Angular는 native DOM element를 사용하듯이 custom elements를 사용할 수 있고, event도 listen할 수 있다.



가이드-1 다시 보기



<참조> 


  - Angular Templating 디장인 가이드 

  - Creating, Triggering Events 모질라 문서 

신고
posted by peter yun 윤영식
2016.01.14 08:23 Angular/Concept

이번에는 Angular2 Templating에 대한 Design Guide를 살펴본다. Angular1에서 Angular2로 오면서 살아남은 명칭중 하나는 Directive(지시자)이다. 지시자는 Angular 프레임워크에서 컴포넌트의 지위를 얻을 수 있는 방법을 제공했으나, Scope에 대한 격리 옵션과 모델들의 @/&/= 옵션, ControllerAs 등 문법적으로 알고 가야할 사항이 많아서 처음 접근하는 개발자에게 상당한 Learning Curve가 존재한다.




친숙함과 간결함 사이에서 (Familiarity vs Simplicity)


친숙함은 달콤하다. 지금 잘 알고 있는 것을 가지고 현재를 편하게 즐길 수 있도록 해준다. 하지만 변화에 둔하고 러닝커브가 높을 경우 신규로 알아야 할 사람들에게 많은 시간을 투자하게 만든다. 친숙함이 정말 친숙한 것인지 한번 더 돌아보고 주위 사람들이 왜(Why) 어려워 하고 어떻게(How)하면 쉽고 빠르게 익히고 널리 사용할 수 있도록 무엇(What)으로 만들 수 있을지 기술 리더는 고민해 보아야 한다. 


간결함은 하는 역할이 명확하여 쉽게 이해한다. 배움의 시간을 단축시켜주고 즐거움을 줄 수 있다. 그러나 복잡함을 간결하게 표현하기 위해서는 함축적이다. 따라서 그 내면에 있는 내용을 이해하고 쓰면 더 많은 생각들을 읽을 수 있다. 따라서 간결함을 왜 어떻게 무엇으로 만들었는지 알아 보는 것이 중요하겠다. 그 간결함을 쉽게 설명할 수 있다면... Angular2 Templating은 간결함의 시작중 하나라 본다. 


* 주의) 글에서 Angular 또는 Angular2는 Angular version 2를 지칭하고, AngularJS 또는 Angular1는 Angular version 1를 가르킨다. 


 

Templating 개념 둘러보기


Angular는 DOM element와 행위에 대한 것을 연관(연결) 한다. 여기서 행위(Behavior)를 Directive라고 부른다. 행위에 대한 부분이기 때문에 Directive는 CSS Selector와 맵핑되는 DOM element를 위해 인스터스화된 자바스크립이다. DOM element는 애플리케이션과 상호작용하기 위해 properties와 events를 사용하는데, Directive도 DOM element의 properties와 events의 동일 인터페이스를 사용한다. 


Angular 애플리케이션이 사용자 인터페이스는 "Component Directive"를 사용해 구성한다. Component Directive는 애플리케이션의 Data와 Logic을 저장하고, 또한 사용자 인터페이스를 정의하는 html template도 가진다. 컴포넌트 안에서 element와 상호작용하는 컴포넌트는 Decorator Directive이다. 보통 ng-show="false"와 같이 element의 attribute로 동작하고 엘러먼트에 여러개가 놓일 수 있다. 


Angular는 양방향 데이터 바인딩을 사용한다. 데이터 바인딩은 template안에 특별한 html attribute를 이용해 설정한다. 이때 바인딩은 expression이라고 부르는 것을 이용해 정의한다. expression은 함수(Function)를 호출하거나 데이터에 접근하는 것으로 모든 expression은 execution context안에서 실행된다. 이런 작용은 Dependency Injection Container 안에서 아래와 같이 수행된다.





Directive Type


지시자의 종류 목록


Provides execution context for expressions

Provides a hole in the DOM to insert template instances

Isolates the DOM/expressions/css for reuse

Decorator Directive

no

no

no

Template Directive

can create child execution context, don't have to.

yes

no

Component Directive

isolated execution context, always.

no

yes


Directive에 대한 일반 제약조건은 Directive는 DOM 구조를 제거(remove)/변경(change) 해서는 안된다. Template Directive와 Component Directvie는 별도의 Execution Context를 가질 수 있다. Angular에서 template directive가 execution context를 갖는 것에 유념하자.



Decorator Directive


새로운 element를 만들고 해당 element에서 뭐든 할 수 있다. 그러나 만들지 않은 것은 다른 element는 attribute 변경만 가능하다.



Template Directive


원래 있던 element/template 영역의 특정 부분에 template 인스턴스를 넣을 수 있고, template directive는 자식(child) execution context를 생성한다. 예로 ng-repeat는 row 마다 execution context를 만들지만 ng-if는 부모(parent) execution context를 재사용한다. Template Directive 종류로는 ng-if, ng-repeat, ng-view, ng-switch, ng-include 가 있다. 일반적인 문법을 보자 

<template ng-repeat>

  <div> ... </div>

</template>


template element 자체는 제거되고 template의 일부가 되지 않는다. 즉, template directive는 <template> element만을 허용한다. 중첩으로 아래와 같이 사용할 수도 있다. 

<template ng-repeat>

  <template ng-if>

    <div>...</div>

  </template>

</template>

    

template directives를 사용하면 element에 아래와 같이 template로 변경된다. 

<ul>

  <li ng-repeat>

    <span ng-if> ... </span>

  </li>

</ul>

은 아래 내용과 완전 동일하다

<ul>

  <template ng-repeat>

    <li>

      <template ng-if>

        <span> ... </span>

      </template>

    </li>

  </template>

</ul>


위이 template 태그는 Angular1의 directive의 compile/link 과정과 동일하다. 즉, Angular2에서는 compile/link과정을 template directive로 대체하는 것이다. 


    // Angular1의 이런 구문이 사라지는 것이다. 

    return {  

           ....

            templateUrl: 'plugins/taskers/wafer-traffic/wafer-traffic.html',

            replace: true,

            link: link,

            controller: ctrl,  

            ....

        };

    function link() {}

    function ctrl() {}



Component Directive 


javascript 로직, html template 과 옵션으로 css style을 컴포넌트에 가지고 있다. 이를 위해서 template안의 expressions을 격리(isolate)한다. - execution context의 격리: component directive 인스턴스가 template안의 expresssions를 위한 새로운 execution context가 되는 것이다. 즉, template안의 expressions은 component directive 인스턴스의 함수(function) 또는 properties만 접근 가능하다는 것이다. 이것은 Angular1의 directive 정의시 scope: {...} 격리하는 방법과 controllerAs를 사용해서 this로 접근하는 것등을 개발자가 정의할 필요없이 Component directive로 통일한 것으로 보인다. 프로젝트를 진행하다보면 거의 대부분 scope: { model: '=' } 과 controllerAs를 default로 놓고 사용하기 때문에 프레임워크 단에서 simple화 한 것은 환영할만 하다. 


  // Angular1의 이런 구문이 사라지는 것이다. 

  return {

            restrict: 'EA',

            scope: {

              model: '='

            },

            controller: trafficCtrl,

            controllerAs: 'traffic',

            bindToController: true

        };


- DOM 과 CSS의 격리: Component directives는 다른 directive에서 컨텐츠 변경을 못하도록 Shadow DOM을 사용한다. 따라서 Shadow DOM안에서의 element events는 외부 element로 전파되지 않는다. (buble out of component directive) 

   + Properties와 events: Template은 component의 properties들과 데이터 바인딩 되고, 이벤트는 상위 컴포넌트로 전파할 수 있다. 

   + CSS properties: 템플릿은 Shadow DOM을 기본으로 사용하는 컴포넌트에 한해 스타일을 변경할 수 있다. 

   + Child fragment: 템플릿은 Shadow DOM의 <content> 태그를 사용하는 템플릿을 넣는 자식 elements를 제공한다. 자식 elements의 expression은 밖의 템플릿 execution context와 계속 연결된다. 


expression을 위한 Execution Context는 클래스 인스턴스이고 이를 통해 expression에 대한 type checks등 다양한 일을 수행할 수 있다. 



Directives를 인스턴스화 하는 순서 


자식 elements전에 부모 element의 directives가 먼저 인스턴스화 하고, 만약 template directive가 존재하면 이것을 인스턴스화 하고, 다음으로 element의 decorator directive를 인스턴스화 한다. 순서는 directive 생성자의 dependency injection 순서에 의해 결정된다. 그리고 나서 최종 component directive가 인스턴스화 한다. 즉, 다음과 같다. 


>> Parent Element 

     template directive --> decorator directives --> component directive

         >> Child Elements

                template directive --> decorator directives --> component directive




templates에 데이터 바인딩 설정 방법


elements 또는 directives의 events 와 바인딩 하기


형식: @on-[event name]="[expression]" 

        예) <button on-click="doSomething()">

의미: click 이벤트가 발생하면 doSomething을 호출한다. 

DOM events를 기반으로 하는 thrid party 이벤트 시스템을 바인딩 할 수 있고, Directives에 새로운 이벤트를 정의해도 listener를 달 필요가 없다.



elements 의 properties 와 바인딩 하기


형식: @bind-[property name]="expresion" 

         예) <input bind-value="user.name">

의미: expression인 user.name에 대한 값 변경을 감시하다가 element property값을 갱신 한다. 


만일 element의 property가 바뀌었는데 expression은 writable하지 않거나, expression의 값이 바뀌었는데 property가 writable하지 않으면 에러가 발생한다. 양방향 데이터 바인딩은 directive와 element property 사이에 일어나기 때문이다. element의 attribute로 하지 않고 property로 바인딩 하는 이유는 다음과 같다. 


  - 거의 대부분의 attribute는 대응하는 property를 가진다. 만일 대응하는 property가 없으면 attribute가 대신한다. 

  - element properties는 항시 현재 값을 가진다. 그러나 attributes는 때때로 초기값을 지정해 주어야 한다. 예) <input>의 value attribute.

  - native attributes는 브라우저가 알아서 자동으로 대응하는 property 값을 바꿔준다. 

    예) <img>의 src property는 src attribute 바뀌면 자동을 변경된다)



elements의 properties 넣기 


형식: @[property name="a {{ [expression] }} b"

        예) <input title="some Text {{someValue}}">

의미: <input bind-title=" 'a ' + someValue + ' b' ">  


{{}} 를 사용하면 항상 string으로 변환하고, 단방향 연결이 된다. Angular1 directive의 @ 과 같다. 



text 넣기


형식: {{ [expression] }}

        예) hello {{user}}

의미: hello <span bind-text-content=" ' ' + user "> 


text node 내역과 데이터 바인딩도 항시 string으로 변환되고, 단방향이다. 



Guide-2 이어보기



<참조>


  - Templating 디자인 가이드 

  - Template Syntax 개발자 가이드 



신고
posted by peter yun 윤영식
2016.01.12 07:51 Angular/Prototyping

이번에는 앵귤러 컴포넌트가 어떻게 웹 컴포넌트가 될 수 있는지 보자. 본 내용은 앵귤러 2의 디자인 가이드 중 Web Components에 대한 내용을 바탕으로 한다. (디자인 문서는 초기 버전이고 최신 버전의 변경분 반영이 안될 수 있다. 말 그대로 초기 생각의 흔적으로만 보고, 정의한 것들이 확정되었다곤 생각하지 말자. 따라서 변경된 내역도 나중에 찾아 보아야 한다)



Angular Component as a Web Components 


Angular Component가 Web Components가 되기위한 기본은 events, properties와 자바스크립트나 타 프레임워크에서 접근가능하고 Web Components에 적용되는 methods(API)를 갖는 DOM element를 만드는 것이다. Template 디자인 가이드에 나온 아래 그림을 보면 Template을 갖는 Component Directive로 만들면 Web Components로 Export할 수 있고, Import할 수 있다. 





Publishing events, properties, methods


앵귤러로 컴포넌트를 만들어 보자.

@ComponentDirective({

  selector: 'x-zippy'

  template: ...

})

class ZippyComponent {

  constructor(events) {

    this.events;

  }


  @Property('title')

  get title() ...;

  set title(value) ...;


  @Property('isOpen')

  get isOpen() ...;

  set isOpen(value) {

    if (value != this.isOpen) this.toggle();

  }


  @Publish('open')

  open() {

    this.isOpen = truefalse;

    this.events.fire('open');

  }


  @Publish('close')

  close() {

    this.isOpen = falsetrue;

    this.events.fire('close');

  }


  @Publish('toggle')

  toggle() {

    value ? close() : open();

  }

}


@Proptery 와 @Publish 에노테이션을 통해 앵귤러가 앵귤러 프레임워크없이도 엘러먼트 자체에 접근할 수 있는 properties/methods를 노출하고 있다. 사용예는 다음과 같다.

<x-zippy title="Hi YoungSik">

   Some thing...

</x-zippy>


native 엘러먼트처럼 사용되어 지는 컴포넌트가 되기위해 다음과 같이 동작해야 한다. 

var tab = _lookup_custom_element;

var log = null;

tab.addEventListener('open', function() { log = 'open'; })

tab.addEventListener('close', function() { log = 'close'; })


tab.close();

expect(tab.isOpen).toEqual(false);

expect(log).toEqual('close');


tab.toggle();

expect(tab.isOpen).toEqual(true);

expect(log).toEqual('open');  


 

Custom Element 등록하기(Registering)


Angular Component가 Web Components가 되기 위한 마지막은 앵귤러의 스크프밖에서 인스턴스화 할 수 있어야 한다. 이를 위한 기본 생각은 다음과 같이 Angular가 customElementFactory를 registerElement API를 통해 등록하는 방식이다. 

var injector = angular.createInjector(setOfModules);

var customElementFactory = injector.get(CustomElementFactory);

var Greeter = customElementFactory(GreeterDirective);

document.registerElement('x-greeter', { prototype: Greeter });


var div = document.createElement('div');

div.innerHTML = '<greeter>'; // trigger internal DOM parser.

var greeter = div.firstChild; // retrieve the greeter


// Notice that we are now instance of Greeter

expect(greeter instanceOf Greeter).toBe(true);


// Notice that it can be used just like any other DOM Element

greeter.name = 'world';

greeter.greet();



이후 디자인 가이드 내용이 갑작이 끝나버린 관계로 다른 문서들을 뒤적여 보기로 한다.   To be continued...



<참조>

  - Web Components 디자인 가이드 


신고
posted by peter yun 윤영식
prev 1 2 3 4 5 6 7 8 ··· 88 next