Flux 예제
flux design pattern 을 한 번 사용해 보자. 기본적으로 facebook 에서 제공하는 예제가 있어서 그것을 사용해 볼까 했는데, contextify 의 설치때문에 이것저것 설치할 것이 많아서 포기하고 다른 예제를 찾았다. 그래서 ref. 1 을 찾았다. 비슷하게 contexify 를 설치하는 부분은 있지만, 쉽게 없애버릴 수 있다.- nodejs 설치
- jest 삭제(jest-cli)
- package.json 에서 devDependencies 항목에서 jest 빼기. 이녀석이 contextify 를 설치하는데, 이 때 node-gyp rebuild 를 하는데, 이녀석을 동작하게 하려면 해야할 것이 많다. 그래서 그냥 pass
- npm install
- browserfy ./js/app.js
c:\src_root>.\node_modules\.bin\browserify.cmd .\js\app.js > bundle.js
browserify 는 require 부분을 merge 해준다. 원래 nodejs 는 require() 를 지원하지만, javascript 는 지원하지 않기 때문에 require 부분에서 같은 효과를 주기위해 실제 js 를 merge 해주는 일을 한다.
그래서 app.js 를 browserify 를 하면 stdout 으로 결과가 보여지는데 이것을 bundle.js 라는 이름의 file 로 저장하자. 그리고 이 녀석을 .\js\bundle.js 에 위치시키면 된다.
이제 index.html 을 열면 된다.
Flux 설명
Publish-subscribe pattern
출처: ref. 3 |
Flux 에서 자신들이 pub-sub pattern 과 다른점을 Dispatcher.js 에서 이야기 하고 있다. 차이는 아래와 같다.
- Callback 들이 특정 event 에 대해서 호출되지 않는다. 모든 payload 는 등록된 모든 callback 에 전달된다.
Callbacks are not subscribed to particular events. Every payload is dispatched to every registered callback. - 다른 callback 이 수행될 때 까지, Callback 의 전체를 또는 Callback 의 일부를 지연시킬 수 있다.
Callbacks can be deferred in whole or part until other callbacks have been executed.
Flux 를 구현하기 위해 개발자가 할 일
대략적으로 보면 Flux 구조를 만들기 위해서 개발자가 해야 할 사항은 아래 2가지 이다.- Store 라는 하나의 "component 내에서 자신의 event 에 대한 처리 routine"
- 그리고 이 routine을 facebook 의 Dispatcher 에 register 하기
이 루틴은 ref. 1 의 예제소스 에서 보면 nodejs 의 EventEmitter 로 간단히 구현하고 있어서 실제로 사용은 어렵지 않을 듯 하다.
flux 의 동작
flux 의 모습은 아래 그림처럼 pub-sub pattern 2개를 이어 놓은 듯한 느낌이다.대략 말로 설명하면
- 무슨이벤트가 발생할 때 dispatcher 에게 이 이벤트를 처리하라고 준다.
- 그럼 dispatcher가 해당 "store 의 콜백"을 호출한다.
- 이 "store 의 콜백"은 직접 만들면 된다. 이 callback 에서 "store 에 대한 event" 를 처리하도록 만들면 된다.(case 문으로 간단하게 만들면된다.)
- 이 때 이 "store 의 콜백"에서 다시 event 를 발생(fire) 시킨다. store는 EventEmitter를 상속해서 만들기 때문에 이녀석이 스스로 event 를 날리면 된다.
- 그러면 이 event 에 대해 등록된 callback 이 호출된다.(등록은 addEventHandler로 할 수 있다.)
Flux flow
간단한 흐름
- Action --> Dispatcher --> Store --> View
위의 흐름이 flux 의 일반적인 흐름이다.
Action 이 생기면 언제나 이것은 Dispatcher 를 통해서 다른 곳으로 전달되도록 구조가 되어야 한다. 즉 반대로 말하면 Action 은 Dispatcher 를 호출하는 부분이라고 보면 된다.
그리고 Dispatcher 는 언제나 Store 를 호출 한다. 그러면 이 Store 가 다시 다른 Action 을 하거나, View 를 변경하게 된다.
ref. 1 소스에서 본다면, 아래부분이 Dispatcher 에 Action 을 전달하는 부분이 된다. 즉
AppDispatcher.handleServerAction({ type: "RECEIVE_RAW_NODES", rawNodes: rawNodes });
아래 object 가 action 이 되는 것이다.
{type: "RECEIVE_RAW_NODES", rawNodes: rawNodes }
그러면 Dispatcher 는 이 action RECEIVE_RAW_NODES 에서 알맞은 store.function 을 호출하게 된다.
AppDispatcher.register(function(payload) { var action = payload.action; switch(action.type) { case "RECEIVE_RAW_NODES": _nodes = action.rawNodes; OutlineStore.emitChange(); break; ... }
그럼 다시 Store 가 event 를 날려서 view 를 변경시킨다. 또는 또다른 action 을 호출할 수 있다.
실제 구현, 소스 분석
ref. 1 의 소스를 가지고 대략적인 flux 의 흐름에 대해 알아보자.처음에 AppDispatcher.register 를 통해서 function 이 하나 등록되는데, 이녀석이 각 action type 에 대해 어떤 일을 할 지 정해주는 function 이다.
이 function 이 $Dispatcher_callbacks 에 등록되고, 이 때 만들어진 이 callback 에 대한 id 를 OutlineStore.dispatchToken 에 assign 하게 된다.
아마, 여러 Store 가 하나의 dispatcher 를 공유하고, 이 id 를 Store 마다 한개씩 준다. Dispatcher 는 waitFor(id) 라는 함수를 제공하는데, 이 함수를 이용해서 각 id 에 해당하는 callback 의 수행순서를 조절할 수 있다.
Dispatcher.prototype.register=function(callback) {"use strict"; var id = _prefix + _lastID++; this.$Dispatcher_callbacks[id] = callback; return id; }; OutlineStore.dispatchToken = AppDispatcher.register(function(payload) { var action = payload.action; switch(action.type) { case "RECEIVE_RAW_NODES": _nodes = action.rawNodes; OutlineStore.emitChange(); break; ... }
이제 OutlineWebAPIUtils.getAllNodes() 의 동작을 보자. 이부분은 실제로 data 를 받아온 것처럼 simulate 하는 부분이다. request 를 하고 난 후 success response 에 대한 callback 부분이라고 생각하면 된다.
OutlineWebAPIUtils.getAllNodes();
module.exports = {
getAllNodes: function() {
// simulate retrieving data from a database
var rawNodes = JSON.parse(localStorage.getItem('nodes'));
// simulate success callback
OutlineServerActionCreators.receiveAll(rawNodes);
}
};
이제 data 를 잘 수신한 상태이다. 이제부터 어떤 동작을 하는지가 flux 의 동작을 살펴보는 것이 된다.
대략적으로 정리하면 아래와 같은 작업들을 한다.
- Dispatcher.dispatch(action) 을 호출
- event 를 emit + 해당하는 callback 을 호출
Dispatcher.dispatch(action) 을 호출
아래와 같은 과정을 통해 payload 를 만들고, dispatch 를 호출한다.module.exports = { receiveAll: function(rawNodes) { AppDispatcher.handleServerAction({ type: "RECEIVE_RAW_NODES", rawNodes: rawNodes }); }, }; var AppDispatcher = assign(new Dispatcher(), { handleServerAction: function(action) { var payload = { source: 'SERVER_ACTION', action: action }; this.dispatch(payload); }, ... }
Dispatcher.dispatch(action) 를 호출하면 가지고 있는 모든 id 에 대한 callback 을 invoke 한다.
Dispatcher.prototype.dispatch=function(payload) { ... this.$Dispatcher_startDispatching(payload); try { for (var id in this.$Dispatcher_callbacks) { if (this.$Dispatcher_isPending[id]) { continue; } this.$Dispatcher_invokeCallback(id); } } finally { this.$Dispatcher_stopDispatching(); } };그중에 여기서는 OutlineStore 에 대한 callback 이 호출 될 때를 보자.(참고로, 이 예제에서는 등록된 Store 가 한가지 뿐이라서, 다른 callback 은 없다. ^^;;)
알다시피, OutlineStore 의 callback 은 맨 처음에 등록했다.
OutlineStore.dispatchToken = AppDispatcher.register(function(payload) { ...
data 를 잘 수신한 상태에서 payload 에 RECEIVE_RAW_NODES 를 보냈기 때문에, 이 callback 의 RECEIVE_RAW_NODES 로 가게 된다.
event 를 emit + 해당하는 callback 을 호출
여기서 OutlineStore.emitChange(); 를 호출한다. 이녀석은 그냥 CHANGE event 를 발생시키는 함수로 보면 된다. 이 내부로 가면, emit() 을 호출하는데, 여기서 각 "event 에 대해 등록된 handler" 를 호출한다.그냥 여기서 직접 실행할 함수를 호출해도 된다. 즉, OutlinStore 를 만들때 emitChnage 에 this._onChange 를 호출할 수 있도록 만들어도 된다.
그런데, 이 부분을 좀 더 일반화 해서 작성해 놓은 것이 nodejs 의 EventEmitter(ref.5 참고) 라서, 이녀석을 이용하기 위해 조금 단계를 더 거쳐서 호출했다고 보면 된다.
var OutlineStore = assign({}, EventEmitter.prototype, { emitChange: function() { this.emit(CHANGE_EVENT); }, ... } EventEmitter.prototype.emit = function(type) { ... if (!this._events) this._events = {}; // If there is no 'error' event listener then throw. if (type === 'error') { ... } handler = this._events[type]; ... if (isFunction(handler)) { switch (arguments.length) { // fast cases case 1: handler.call(this); break; case 2: ... }
이 handler 는 원하는 component 에서 각자 등록하게 된다.
var Outline = React.createClass({displayName: "Outline",
getInitialState: function() {
return getStateFromStores();
},
componentDidMount: function() {
OutlineStore.addChangeListener(this._onChange);
},
...
});
Dispatcher.prototype.register=function(callback) {"use strict"; var id = _prefix + _lastID++; this.$Dispatcher_callbacks[id] = callback; return id; }; OutlineStore.dispatchToken = AppDispatcher.register(function(payload) { var action = payload.action; switch(action.type) { case "RECEIVE_RAW_NODES": _nodes = action.rawNodes; OutlineStore.emitChange(); break; ... } OutlineWebAPIUtils.getAllNodes(); module.exports = { getAllNodes: function() { // simulate retrieving data from a database var rawNodes = JSON.parse(localStorage.getItem('nodes')); // simulate success callback OutlineServerActionCreators.receiveAll(rawNodes); } }; module.exports = { receiveAll: function(rawNodes) { AppDispatcher.handleServerAction({ type: "RECEIVE_RAW_NODES", rawNodes: rawNodes }); }, }; var AppDispatcher = assign(new Dispatcher(), { handleServerAction: function(action) { var payload = { source: 'SERVER_ACTION', action: action }; this.dispatch(payload); }, ... } Dispatcher.prototype.dispatch=function(payload) { ... this.$Dispatcher_startDispatching(payload); try { for (var id in this.$Dispatcher_callbacks) { if (this.$Dispatcher_isPending[id]) { continue; } this.$Dispatcher_invokeCallback(id); } } finally { this.$Dispatcher_stopDispatching(); } }; Dispatcher.prototype.$Dispatcher_startDispatching=function(payload) {"use strict"; for (var id in this.$Dispatcher_callbacks) { this.$Dispatcher_isPending[id] = false; this.$Dispatcher_isHandled[id] = false; } this.$Dispatcher_pendingPayload = payload; this.$Dispatcher_isDispatching = true; }; Dispatcher.prototype.$Dispatcher_invokeCallback=function(id) {"use strict"; this.$Dispatcher_isPending[id] = true; this.$Dispatcher_callbacks[id](this.$Dispatcher_pendingPayload); this.$Dispatcher_isHandled[id] = true; }; OutlineStore.dispatchToken = AppDispatcher.register(function(payload) { var action = payload.action; switch(action.type) { case "RECEIVE_RAW_NODES": _nodes = action.rawNodes; OutlineStore.emitChange(); break; ... } var OutlineStore = assign({}, EventEmitter.prototype, { emitChange: function() { this.emit(CHANGE_EVENT); }, ... } EventEmitter.prototype.emit = function(type) { ... if (!this._events) this._events = {}; // If there is no 'error' event listener then throw. if (type === 'error') { ... } handler = this._events[type]; ... if (isFunction(handler)) { switch (arguments.length) { // fast cases case 1: handler.call(this); break; case 2: ... } var Outline = React.createClass({displayName: "Outline", getInitialState: function() { return getStateFromStores(); }, componentDidMount: function() { OutlineStore.addChangeListener(this._onChange); }, ... });
var Outline = React.createClass({displayName: "Outline", componentDidMount: function() { OutlineStore.addChangeListener(this._onChange); }, ... _onChange: function() { this.setState(getStateFromStores()); } } var Node = React.createClass({displayName: "Node", props: { node: ReactPropTypes.object }, render: function() { ... <div onClick={this._onClick} /> ... } _onClick: function(event) { if (typeof this.props.node.key != 'undefined') { NodeActionCreators.selectNode(this.props.node.key); } } }); var AppDispatcher = require('../dispatcher/AppDispatcher'); module.exports = { selectNode: function(key) { AppDispatcher.handleServerAction({ type: "SELECT_NODE", key: key }); }, ... } var AppDispatcher = assign(new Dispatcher(), { /** * @param {object} action The details of the action, including the action's * type and additional data coming from the server. */ handleServerAction: function(action) { var payload = { source: 'SERVER_ACTION', action: action }; this.dispatch(payload); }, ... } Dispatcher.prototype.dispatch=function(payload) {"use strict"; ... this.$Dispatcher_startDispatching(payload); try { for (var id in this.$Dispatcher_callbacks) { if (this.$Dispatcher_isPending[id]) { continue; } this.$Dispatcher_invokeCallback(id); } } ... }; Dispatcher.prototype.$Dispatcher_invokeCallback=function(id) {"use strict"; this.$Dispatcher_isPending[id] = true; this.$Dispatcher_callbacks[id](this.$Dispatcher_pendingPayload); this.$Dispatcher_isHandled[id] = true; }; OutlineStore.dispatchToken = AppDispatcher.register(function(payload) { var action = payload.action; switch(action.type) { ... case "SELECT_NODE": _selected = action.key; OutlineStore.emitChange(); break; ... } var OutlineStore = assign({}, EventEmitter.prototype, { emitChange: function() { this.emit(CHANGE_EVENT); }, ... } EventEmitter.prototype.emit = function(type) { var er, handler, len, args, i, listeners; ... if (isFunction(handler)) { switch (arguments.length) { // fast cases case 1: handler.call(this); break; } var Outline = React.createClass({displayName: "Outline", ... _onChange: function() { this.setState(getStateFromStores()); } });
댓글 없음:
댓글 쓰기