기본 콘텐츠로 건너뛰기

Cycle.js 의 Driver에 대한 이야기

왜 이름이 Driver 인가
OS에서 외부하드웨어와 연결하는 소프트웨어를 Driver라고 하는데 외부로부터 영향(effect)를 주고 영향을 받는다는 점에서 아이디어를 얻음.

DOM Driver


Sink가 없는 형태의 Driver
function WSDriver(/* no sinks */) {
  return xs.create({
    start: listener => {
      this.connection = new WebSocket('ws://localhost:4000');
      connection.onerror = (err) => {
        listener.error(err)
      }
      connection.onmessage = (msg) => {
        listener.next(msg)
      }
    },
    stop: () => {
      this.connection.close();
    },
  });
}
websocket의 예

Driver 만드는 법
function myDriver(sink$, name /* optional */)
부터 시작.

다시 Sock(가짜 실시간 리얼타임 채널 API) 구현
// Establish a connection to the peer
let sock = new Sock('unique-identifier-of-the-peer');
// Subscribe to messages received from the peer
sock.onReceive(function (msg) {
  console.log('Received message: ' + msg);
});
// Send a single message to the peer
sock.send('Hello world');
이렇게 일단 가정.
effect가 어떤 것인지 가려내보자
write effect는 sock.send(msg) 일테고
read effect는 sock.onReceive
import {adapt} from '@cycle/run/lib/adapt';
function sockDriver(outgoing$) {
  outgoing$.addListener({
    next: outgoing => {
      sock.send(outgoing);
    },
    error: () => {},
    complete: () => {},
  });
  const incoming$ = xs.create({
    start: listener => {
      sock.onReceive(function (msg) {
        listener.next(msg);
      });
    },
    stop: () => {},
  });
  return adapt(incoming$);
}
구현은 이렇게.
adapt를 가져와서

  1. outgoing 스트림을 인자로 받는다.
  2. outgoing 스트림에 대해 Listener(subscriber)를 추가한다.
  3. subscriber는 outgoing 스트림에서 받아 sock.send 를 한다.
  4. incoming 스트림은 start에 sock이 데이터를 받을 때 해당 인자(listener)의 next로 받은 메시지를 보낸다.
  5. incoming 스트림을 adapt의 인자로 반환하는 것으로 마무리
여기까지가 sockDriver 라면 Sock을 생성하는 것을 포함한 makeSockDriver를 만들어본다.
import {adapt} from '@cycle/run/lib/adapt';
function makeSockDriver(peerId) {
  let sock = new Sock(peerId);

  function sockDriver(outgoing$) {
    outgoing$.addListener({
      next: outgoing => {
        sock.send(outgoing));
      },
      error: () => {},
      complete: () => {},
    });
    const incoming$ = xs.create({
      start: listener => {
        sock.onReceive(function (msg) {
          listener.next(msg);
        });
      },
      stop: () => {},
    });
    return adapt(incoming$);
  }
  return sockDriver;
}
makeSockDriver는 peerId라는 인자를 받아 Sock을 생성한다.

실제 사용.
function main(sources) {
  const incoming$ = sources.sock;
  // Create outgoing$ (stream of string messages)
  // ...
  return {
    sock: outgoing$
  };
}
run(main, {
  sock: makeSockDriver('B23A79D5-some-unique-id-F2930')
});
익숙한 방식이다.
https://github.com/Widdershin/cycle-animation-driver/blob/master/src/driver.js
requestAnimationFrame 을 사용한 Driver 를 보면서 응용의 폭을 생각해보자.

https://www.npmjs.com/package/cycle-canvas
Canvas에도 마찬가지로 적용할 수 있다.

https://github.com/cyclejs-community/cycle-canvas/blob/master/examples/flappy-bird/index.js
에서 KeysDriver도 흥미롭다.
function makeKeysDriver () {
  const keydown$ = Observable.fromEvent(document, 'keydown');
  function isKey (key) {
    if (typeof key !== 'number') {
      key = keycode(key);
    }
    return (event) => {
      return event.keyCode === key;
    };
  }
  return function keysDriver () {
    return {
      pressed: (key) => keydown$.filter(isKey(key))
    };
  };
}
이렇게 정의하고
function App ({Canvas, Keys, Time}) {
  ...
  const space$  = Keys.pressed('space');
  ...
}
이렇게 사용한다. space 키에 대한 이벤트에 대해서만 filter한 스트림이 space$가 된다.
Driver에 대해 이해하면 Cycle.js 가 더욱 가깝게 느껴진다.

https://github.com/cyclejs-community/cycle-canvas/blob/master/examples/flappy-bird/app.js
flappy bird 예제인데 state에서부터 반복적으로 발생하는 스트림, 화면 갱신 주기. 이 모든 걸 scan하는 것 등등 참으로 알차고 값진 예제다. 이해하기도 쉽고.

이 블로그의 인기 게시물

meteor로 nw.js 개발하기.

실제로 nw.js 어플리케이션을 개발하다보면 UI구현하기 막막하고 수동으로 리프레쉬 하는 것도 귀찮아서 Meteor 연동을 하려고 했더니 생각보다 간단했다.

디렉토리 구조는 먼저 이렇게 잡았다.
`- app
  `-client
  `-public
`- dist
  `- 배포용 html,css,js
  `- package.json
`- package.json 아이디어는 이렇다. nw.js의 시작페이지를 http://localhost:3000으로 두고 배포시엔 meteor client 배포툴인 meteor-build-client를 사용하여 html,css,js 로 분리하는 계획이다.

가장 중요한 nw.js 용 package.json 파일은 아래와 같이 구성한다.
{
  "main": "http://localhost:3000",
  "node-remote": "http://localhost:3000",
  "name": "<앱이름>"
} 이게 전부. 어떻게 보면 Web과 nw.js를 동시에 개발할 수도 있는 환경이기도 한 것이다.
meteor create app 을 해서 meteor용 앱을 만들고 meteor 를 시작한다.
그리고, 위의 package.json이 있는 경로로 돌아가서 nw . 으로 nwjs를 실행한다.

한번 번쩍하더니 잘 된다.
cordova 등에서 index.html 대신 http://localhost:3000을 하는 것도 비슷한 느낌이다.

즐겁게 개발을 일단 마구 하고 실제로 배포하기 위해서는 개발환경이 아니라 html,css,js로 구성된 배포본을 만들어야한다.
npm install -g 해도 되지만 어짜피 Meteor에서만 쓸거
meteor npm install -g meteor-build-client 해버릴거다.

개발은 문제 없어 보이고 배포판을 한번 만들어보자. meteor app 이 있는 경로(meteor run으로 …

RxJS - ReactiveX 인터뷰

A: 왜 RxJS입니까
B: javascript는 참 쉽고 친숙한 언어죠.
A: 별로 그렇게 생각 안합니다만.
B: 그래서 좀 어렵고 있어보이는게 뭘까 싶어서...
A: 네?
B: 함수형이라는게 유행하기도 하고
f(x) 좋쟎습니까? 미스테리~ 미스테리~ 정수정짱짱 으아아

이런 수학선생님이라면 수포자 따윈 A: ...
B: 그리고 반응형이라는 말 뭔가
A: 뭔가?
B: 대충대충해도 막 알아서 할거 같고...
A: 그럴리가요?
B: 안그렇겠죠?
A: 네
B: 네

(잠시만 기다려주세요)

A: 그래도 뭔가 매력이 있으니 이렇게 시간을 내셔서 이것저것 Rx에 대해 글도 쓰고 이야기도 하고 그러시는거 아닌가요?
B: 매력이라.
으음.
제가 팔꿈치 터널 증후근이 좀 있어요.
오른손 세끼손가락, 약지손가락이 저립니다.
A: 무슨 상관이?
B: 그래서 각종 괄호를 쓰는게 너무 힘듭니다.
소중대괄호 만든 사람 죽었으면.
Hello world (ASCII): https://esolangs.org/wiki/Parenthesis_Hell
A: 이미 옛날에 돌아가셨겠죠.
B: 그렇겠네요.
아무튼 그래서 소중대괄호 의존이 적은 커피스크립트를 쓰는데요.
A: 빨리 본론을 말씀해주시죠.
B: 커피스크립트에서 가로로 80자 이상쓰면 Line exceeds maximum allowed length 라고 경고해요.
A: 그래서요?
B: 근데 Rx를 쓰면 코드를 가늘게 쓸 수가 있더라구요.
A: 호오?
B: 그리고 = 쓰는 것도 너무 힘듭니다.
A: 네?
B: 오른손을 쓰쟎아요.
A: ...
그러니까 정리하면
1. 괄호가 힘들다
2. 커피를 쓴다
3. 커피는 길게 쓰면 경고
4. Rx를 쓰면 코드가 가늘다
5. 대입문을 줄이고 싶다.
B: 네
하지만 5번은 생각보다 별로...
A:
B:

B: 아!
A: ?
B: 코드가 가늘어서 좋은 점이.
A: 네.
B: 핸드폰에서 코드를 보기 좋습니다.
A: ... 왜 팔꿈치 터널 증후근이 안 낫는지 알겠습니다.
B: 도와주세요.
쇠고기 사묵으면 나을 것 같습니…

Troubleshooting - Meteor package가 적용이 되지 않을 때

버전 1.5 기준 package.js에서 Package.onUse 에 새 패키지를 추가했는데 인식하지 못하는 경우가 있다.
Package.onUse((api) => {
  api.use([
    'vulcan:core',
    'vulcan:forms',
    'vulcan:accounts' /* <-- 추가함! */
  ]);
...
} 내부패키지건 원격패키지건 안되는 안된다. 이럴 때 meteor add 후 meteor remove 해도 되지만 더 간단한 방법이 있다. meteor update vulcan:accounts 이렇게 update 해주는 방법이 있다. .meteor/package 파일을 건들지 않아서 좋다. 그래도 역시 좋지 않다. Meteor 스럽지 않다. https://github.com/meteor/meteor/issues/7721 현재 1.5.2에서도 해결이 안되었군요. 해결되어 적용되면 다시 글 올리겠습니다.