[컴][웹] Flux 예제 따라하기

플럭스 flux 예제 실행 해 보기 / flux 디자인 패턴 예제로 이해하기

Flux 예제

flux design pattern 을 한 번 사용해 보자. 기본적으로 facebook 에서 제공하는 예제가 있어서 그것을 사용해 볼까 했는데, contextify 의 설치때문에 이것저것 설치할 것이 많아서 포기하고 다른 예제를 찾았다. 그래서 ref. 1 을 찾았다. 비슷하게 contexify 를 설치하는 부분은 있지만, 쉽게 없애버릴 수 있다.
  1. nodejs 설치
  2. jest 삭제(jest-cli)
  3. package.json 에서 devDependencies 항목에서 jest 빼기. 이녀석이 contextify 를 설치하는데, 이 때 node-gyp rebuild 를 하는데, 이녀석을 동작하게 하려면 해야할 것이 많다. 그래서 그냥 pass
  4. npm install
  5. 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 는 페이스북(facebook) 에서 자신들이 만든 architecture 에 이름을 붙인 것이다. MVC architecture 라고 유명(?)한 녀석이 있는데, 이녀석을 조금 변형시킨 것이다.

어떤 식의 변화인가하면, Publish-subscribe pattern 을 끼워넣었다. 이를 위해서 Store 라는 개념을 끼워넣었다. 이 녀석이 위의 그림의 Event Channel 로 보면 된다.

ref. 6 의 MVC 와의 차이를 알려주는데, Flux 패턴의 핵심은 data 의 흐름을 한방향으로 만드는 것에 있다. 그래서 항상 모든 Action 은 Dispatcher 에게만 명령을 내리고, Dispatcher 는 다시 Store 에게만 명령을 내린다. 그러면 이 Store 에서 View 를 수정하거나, 다시 Action 을 하게 된다.



Flux 에서 자신들이 pub-sub pattern 과 다른점을 Dispatcher.js 에서 이야기 하고 있다. 차이는 아래와 같다.
  1. Callback 들이 특정 event 에 대해서 호출되지 않는다. 모든 payload 는 등록된 모든 callback 에 전달된다.

    Callbacks are not subscribed to particular events. Every payload is dispatched to every registered callback.
  2. 다른 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개를 이어 놓은 듯한 느낌이다.

대략 말로 설명하면

  1. 무슨이벤트가 발생할 때 dispatcher 에게 이 이벤트를 처리하라고 준다.
  2. 그럼 dispatcher가 해당 "store 의 콜백"을 호출한다.
  3. 이 "store 의 콜백"은 직접 만들면 된다. 이 callback 에서 "store 에 대한 event" 를 처리하도록 만들면 된다.(case 문으로 간단하게 만들면된다.)
  4. 이 때 이 "store 의 콜백"에서 다시 event 를 발생(fire) 시킨다. store는 EventEmitter를 상속해서 만들기 때문에 이녀석이 스스로 event 를 날리면 된다. 
  5. 그러면 이 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 의 동작을 살펴보는 것이 된다.


대략적으로 정리하면 아래와 같은 작업들을 한다.
  1. Dispatcher.dispatch(action) 을 호출
  2. 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());
  }

});

Reference

  1. Atlassian Blogs
  2. https://facebook.github.io/flux/docs/overview.html#content
  3. http://www.devx.com/dotnet/Article/34220
  4. Fluxxor - What is Flux?
  5. events, npm package
  6. Flux vs. MVC (Design Patterns) – Hacking and Gonzo – Medium

댓글 없음:

댓글 쓰기