서두에
Backbone 은 아마도 최초로 널리 받아들여진 프론트엔드 MVC 프레임워크인 것 같다. 확실히 나이가 들었다 (첫 번째 릴리스는 2010 년). 현재 눈이 어지러운 MV* 프레임워크들을 마주하면, Backbone 을 떠올리기는 거의 어렵고, 더군다나 그것을 선택할 이유는 더욱 없다
정말 그럴까? Backbone 을 계속 사용하는 것은 단순히 바꾸기가 귀찮아서인가? 지금도 Backbone 을 선택하는 것은 단순히 향수 때문인가?
아니다. 그것은충분히 유연하기 때문이다. Backbone 은 MV* 프레임워크 중에서 가장 강력하지는 않다 (실제로는 매우 약하지만), 그러나 가장 유연하다. 비할 데가 없다
일.Backbone v0.1.0
2010 년으로 돌아가서, Backbone 이 당초 무엇을 하려고 했는지 살펴보자
구조
View DOM 요소를 랩핑. jq 이벤트 델리게이트와 결합하여 뷰 관련 로직 관리
-------
Collection Model 컬렉션
Model 데이터 구조
데이터 추가/삭제/수정/조회 -> CRUD(create, read, update, delete) 트리거 -> Backbone.sync() -> Server
-------
Events sync(method, model, success, error)
임의의 객체에 커스텀 이벤트 지원 제공 CRUD 에서 RESTful API 로의 변환을 완료하고, jq.ajax() 로 요청 발송
그림으로 나타내면 다음과 같다:
[caption id="attachment_1178" align="alignnone" width="814"]
backbone-0.1.0[/caption]
가장 하층에서 가장 중요한 부분은 Events 다. M 에서 V 로의 통신, M 변경을 Server 에 알리는 것은 모두 Events 가 제공하는 커스텀 이벤트로 완료된다. 심지어 Backbone 객체 자체를 이벤트 버스로 사용하는 테크닉까지 있다. 다음과 같이:
// 토픽
var EVENT_DATA_READY = 'dataReady';
// 구독
Backbone.on(EVENT_DATA_READY, function(data) {
console.log(data); // Object {res: 1}
});
// 发布
Backbone.trigger(EVENT_DATA_READY, {res: 1});
Backbone 이 제공하는 모든 클래스 (Model, Collection, View, 그리고 최신 버전의 Router, History) 는 Events 에 기반한다. 따라서, 임의의 Backbone 인스턴스에서 커스텀 이벤트를 사용할 수 있다. 예를 들어 Model 인스턴스:
var EVENT_BEFORE_CHANGE = 'beforeChange';
var Model = Backbone.Model.extend({
// Overriding set
set: function(attributes, options) {
this.trigger(EVENT_BEFORE_CHANGE, attributes, options);
// Will be triggered whenever set is called
if (attributes.hasOwnProperty('prop')) {
this.trigger('change:prop');
}
return Backbone.Model.prototype.set.call(this, attributes, options);
}
});
var model = new Model();
model.on(EVENT_BEFORE_CHANGE, function() {
console.log(arguments); // ["key", "value"]
});
// test
model.set('key', 'value');
model 속성이 변경되기 전에, beforeChange 알림을 발송했다. 즉, Backbone 이 제공하는 네이티브 이벤트로 부족하다면, 임의의 beforeXXX, afterXXX, 그리고 완전히 독립적인 커스텀 이벤트를 추가할 수 있다
데이터 동기화
핵심 소스 코드는 다음과 같다:
// Map from CRUD to HTTP for our default `Backbone.sync` implementation.
var methodMap = {
'create': 'POST',
'update': 'PUT',
'delete': 'DELETE',
'read' : 'GET'
};
// Override this function to change the manner in which Backbone persists
// models to the server. You will be passed the type of request, and the
// model in question. By default, uses jQuery to make a RESTful Ajax request
// to the model's `url()`. Some possible customizations could be:
//
// * Use `setTimeout` to batch rapid-fire updates into a single request.
// * Send up the models as XML instead of JSON.
// * Persist models via WebSockets instead of Ajax.
//
Backbone.sync = function(method, model, success, error) {
$.ajax({
url : getUrl(model),
type : methodMap[method],
data : {model : JSON.stringify(model)},
dataType : 'json',
success : success,
error : error
});
};
Model 내에서 리소스에 해당하는 url 을 지정하고, Model 인스턴스가 변경될 때 CRUD 를 트리거하여, jQuery.ajax() 를 통해 Server 에 알린다
RESTful API 지원을 제공하지만, 실제 시나리오에 따라 다시 쓰는 것을 권장한다. 또는 이 데이터 동기화 메커니즘을 사용하고 싶지 않다면, Model 내에서 url 을 지정하지 않으면 된다. 강제로 부과된 규칙은 없다
라우트
실제로 Backbone 의 첫 번째 버전에는 라우트 제어가 제공되지 않았다. 단지 MVC 를 구현하기 위한 비교적 일반적인 도구 세트만 제공했을 뿐이다. 데이터 동기화 메커니즘조차도 사은품 같은 것이었다 (사용해도 되고 사용하지 않아도 됨)
MVC
M 과 V 에는 명확한 위치가 있다 (각각 Model 과 View 에 해당). 그러나 C 에는 명확한 위치가 없어서, M 과 V 에 분산시킬 수밖에 없었다. 데이터 검증, 데이터 동기화 등의 비즈니스 로직은 Model 에, 인터랙션 로직은 View 에 배치한다
이.Backbone v1.3.3
설계 이념
Philosophically, Backbone is an attempt to discover the minimal set of data-structuring (models and collections) and user interface (views and URLs) primitives that are generally useful when building web applications with JavaScript.
원시 데이터와 인터페이스 위에, 일반적인 최소 집합을 제안하고 싶다
그래서这么多年过去了, 라우트 지원만 추가되었고, 최초의 Events, Model, Collection, View 는 거의 변화가 없다
라우트 구조
Router 라우트 테이블 확립 담당
-------
History 실제 라우트 제어 구현. url 수정, 변경 감지, URL 파라미터 추출 및 라우트 콜백 실행, 라우트 이벤트 트리거
그림으로 나타내면 다음과 같다:
[caption id="attachment_1179" align="alignnone" width="761"]
backbone-1.3.3[/caption]
라우트를 라우트 테이블 확립 (Router) 과 라우트 제어 (History) 두 부분으로 나눈다. 실제 사용은 다음과 같다:
var App = Backbone.Router.extend({
routes: {
// key 는 path 패턴, val 은 이벤트명
'': 'index',
':list1ItemId': 'list2', // 'rsshelper/2'
':list1ItemId/:list2ItemId': 'detail' // 'rsshelper/2/5'
}
});
//--- run
var app = new App();
// listen route events
app.on('route:index', function() {
console.log('trigger route:index');
}).on('route:list2', function(list1ItemId) {
console.log('trigger route:list2');
}).on('route:detail', function(list1ItemId) {
console.log('trigger route:list2');
});
// pushState 활성화, 기본은 hashchange
Backbone.history.start({pushState: true,
root: location.pathname.replace('index.html', '')
});
Router 에서 라우트 테이블을 확립하고, Router 인스턴스를 통해 라우트 이벤트를 감시하며, 마지막으로 History 를 통해 라우트를 활성화한다
Router 가 전권을 장악한 것처럼 보이지만, 실제로는 Router 는 History 의 의존항목이며, 실제 제어는 History 에 의해 완료된다
라우트 구현
//--- History
// 점프 함수, 전달된 fragment 는 수동으로 encode 해야 함
// options {trigger: true} 를 전달해야 라우트 이벤트 트리거됨, 기본은 트리거 안 됨
// options {replace: true} 를 전달해야 현재 URL 을 덮어씀, 돌아갈 수 없음, 기본은 1 조 삽입, 돌아갈 수 있음
// pushState/replaceState | location.hash 대입/location.replace | location.assign(url) 새로고침
navigate: function(fragment, options) {
if (!History.started) return false;
if (!options || options === true) options = {trigger: !!options};
// Normalize the fragment.
fragment = this.getFragment(fragment || '');
// Don't include a trailing slash on the root.
var rootPath = this.root;
if (fragment === '' || fragment.charAt(0) === '?') {
rootPath = rootPath.slice(0, -1) || '/';
}
var url = rootPath + fragment;
// Strip the hash and decode for matching.
fragment = this.decodeFragment(fragment.replace(pathStripper, ''));
if (this.fragment === fragment) return;
this.fragment = fragment;
// If pushState is available, we use it to set the fragment as a real URL.
if (this._usePushState) {
//!!! backbone 은 state 파라미터를 사용하지 않음, {} 를 전달함
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
// If hash changes haven't been explicitly disabled, update the hash
// fragment to store history.
} else if (this._wantsHashChange) {
this._updateHash(this.location, fragment, options.replace);
// iframe url 업데이트
if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) {
var iWindow = this.iframe.contentWindow;
// Opening and closing the iframe tricks IE7 and earlier to push a
// history entry on hash-tag change. When replace is true, we don't
// want this.
if (!options.replace) {
iWindow.document.open();
iWindow.document.close();
}
//! iframe 의 window.location 객체를 전달함
this._updateHash(iWindow.location, fragment, options.replace);
}
// If you've told us that you explicitly don't want fallback hashchange-
// based history, then `navigate` becomes a page refresh.
} else {
return this.location.assign(url);
}
// 라우트 테이블 조회
if (options.trigger) return this.loadUrl(fragment);
}
//--- Router
// Manually bind a single named route to a callback. For example:
//
// this.route('search/:query/p:num', 'search', function(query, num) {
// ...
// });
//
route: function(route, name, callback) {
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (_.isFunction(name)) {
callback = name;
name = '';
}
if (!callback) callback = this[name];
var router = this;
Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment);
if (router.execute(callback, args, name) !== false) {
router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args);
}
});
return this;
}
실제 라우트 테이블 확립 시 (Router.route() 내의 Backbone.history.route()), Router 인스턴스 전체를 History 에 전달한다. 따라서 History 는 Router 위에 있다
History 내 라우트 구현 방식도 pushState -> onhashchange -> iframe poll 의 다운그레이드 방안이지만, Backbone 은 기본적으로 pushState 를 사용하지 않는다. 수동으로 pushState 를 활성화해야 한다. 이는其它 프레임워크의 라우트 구현 방안과 다르다:
// pushState 활성화, 기본은 hashchange
Backbone.history.start({pushState: true,
root: location.pathname.replace('index.html', '')
});
서버 사이드 지원
Backbone 라우트는서버 사이드 지원이 필요하다. 다음과 같이:
Note that using real URLs requires your web server to be able to correctly render those pages, so back-end changes are required as well. For example, if you have a route of /documents/100, your web server must be able to serve that page, if the browser visits that URL directly.
적어도 정의된 라우트가 접근 가능함을 보장해야 한다. 경로의 접근을 강제 요구하므로, 동기 미러 사이트를 만들기 싫은/만들 필요 없는 싱글 페이지 애플리케이션에게, 이 점은 매우 괴롭다. 라우트의 유연성을 크게 제한한다
또한 root 문제에도 주의해야 한다
If your application is not being served from the root url / of your domain, be sure to tell History where the root really is, as an option: Backbone.history.start({pushState: true, root: "/public/search/"})
기본적으로 history.start() 후 직접 / 로 점프하며, 페이지가 소재한 현재 경로가 아니다. 따라서 위의 샘플은 매우 이상한 일을 하고 있다:
// pushState 활성화, 기본은 hashchange
Backbone.history.start({pushState: true,
//!!! root 를 현재 경로로 변경
root: location.pathname.replace('index.html', '')
});
라우트는 싱글 페이지 애플리케이션에서 중요한 역할을 하지만, 결국 비즈니스 로직은 아니다. 라우트 때문에 이렇게 고생하는 것은 번거롭다
삼.Backbone 과 싱글 페이지 애플리케이션
싱글 페이지 애플리케이션에는 최소한 다음이 필요하다:
-
뷰 (가짜 페이지)
-
라우트 (페이지와 URL 의 관계 확립)
-
템플릿 (캐시에서 페이지를 복구하는 데 사용)
-
캐시 메커니즘 (싱글 페이지 애플리케이션의 큰 장점 중 하나)
Backbone 은 첫 세 가지를 제공하지만, 응용 장면을 만족시키기는 어렵다. 왜냐하면:
-
자식 뷰를 지원하지 않는다. 그러나 조금 복잡한 장면에서는 중첩된 뷰가 필요
-
라우트가 매우 사용하기 어렵고, 번거로우며, 유연하지도 않다
-
템플릿은 underscore 에서 왔으며, 마찬가지로 자식 템플릿을 지원하지 않는다
물론, 억지로 사용하는 것도 가능하다. 다만, 더 많은 일을 하고, 더 많은 코드를 써야 한다. 그리고 장점은모든 것이 장악之中에 있다는 것이다
거의 모든 세부 사항이 장악之中에 있다. 이는 매우 실리적인 장점이며, Backbone 과其它 프레임워크와의 가장 큰 차별점이다. 제약이 적고, 매우 유연하다
하지만 그래도 싱글 페이지 애플리케이션에서 Backbone 채택은 권장하지 않는다. 왜냐하면 너무 힘들기 때문이다. 필요 없다. Angular 가 분명히 더 적합하고 편리하다
사.유연성
프레임워크로서, Backbone 은 가장 유연하다. Angular 등의 중량급 선수들과 비교하여, Backbone 의 제약은 가장 적고, 자유도는 가장 높다:
-
네이티브 View 이벤트가 너무 많지 않다. 쉽게 스스로 추가할 수 있다
-
Model 을 수정하고 싶지 않다. 강제로 수동으로 DOM 을 조작하고 싶다. 마음대로. 그리고 수동으로 Model 을 일치시키면 된다
-
성능이 너무 나쁘다. 많은 불필요한
render()가 있다. 내장된add,remove,change,sort이벤트를 사용하지 않는다. 렌더링이 필요할 때 커스텀 이벤트로 View 에 렌더링을 알림 -
underscore 템플릿이 너무 약하다. jade 로 바꾸고 싶다. 마음대로 교체 가능.反正
view.render()는 완전히 제어 가능 -
jQuery 가 너무 크다. Zepto 로 바꾸고 싶다. 가능. 이것을 변경하는 비용은 거의 없다 (Zepto 가 지원하지 않는 jQuery API 를 사용하지 않는 경우)
-
underscore 가 너무 느리다. lodash 로 바꾸고 싶다. 물론 가능. 완전히 비용 없음
Backbone 은 DOM 조작을 jQuery/Zepto 에 구현하게 하고, Collection 조작을 underscore/lodash 에 구현하게 한다. 자신은 데이터와 인터페이스 간의 가장 핵심적인 부분만 보장할 뿐이다. 또한 내장된 렌더링 메커니즘도 없다. Backbone 은 어떤 것을 어디에 배치해야 하는지 제안할 뿐이며, 제안된 위치에 배치하지 않아도 전혀 문제없다. 특히정교한 제어가 필요한 장면에 적합하다
오.응용 장면
이전에는 Backbone 이 구식이어서 쓸모없다고 생각했다. 왜냐하면其它의 다양한 MV* 프레임워크와 비교하여, Backbone 은 너무 약해 보이기 때문이다
업무에서는 易企秀, 搜狐快站 등과 같은 드래그 앤 드롭으로 페이지를 빠르게 생성하는 것을 구현할 필요가 있었다. 처음에는 데이터 바인딩이 문제라고 생각하여 Angular 를 채택하기로 결정했다. 곧 Drag&Drop 을 Angular 와 결합하는 것이 매우 어렵다는 것을 발견했다. 왜냐하면 DnD 는 DOM 요소를 보유해야 하지만, Angular 는 끊임없이 새로운 DOM 요소를 생성하며, 직접 DOM 요소에 액세스하는 것을 원하지 않기 때문이다. 상태 불일치를 일으킬 수 있으므로
유연한 조합
Backbone 으로 변경하고, 즉시 그 거대한 장점을 발견했다. Backbone 은其它의 서드파티 것과 쉽게 결합할 수 있다. 왜냐하면제약과 제한이 없기때문이다. DOM 에 액세스하고 싶은가? 마음대로. 직접 수정하고 싶은가? 마음대로. 상태 불일치가 렌더링에 영향을 미치는가? 물론 영향을 미친다. 그러나 렌더링 부분도 모두 자신이 쓴 것이다. 무엇이 영향을 미치는지 명확하다
DOM 요소에 의존하는 서드파티 라이브러리, 예를 들어 DnD 는, 중량급 MV* 프레임워크와 결합하기 어렵다. 왜냐하면大而全인 프레임워크는 일반적으로 도덕적 제약이 있기 때문이다: 직접 DOM 을 수정하는 것을 강력히 권장하지 않으며, Model 을 수정하고, 렌더링 메커니즘이 DOM 을 다시 렌더링하게 해야 한다. Backbone 을 사용하면 이러한 문제가 없다. 그것은 느슨하고, 마음대로이며, 자유롭고 제어 가능하다
억지로 결합되었다고 해도, 서드파티 라이브러리가 요구를 만족시키지 못해 확장改写가 필요하다고发现하면, MV* 프레임워크의 층층의 제한이 모든 것을 매우 어렵게 만든다는 것을 알게 된다. 왜냐하면 프레임워크는 많은 알려지지 않은 내부 메커니즘을 제공하며, 일부를 건드리면 전체에 영향을 미칠 수 있기 때문이다. 반면 Backbone 은 거의 메커니즘을 내장하지 않아, Model, View 를 마음대로 빚을 수 있다
세부 제어
Backbone 을 사용하면 모든 것이 장악之中에 있음을 보장할 수 있다. 모든 Model 속성, 모든 change, 모든 render() 는 완전히 제어 가능하다. Backbone 은 이벤트 메커니즘만 제공하는 것처럼 보인다.其它는 모두 자신이 빚은 것이다. 로직이 명확하며, 그렇게 많은 알려지지 않은 내부 메커니즘이 없다. 두꺼운 베스트 프랙티스를 조심스럽게 지킬 필요가 없다
여분의 render() 가 존재함을发现하면, 쉽게 render() 내에서 필터링할 수 있다. 내장된 change 이벤트가 사용하기 어렵다고发现하면, 완전히 사용하지 않고, 쉽게 커스텀 이벤트로 변경할 수 있다……거의 모든 세부 사항이 제어 가능하다. Model, View 는 조금도 신비롭지도特殊하지도 않으며, 모두 커스텀 이벤트를 지원하는 일반 객체일 뿐이다
단순하고 깨끗함
단순한 데이터 바인딩 (Model, View) 만 원한다면, Backbone 은 의심할 여지 없이 최선의 선택이다. 왜냐하면任何의 제한을 강요하지 않으며,太多의 불필요한 것을 도입하지 않고,ちょうど간신히 요구를 만족시키며, 추가 비용도 없기때문이다 (학습 비용, 이전 비용 등)
P.S. 실제로 첫 번째 버전의 Backbone 이 제공했던 것으로 이미 충분했다. 나중에 추가된 Router 와 History 는 오히려계륵과 같다. 왜냐하면 라우트는 매우 유연할 필요는 없지만, 그러나충분히 편리해야 하기 때문이다. 우리가 원하는 라우트는 URL 과 대응하는 처리 함수와의 연결이다. Angular 라우트처럼. 한편, 일관되게 범용성을 추구하는 Backbone 은 이렇게 유연하지도 (강제 제약, 서버 사이드 지원 필요), 편리하지도 (기본 root 가 /) 않은 라우트를 제공했다. 사용해 보면 물론 괴롭다
아직 댓글이 없습니다