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