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

Publication

Category

Recent Post

2013. 3. 26. 18:29 Backbone.js

마리오네트는 엔터프라이즈 애플리케이션 개발시 필수적으로 갖추어야할 부분에 대한 고려사항이 반영되어 있으며 백본을 기본으로 확장하여 기능을 추가하였다. 마리오네트 공연을 보자



1) 마리오네트 장점

  - 모듈, 이벤트 지향 아키텍쳐의 확장

  - View에 대한 rendering 반복코드 줄여줌 

  - Region and Layout 개념을 통한 화면 통합

  - 메모리 관리 및 좀비 화면면/리젼/레이아웃등 관리

  - EventBinder 통한 이벤트 누수방지 (clean-up)

  - EventAggreagator 통한 이벤트 지향 아키텍쳐

  - 필요한 부분만 선택적으로 적용 가능



2) 반복 렌더링 코드

  - 백본 + 언더스코어 코드

    + View 에 대한 일반적인 define -> build -> render -> display == boilerplate code 라고 한다 

    + 이러한 코드가 애플리케이션 내에서 계속해서 반복된다 

//////////////////

// template file

<script type="text/html" id="my-view-template">

  <div class="row">

    <label>Name:</label>

    <span><%= name %></span>

  </div>

</script>


//////////////////

// 백본 View 코드 

var MyView = Backbone.View.extend({

  template: $('#my-view-template').html(),


  render: function(){

    // compile the Underscore.js template

    var compiledTemplate = _.template(this.template);


    // render the template with the model data

    var data = this.model.toJSON();

    var html = compiledTemplate(data);


    // populate the view with the rendered html

    this.$el.html(html);

  }

});


//////////////////

// 백본 App

var Dowon = new Person({

  name: 'Hi YoungSik'

});

var myView = new MyView({

  model: Dowon

});

myView.render();


//템플릿 결과 html 내역을 include

$('#content').html(myView.el)


  - Marionette의 ItemView 사용

    + render 메소드를 내장하고 있다. 즉 render 메소드가 제거되었다

    + Underscore를 기본 사용한다 

////////////////////////

// 마리오네트 View 코드 

var MyView = Marionette.ItemView.extend({

  template: '#my-view-template'

});



3) 뷰의 Render 및 참조 오류 제거

  - View 인스턴스와 Event 핸들러를 메모리 관리 걱정없이 쉽게 다루도록 해준다 

  - 백본 코드

    + 하나의 뷰를 만들고 내부적으로 이벤트 핸들러를 등록한다 

    + 뷰의 변수에 두개의 레퍼런스가 할당되면 첫번째 뷰 객체는 좀비가 되지만 이벤트 핸들러가 2회 반응한다

       즉, View 의 render가 두번 호출되어 alert이 두번 뜸

////////////////////

// 좀비 View 만들기 

var ZombieView = Backbone.View.extend({

  template: '#my-view-template',

  initialize: function(){

    // bind the model change to re-render this view

    this.model.on('change', this.render, this);

  },

  render: function(){

    // This alert is going to demonstrate a problem

    alert('We`re rendering the view');

  }

}); 


///////////////////

// 수행

var Person = Backbone.Model.extend({

  defaults: {

    "name": "hi dowon"

  }

});


var Dowon = new Person({

  name: 'youngsik'

});


// create the first view instance

var zombieView = new ZombieView({

  model: Person

});


// 해결을 위하여 zombieView.stopListening(); 호출이 필요한다


// create a second view instance, re-using

// the same variable name to store it

zombieView = new ZombieView({

  model: Person

});

// set 호출하여 이벤트 trigger 발생

Person.set('name', 'yun youngsik');


  - Marionette 코드 

    + listenTo를 사용하면 중복된 이벤트 핸들러는 제거함 

var ZombieView = Marionette.ItemView.extend({

  template: '#my-view-template',

  initialize: function(){

    // bind the model change to re-render this view

    this.listenTo(this.model, 'change', this.render, this);

  },


  render: function(){

    // This alert is going to demonstrate a problem

    alert('We`re rendering the view');


  }

});



4) Region을 통한 View lifeCycle 자동 관리

  - 템플릿을 해석하고 데이터 맵핑후 jQuery를 이용하여 html()이용하여 템플릿 삽입하는 부분도 boilerplate 코드이다 

    위 예에서 $('#content').html(myView.el); 코드

  - View 메모리 문제처럼 화면에 보이는 부분을 Region 으로 사용하면 개별 View들에 대한 LifeCycle 을 관리해 준다 

  - DOM element를 관리하는 Region 을 생성하고 View의 관리를 Region에게 위탁한다 

    + el 을 지정한다

    + render의 구현은 필요없다

    + view에 대한 stopListening를 호출할 필요가 없고 자동으로 DOM에서 이전 view는 제거된 후 새로운 view가 render 된다다

// create a region instance, telling it which DOM element to manage

var myRegion = new Marionette.Region({

  el: '#content'

});


// show a view in the region

var view1 = new MyView({ /* ... */ });

myRegion.show(view1);


// somewhere else in the code,

// show a different view

var view2 = new MyView({ /* ... */ });

myRegion.show(view2); 



5) Application을 통한 Region 통합 관리

  - Marionette ToDoMVC AMD방식의 GitHub 소스를 통하여 살펴보자 (참조 문서에 없는 자체 분석 결과임)

    + git clone -b marionette https://github.com/derickbailey/todomvc.git  수행하여 clone 할 때 marionette 브랜치로 구성한다

       git clone https://github.com/derickbailey/todomvc.git  후에 git checkout marionette 해도 된다 

    + cd todomvc 디렉토리로 들어가서 static 수행한다 (8080 port)

    + 브라우져에서 바로 호출한다

       http://localhost:8080/labs/dependency-examples/backbone_marionette_require/index.html

    


  - backbone_marionette_require 의 index.html 의 body 태그 중요내역

    + todoapp 섹션안에 #header, #main, #footer selector가 존재

    + Require.js 를 통한 js 폴더 밑의 main.built.js 파일을 수행(Minified 파일) -> main.js 파일 수행함 

<body>

<section id="todoapp">

  <header id="header">

  </header>

  <section id="main">

  </section>

  <footer id="footer">

  </footer>

</section>

<footer id="info">

  <p>Double-click to edit a todo</p>

  <p>Created by <a href="http://github.com/jsoverson">Jarrod Overson</a></p>

  <p><a href="./index-dev.html">View the unbuilt version</a></p>

</footer>

<script src="../../../assets/base.js"></script>

<script data-main="js/main.built" src="./js/lib/require.js"></script>

...

</body> 


  - main.js 파일에서 AMD 모듈 환경설정

    + 마리오네트를 AMD 모듈에 넣는다. 최근 버전은 AMD버전이 있으므로 shim에 설정하지 않아도 됨

    + app.js 파일을 수행한다 

    + baseUrl 을 설정하지 않았으므로 현재 디렉토리가 된다 

require.config({

  paths : {

    underscore : 'lib/underscore',

    backbone   : 'lib/backbone',

    marionette : 'lib/backbone.marionette',

    jquery     : '../../../../assets/jquery.min',

    tpl        : 'lib/tpl'   // underscore micro template 

  },

  shim : {

    'lib/backbone-localStorage' : ['backbone'],

    underscore : {

      exports : '_'

    },

    backbone : {

      exports : 'Backbone',

      deps : ['jquery','underscore']

    },

    marionette : {

      exports : 'Backbone.Marionette',

      deps : ['backbone']

    }

  },

  deps : ['jquery','underscore']

});


require(['app','backbone','routers/index','controllers/index'],function(app,Backbone,Router,Controller){

  "use strict";


  // 애플리케이션 시작

  app.start();


  // 라우터에 컨트롤러를 설정함

  new Router({

    controller : Controller

  });


  // 백본 이력 시작

  Backbone.history.start();

});

 

  - app.js 에서 Application.addRegions를 통하여 화면 Fragment를 등록한다

    + 애플리케이션이 초기화 된후 Fragment의 히스토리를 시작한다 (참조)

    + collections/TodoList.js 파일은 Backbone.Collection 이다 

    + views/Header 형식은 각각 views/Header.js 로 연결되고 다시 templates/header.tmpl 파일과 연결된다 

       underscore micro template (tpl)을 사용하고 있다

// 마리오네트안에 

define(

  ['marionette','vent','collections/TodoList','views/Header','views/TodoListCompositeView','views/Footer'],

  function(marionette, vent, TodoList, Header, TodoListCompositeView, Footer){

    "use strict";


    // SPA 애플리케이션 1개를 생성한다 

    var app = new marionette.Application(),

        todoList = new TodoList();


    app.bindTo(todoList, 'all', function() {

      if (todoList.length === 0) {

        app.main.$el.hide();

        app.footer.$el.hide();

      } else {

        app.main.$el.show();

        app.footer.$el.show();

      }

    });


    // 리젼안에 화면을 담는다 

    app.addRegions({

      header : '#header',

      main   : '#main',

      footer : '#footer'

    });


 ... 중략 ...

});


  - Header.js 를 통한 Todo 입력하기 

    + todo 입력을 수행한다 

    + id="new-todo"   

    

    + 소스 

////////////////////////

// views/Header.js

define(['marionette','templates'], function (Marionette,templates) {

  "use strict";


  // ItemView 을 상속 

  return Marionette.ItemView.extend({

    // templates/header.tmpl 파일을 참조 

    template : templates.header,


    // 템플릿 화면의 new-todo 아이디 

    // jQuery selected object를 가르키는 캐쉬된 속성을 input 이라고 생성한다 

    ui : {

      input : '#new-todo'

    },


    // 이벤트 설정 

    events : {

      'keypress #new-todo': 'onInputKeypress'

    },


    // 이벤트 핸들러 

    // 입력하고 엔터키 쳐지면 값을 컬렉션에 담는다 

    onInputKeypress : function(evt) {

      var ENTER_KEY = 13;

      var todoText = this.ui.input.val().trim();


      if ( evt.which === ENTER_KEY && todoText ) {

        // Collection에서 직접 Model객체를 생성함 

        this.collection.create({

          title : todoText

        });

        this.ui.input.val('');

      }

    }

  });

});


////////////////////////

// templates/header.tmpl 

<h1>todos</h1>

<input id="new-todo" placeholder="What needs to be done?" autofocus>


  - TodoListCompositeView.js 를 통한 TodoList

    + header 밑으로 첨부된 todo list에 대한 태그입니다 

    


    + #todo-list 는 ul 태그안에  컬렉션이 리스팅된다 

    + 한개의 todo를 입력하였을 경우 : <ul id="todo-list> 태그안에  <li class="active">..</li> 내역이 동적으로 생성된다. 이것은 TodoItemView.js 가 된다. 

    + 즉, TodoListCompositeView.js는 TodoItemView.js 목록을 관리한다. itemView 속성에 지정한다

    + 좌측의 checkbox를 클릭하면 <li class="active"> 에서 <li class="completed">  로 변경된다 

    


// TodoListCompositeView.js

define(['marionette','templates','vent','views/TodoItemView'], function (Marionette,templates,vent,ItemView) {

  "use strict";


  return Marionette.CompositeView.extend({

    template : templates.todosCompositeView,

    itemView : ItemView,  // TodoItemView.js 

    itemViewContainer : '#todo-list',


    ui : {

      toggle : '#toggle-all'

    },


    events : {

      'click #toggle-all' : 'onToggleAllClick'

    },


    initialize : function() {

      this.bindTo(this.collection, 'all', this.updateToggleCheckbox, this);

    },


    onRender : function() {

      this.updateToggleCheckbox();

    },


    // 전체 checkbox 토글들에 대한 초기화 

    updateToggleCheckbox : function() {

      function reduceCompleted(left, right) { return left && right.get('completed'); }

      var allCompleted = this.collection.reduce(reduceCompleted,true);

      this.ui.toggle.prop('checked', allCompleted);

    },


    // 전체 todo 에 대한 토글

    onToggleAllClick : function(evt) {

      var isChecked = evt.currentTarget.checked;

      this.collection.each(function(todo){

        todo.save({'completed': isChecked});

      });

    }

  });

});


/////////////////////////////////////////

// templates/todoListCompositeView.tmpl

<input id="toggle-all" type="checkbox">

<label for="toggle-all">Mark all as complete</label>

<ul id="todo-list"></ul>


  - TodoItemView.js 

    + templates/todoItemView.tmpl 사용

    + todo를 더블클릭하면 class="active" 에서 class="active editing" 이 추가된다 

    


define(['marionette','templates'], function (Marionette,templates) {

  "use strict";


  return Marionette.CompositeView.extend({

    // 상단 <li class="active editing"> 태그를 표현 

    tagName : 'li',


    // todoItemView.tmpl 지정 

    template : templates.todoItemView,


    // <input class="edit" value="hi dowon"> 태그에 대한 jQuery selected object 를 가르킴

    ui : {

      edit : '.edit'

    },


    events : {

      'click .destroy' : 'destroy',

      'dblclick label' : 'onEditClick',

      'keypress .edit' : 'onEditKeypress',

      'click .toggle'  : 'toggle'

    },


    initialize : function() {

      this.bindTo(this.model, 'change', this.render, this);

    },


    onRender : function() {

      this.$el.removeClass('active completed');

      if (this.model.get('completed')) this.$el.addClass('completed');

      else this.$el.addClass('active');

    },


    destroy : function() {

      this.model.destroy();

    },


    toggle  : function() {

      this.model.toggle().save();

    },


    // 더블클릭을 하면 editing 중이라는 class가 추가되고 edit input 태그에 포커스가 가서 edit상태가 된다

    onEditClick : function() {

      this.$el.addClass('editing');

      this.ui.edit.focus();

    },


    // input 태그로 focus가 와서 입력후 enter를 치면 값을 넣고 editing class는 제거한다 

    onEditKeypress : function(evt) {

      var ENTER_KEY = 13;

      var todoText = this.ui.edit.val().trim();


      if ( evt.which === ENTER_KEY && todoText ) {

        this.model.set('title', todoText).save();

        this.$el.removeClass('editing');

      }

    }

  });

});


/////////////////////////////

// todoItemView.tmpl

<div class="view">

  <input class="toggle" type="checkbox" <% if (completed) { %>checked<% } %>>

    <label><%= title %></label>

  <button class="destroy"></button>

</div>

<input class="edit" value="<%= title %>">


  - Footer.js 를 통한 Layout 개념이해 

    + Layout은 Region을 가지고 있다 

    + 좌측 완료안된 todo 건수 + 중앙 All, Active, Completed 상태별 todo 필터링 보기 + 우측 Clear Completed로 완료된 todo 제거 

    


    + 소스코드

// Footer.js 

define(['marionette','vent','templates','views/ActiveCount'], function (Marionette,vent,templates,ActiveCount) {

  "use strict";


  return Marionette.Layout.extend({

    // templates/footer.tmpl  

    template : templates.footer,


    // footer.tmpl의 id="todo-count" 밑의 <strong> 태그 

    regions : {

      count : '#todo-count strong'

    },


    // filters의 <a> 태그 

    ui : {

      filters : '#filters a'

    },


    // 제일 우측의 clear completed 

    events : {

      'click #clear-completed' : 'onClearClick'

    },


    initialize : function() {

      this.bindTo(vent, 'todoList:filter', this.updateFilterSelection, this);

    },


    // 리젼에 속한 count에 건수를 측정하는 ActiveCount 클래스에 컬렉션을 넘김

    onRender : function() {

      this.count.show(new ActiveCount({collection : this.collection}));

    },


    // All, Active, Completed <a> 태그의 class="selected"는 선택하는 곳으로 이동을 한다  

    // 또한 최상단의 <sectio id="todoapp" class="filter-active"> 또는 "filter-all", "filter-completed"로 변경됨 

    updateFilterSelection : function(filter) {

      this.ui.filters.removeClass('selected').filter('[href="#/' + filter + '"]').addClass('selected');

    },


    onClearClick : function() {

      vent.trigger('todoList:clear:completed');

    }

  });


});

//////////////////////

// footer.tmpl

<span id="todo-count"><strong></strong> items left</span>

<ul id="filters">

  <li>

    <a href="#/">All</a>

  </li>

  <li>

    <a href="#/active">Active</a>

  </li>

  <li>

    <a href="#/completed">Completed</a>

  </li>

</ul>

<button id="clear-completed">Clear completed</button>


/////////////////////////
// views/ActiveCount.js
define(['marionette'], function (Marionette) {
  "use strict";

  return Marionette.View.extend({
    tagName : 'span',
    // collection에 변경이 있으면 다시 그려준다 
    initialize : function() {
      this.bindTo(this.collection, 'all', this.render, this);
    },
    render : function() {
      this.$el.html(this.collection.getActive().length);
    }
  });
});


쿨럭 목감기로 오늘은 여기까지 아무래도 Model/Collection 그리고 Marionette API 에 대한 분석이 필요해 보인다. 또한 TodoMVC 를 구현함에 있어서 개발 프로세스를 고려하여 분석하는 것이 맞을 듯하다. 다시 분석을 해보도록 한다 


<참조>

  - 원문 : Backbone.Marionette 

posted by 윤영식