2017년 4월 17일 월요일

aframe 일기 - controller는 중구난방

webVR 쪽 일을 할 수도 있을 거 같아 들이대보려는 중.
aframe 이 확 끌린다.
x3dom은 솔직히 너무 구렸어. 그리고 느렸어. 별로야.
https://jsbin.com/suloji/edit?html,js,output
비슷한데 쉽다. korat을 사용해서 코드는 좀 독특한데 어쨌든.

https://aframe.io 는 데모도 좋고
ctrl+alt+I로 전용 inspector가 열리는 것도 좀 신박했어
내가 좋아하는 MagicaVoxelBlender도 지원하는 로더들이 있고
무엇보다 컨트롤러 지원이 있었어.
그래, 컨트롤러가 VR에서 반 이상은 되지.
원래 또 내가 한 컨트롤러 좋아하지.
난 gaze control이 너무 싫어.
정말 멍청한 생각이야.
도구를 쓰라고 도구를!

보니까 가격이 비싸든 싸든 헤드기어 쪽은 어느 정도 수준이 올라온 것 같은데 가격대비 공신력(?)에서 Daydream이 좀 끌리긴 했다.
싸고 가볍고 간편하고 구글이고 훌륭하지. 듀얼콘은 아니고 Wii 컨트롤러 같은데 감도도 그렇고 좋아보인다.
근데, 화웨이, 에이수스, 모또롤라, ZTE 미만 잡이래.
S7이 있음 뭐하나 제기랄.
https://github.com/domination/gvr-services-emulator/blob/master/apks/README.md
보니까 gvr-service를 손봐서 일반 폰에서 쓸 수 있게 한게 있는데 페인트 예제 앱 두 바퀴 빙빙 칠하다가 주저앉고 나서 더 해볼까 싶어도 결국 Daydream은 시작조차 해볼 수 없더라.

가지고 놀아보고 싶었는데 노는 건 나중에.
자료를 찾아보면 이젠 거의 자동인데 https://github.com/aframevr/awesome-aframe 시리즈 먼저 체크.
https://proxy-controls.donmccurdy.com/ 라는 훌륭한 것이 있군.
두개의 웹브라우저.
하나는 컨트롤러, 하나는 뷰어. 그리고 webRTC datachannel까지.
완벽해! 테스트해보자.

음? 좋긴한데 데스크톱에서 키보드나 게임패드로 폰을 제어하는 리모트라 내가 생각하던거랑은 반대.

https://github.com/ryanbetts/dayframe
오히려 이게 찾던 것.
데모는 웹소켓이로 Heroku라 엄청 느리다. 개량을 하던 새로 만들던 해야겠구만.
소스 코드를 보니

            window.addEventListener('devicemotion', handleMotionEvent, true);
            window.addEventListener('deviceorientation', handleOrientationEvent, true);
            // prevent the remote from moving around on the page
            window.addEventListener('touchstart', function (evt) {
                evt.preventDefault();
            });
            trackpadEl.addEventListener('touchstart', function (evt) {
                socket.emit('trackpad:touchstart');
                trackpadContactEl.classList.remove('hidden');
            });
            trackpadEl.addEventListener('touchmove', function (evt) {
             trackpadEl.addEventListener('touchend', function (evt) {
                socket.emit('trackpad:touchend');
                trackpadContactEl.classList.add('hidden');
            });
            trackpadHammer.get('swipe').set({ direction: Hammer.DIRECTION_ALL });
            trackpadHammer.on('doubletap', function (evt) {
                socket.emit('trackpad:click');
            });
            trackpadHammer.on('swipeleft', function (evt) {
                socket.emit('trackpad:swipeleft');
            });
            trackpadHammer.on('swiperight', function (evt) {
                console.log('swiperight');
                socket.emit('trackpad:swiperight');
            });
            trackpadHammer.on('swipeup', function (evt) {
                console.log('swipeup');
                socket.emit('trackpad:swipeup');
            });
            trackpadHammer.on('swipedown', function (evt) {
                console.log('swipedown');
                socket.emit('trackpad:swipedown');
            });
            homeBtnHammer.on('tap', function (evt) {
                socket.emit('home:tap');
                orientationCenter = lastOrientation;
            });
            appBtnHammer.on('tap', function (evt) {
                socket.emit('app:tap');
            });
이정도가 핵심 코드인가 봄.

2017년 4월 11일 화요일

Meteor에서 yarn을 쓰는 간단한 방법

역시 Meteor는 짱짱입니다.
yarn을 좀 써보니까 괜찮길레 Meteor 에선 어떻게 하나 봤더니 진짜 간단하군요.
Meteor의 CLI는 자기 자신의 영역을 가지고 있습니다.
node를 설치를 했건 안했건 상관없이 meteor npm 이 가능하기 때문에 yarn, jsdoc 같은 걸 설치해서 쓸 수 있습니다.

meteor npm install -g yarn
한 후
meteor yarn 으로 package.json을 실행하시면 됩니다.
yarn은 기본으로 --save 옵션으로 관리하기 때문에 로컬 패키지 관리시 실수를 줄일 수 있어 편리합니다.

2017년 3월 16일 목요일

coffee는 쓰고 싶고 jsx는 쓰기 싫고. 그러면 만들어야지.

이 블로그를 꾸준히 보시는 분들은 아마도 {}, [] 같은 시작과 끝만 알리고 아무것도 실행하지 않는 주제에 오롯이 한 행을 죄다 차지하는게 싫어서 coffee나 jade나 stylus를 쓰는 변태영감탱이의 글들을 읽고 계실거다.

나는 Meteor를 매우 좋아하는 사람이고 Meteor 사용자의 저변을 넓히기 위해 frontend에서 가장 뜨거운 React를 Meteor에서 쓰면 좋다라고 열심히 약을 팔고 있는데.
정작 내게 불편한 것은 jsx였다.

https://facebook.github.io/react/docs/react-without-jsx.html
이런 것이 없는 건 아닌데
이따위로 만들어놨다는 얘기는 쓰지 말란 얘기지.
그리고 어짜피 문서가 아닌데 구태여 코드안에 html을 쓸 이유가 있나?
애초에 html,css,js 로 분리한 건 의미를 가진 문서(html)를 서식화(css)해서 로직(js)을 돌아가게 함인데 이미 html도 css도 쓰지 않는 시점에서 코드안에 html을 섞어쓰는 것은 내겐 매우 불편한 일이었다.

그렇다면 DOM과 같이 위계를 갖는 요소를 어떻게 표현할 것이냐는 의문이 생기는데
다행히도 좋은 모델(http://mithril.js.org/#dom-elements)이 있다.

그러고보니 여기는 기껏 jsx를 지원한다고 했는데 내가 하려고 하는 건 그 반대;; 역시 변태영감인 것이다.
<div>
  <hr>
  <ul>
    <li>
      First
    <li>
      <p style="font-weight: bold">Second</p>
    </li>
  </ul>
  <div>
    <span>110</span>
    <hr>
  </div>
  <div>
    <label for="message">Message</label>
    <input type="text" id="message">
    <button>click</button>
  </div>
  <p>HMM Taste Good!</p>
</div>
요정도를 표현할 수 있으면 좋겠다 싶어서 뽑아보았다.
React는 CSS를 별도로 작성하는 걸 좋아하지 않아보인다. style도 인라인으로 넣었다.
만일 jade(pug) 라면 마치 CSS Selector 처럼.
div
  hr
  ul
    li First
    li
      p(style='font-weight: bold') Second
  div
    span 110
  div
    label(for="message") Message
    input#message(type="text")
    button click
  p HMM Taste Good!
이렇게 만들 수 있을 것이다.
닫는 태그들을 생략하여 라인 수가 줄어들었고 보기에도 편하다.
아마도 내가 원하는 최종 결과물은 다음과 같을 것이다.
b 'div',
  b 'hr'
  b 'ul',
    b 'li', 'First'
    b 'li',
      b 'p', style: 'font-weight': 'bold', 'Second'
  b 'div',
    b 'span', 110
  b 'div',
    b 'label', 'for': 'message', 'Message'
    b 'input', id: 'message', type: 'text'
    b 'button', 'click'
  p 'HMM Taste Good!'
이렇게 하기 위해 React.createElement를 재정의할 필요가 있다.
React.createElement(https://facebook.github.io/react/docs/react-api.html#createelement)는 세개의 인자를 받는데
https://facebook.github.io/react/docs/react-api.html#createelement
React.createElement(
  type,
  [props],
  [...children]
)
type, props, children 세가지다. (props 는 사실 object 인데 왜 []안에 넣어놨는지 모르겠다;;; )
구조상 인자가 null이 들어가는게 아름답지 않고 children이 array면 []를 써야해서 싫다.
편의상 b(mithril은 m)라고 하고 syntactic sugar를 만들어보자.
b = ->
  a = Array.from arguments
  args=[]
  if a.length is 1
    args=[a[0]]
  else
    if a[1]?.$$typeof
      args=[a[0], null, a[1..]]
    else
      if typeof a[1] isnt 'object'
        args=[a[0], null, a[1]]
      else
        args=[a[0], a[1], a[2] and a[2..]]
  React.createElement.apply React, args
결과물은 대략 이렇다. React.createElement의 반환형이 .$$typeof를 가지고 있는 것으로 구분했다.
https://jsbin.com/xaqorac/edit?html,js,output
내 기준으론 만족스럽고 아름답다.

꼭 coffee를 쓰지 않고 js로 해도
b('div',
  b('hr'),
  b('ul',
    b('li', 'First'),
    b('li',
      b('p', {style: {'font-weight': 'bold'}}, 'Second')
    )
  ),
  b('div',
    b('span', 110)
  ),
  b('div',
    b('label', 'for': 'message', 'Message'),
    b('input', id: 'message', type: 'text'),
    b('button', 'click')
  ),
  p('HMM Taste Good!')
)
https://goo.gl/VFvnDU  decaffeinate (http://decaffeinate-project.org/repl/) 로 돌려서 js 변환해보았다.
충분히 아름답다.

* 추가 수정: 반복되는 요소를 표현할 때 map을 쓰는데 반환형이 array인 경우 처리를 포함해야해서 조금 수정했다. 하위 children들은 n개의 arguments로 확장할 수도 있어서 그냥 property가 없는 경우 null을 끼워넣는 식으로 구현하는게 낫겠다 싶어서 수정.
b = ->
  a = Array.from arguments
  args = a
  args.splice 1,0,null if (a[1]?.$$typeof or
    Array.isArray a[1]) or
    (typeof a[1] isnt 'object')
  React.createElement.apply React, args
좀 더 단순하고 유연한 것 같기도.

당연한 얘기지만 React-Native에서도 쓸 수 있다.
Element를 만드는 부분을 분리한다.
React = require 'react'
module.exports = ->
  a = Array.from arguments
  args = a
  args.splice 1,0,null if (a[1]?.$$typeof or
    Array.isArray a[1]) or
    (typeof a[1] isnt 'object')
  React.createElement.apply React, args
그리고 본체에서 사용한다.
React = require 'react'
{
  AppRegistry
  StyleSheet
  Text
  View
} = require 'react-native'
b = require './src/ceact'
class App extends React.Component
  render: ->
    b View, style: styles.container,
      b Text, "Open up main.js to start working on your app."
      b View,
        b Text, key:v, "#{v}number" for v in [1..5]
styles = StyleSheet.create
  container:
    flex: 1
    backgroundColor: '#fff'
    alignItems: 'center'
    justifyContent: 'center'
AppRegistry.registerComponent 'main', => App
coffee로 쓰면 style이 stylus같이 나와서 좋다.
React Native는 각각의 Component를 구성하는 방식이라 "div" 식으로 태그 이름을 쓸 필요가 없다.
사실 React 보다는 React-Native랑 더 어울린다고 본다.
React = require 'react'
{Component} = React
{
  AppRegistry
  StyleSheet
  Text
  View
} = require 'react-native'
b = require './src/ceact'
class App extends Component
  render: ->
    b View, style: styles.container,
      b Text, "Open up main.js to start working on your app."
      b View,
        b Text, key:v, "#{v}number" for v in [1..5]
styles = StyleSheet.create
  container:
    flex: 1
    backgroundColor: '#fff'
    alignItems: 'center'
    justifyContent: 'center'
AppRegistry.registerComponent 'main', => App
잘 작동한다. "" 없이 element를 쓰니 더 좋다.

2017년 3월 5일 일요일

Meteor에서 접속(onConnection)/이탈(onClose) 관리에 대해 알아보자.

Meteor에서 로그인과는 상관없이 어떤 유저가 접속해 있고 이탈했는지를 추적하고 싶어서 정리,
서버사이드에서 아래와 같이 작성했다.
Meteor.startup ->
  console.log "server initiated"
  ConnectedUsers.remove {}
Meteor.onConnection (o)->
  console.log "session [#{o.id}] is connected"
  ConnectedUsers.insert userId: o.id
  o.onClose (p)->
    ConnectedUsers.remove userId: o.id
Meteor.methods
  "getSessionId": ->
    @connection.id
Meteor.publish "list.connectedUsers", ->
  ConnectedUsers.find()
결론은 이정도이지 않나 싶다.
mongodb를 사용하지 않고도 해보았는데 지나치게 publish쪽과 onConnection쪽이 복잡하고 일반화하기 좋지 않아 일단 보류.
https://gist.github.com/makrem025/35543cd60aa88ca49fa0 의 예에서 응용하여 onConnection 일때 publish 의 added, remove로 쏘게 하면 되긴 함. 메모리 디비는 사랑입니다 <3

접속자 관리 관련한 기존 package(aka. mizzao:user-status)들은 지나치게 User collection을 괴롭혀서 성능을 떨어뜨린다.
생선을 낚으라고 준 낚시대인데 잡아야 하는 게 상어라면 분질러 버리고 작살을 만드는 게 맞다. (뭐? 상어도 낚시로 잡는다고? 낚알못)
우리는 뾰족하게 DDP를 통해 접근한 소켓(사용자가 아니다. 이름도 몰라요 성도 몰라)에 대해 파악하고 이를 확인하는 것이 전부. 실전은 언제나 case-by-case니까.
Meteor.startup ->
  console.log "server initiated"
  ConnectedUsers.remove {}
먼저 서버 시작시 접속 유저 컬렉션을 깨끗이 비운다. 메모리 디비라면 불필요.
Meteor.onConnection (o)->
  console.log "session [#{o.id}] is connected"
  ConnectedUsers.insert userId: o.id
  o.onClose (p)->
    ConnectedUsers.remove userId: o.id
https://docs.meteor.com/api/connections.html#Meteor-onConnection 말하고 싶은 것은 여기 전부 나와있다.
서버에서 누군가 접속시 Meteor객체의 onConnection 이벤트가 발생한다.
해당 이벤트의 인자안엔 접근 소켓의 UUID를 반환하는 id와 onClose이벤트가 있다.
onConnection에 insert를 구현하고 onClose 시에 remove 하게 하자.

필요하다면 피아구분을 위해 자신의 id를 알아야할 수도 있는데
Meteor.methods
  "getSessionId": ->
    @connection.id
명시적으로 아이디가 궁금하다면 이렇게 하자. 사실 웹에선 Meteor.connection._lastSessionId를 통해 동기화하지만 외부 정적 페이지나 비Meteor환경에선 이게 오히려 간편하다.
method 안에서 this는 connection 객체를 가지고 있고 이는 해당 method를 call한 사용자의 정보와 일치한다.
만일 websocket을 직접 다룰 수 있다면 서버 응답 중
"{"msg":"connected","session":"yECjz3DgK5nyhNMWJ"}"
connected일때 session값을 가로채자.

이게 전부다.
남은 건 ConnectedUsers를 정밀하게 publish해서 권한에 따라 접속여부를 감시하게 하면 된다.
Meteor.publish "list.connectedUsers", ->
  ConnectedUsers.find() # 여기서 조건을 정밀하게!
이상이다.
기회가 되면 mongodb를 사용하지 않고 하는 방법에 대해서도 언급하려고 한다. 끗.

2017년 2월 21일 화요일

Grinder를 사용한 Meteor load testing

https://www.youtube.com/watch?v=tCPBGVI3PdA
Adrian Lanning의 영상을 전에 본적이 있는데 기회가 있어 해보기로 함.

https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein 설치는
curl https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein > /usr/local/bin/lein
chmod 755 /usr/local/bin/lein
대략 이런 식으로 하면 된다.
lein 을 한번 실행하고 https://github.com/alanning/meteor-load-test 를 받았다.
lein deps 를 실행해서 의존성 라이브러리들을 받아놓자.

https://github.com/alanning/meteor-load-test 에서 시키는 대로 해보자.

로컬 Meteor Application을 띄우고
bin/grinder agent start
로 에이전트 먼저 시작 start 뒤에 URL을 적어도 된다고 한다.
tail -f log/agent_1.log
로그 파일도 모니터 하고
bin/grinder console start
grinder 콘솔을 실행하자.
clojure로 만든 되게 불편해 보이는 툴이 뜬다.
그냥 의전용 스샷이 아니니 잘 봐둬야...

script탭을 선택하고 grinder 경로 아래에 working.properties 를 열어보자. OS X 유저들은 cmd 키가 안먹는다고 당황하지 말자. ctrl+c, ctrl+v, ctrl+a 등등 ctrl키를 이용.
grinder.script = meteor.clj
grinder.targetUrl = http://localhost:3000/
가 보인다.
실제 grinder가 구동하는 스크립트는 clj파일이다.
;;
;; Load-testing Meteor apps with The Grinder
;;
(ns meteor-load-test.http
  (:import [net.grinder.script Grinder Test]
           [net.grinder.plugin.http HTTPRequest]
           )
  (:use [meteor-load-test core util subscriptions method_calls])
  )
(let [grinder Grinder/grinder
      test1 (Test. 1 "Retrieve initial payload")
      test2 (Test. 2 "DDP subscriptions")
      test3 (Test. 3 "DDP calls")
      properties (.getProperties grinder)
      ]
  (defn instrumented-get [url]
    (log "Requesting url: " url)
    (.. (HTTPRequest.) (GET url)))
  ;; record calls to the instrumented function
  (.. test1 (record instrumented-get))
  (.. test2 (record subscribe))
  (.. test3 (record call-method))
  (defn get-client-id []
    (str (.getAgentNumber grinder) "-"
         (.getProcessName grinder) "-"
         (.getThreadNumber grinder)))
  (defn get-run-id []
    (.getRunNumber grinder))
  (defn stop-fn []
    (#(.stopThisWorkerThread grinder)))
  (defn sleep-fn [ms]
    (.sleep grinder ms))
  ;; return function that is executed once per thread by each worker process
  (worker-thread-factory
    stop-fn
    sleep-fn
    properties
    get-client-id
    get-run-id
    instrumented-get
    ))
내용을 보니 이렇다. 대략보니 DDP Subscription과 DDP call을 테스트할 수 있어 보인다.
툴바 왼쪽에서 첫번째인 Start the worker processes를 눌러서 테스트를 돌려보자.
여기!
tail로 열어보자.
[thread 0] INFO worker.jhAir.local-1 - starting, will do 1000 runs
[main] INFO worker.jhAir.local-1 - start time is 1487643542814 ms since Epoch
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-0", "type" "client"}]
[thread 0] INFO data - 0, 0, 3, 1487643542835, 9, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-1", "type" "client"}]
[thread 0] INFO data - 0, 1, 3, 1487643542855, 1, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-2", "type" "client"}]
[thread 0] INFO data - 0, 2, 3, 1487643542868, 1, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-3", "type" "client"}]
[thread 0] INFO data - 0, 3, 3, 1487643542881, 2, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-4", "type" "client"}]
[thread 0] INFO data - 0, 4, 3, 1487643542895, 1, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-5", "type" "client"}]
[thread 0] INFO data - 0, 5, 3, 1487643542907, 2, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-6", "type" "client"}]
[thread 0] INFO data - 0, 6, 3, 1487643542920, 6, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-7", "type" "client"}]
[thread 0] INFO data - 0, 7, 3, 1487643542941, 1, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-8", "type" "client"}]
[thread 0] INFO data - 0, 8, 3, 1487643542953, 2, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-9", "type" "client"}]
[thread 0] INFO data - 0, 9, 3, 1487643542966, 1, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-10", "type" "client"}]
[thread 0] INFO data - 0, 10, 3, 1487643542978, 3, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-11", "type" "client"}]
[thread 0] INFO data - 0, 11, 3, 1487643542990, 1, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-12", "type" "client"}]
[thread 0] INFO data - 0, 12, 3, 1487643542992, 0, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-13", "type" "client"}]
[thread 0] INFO data - 0, 13, 3, 1487643542993, 1, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-14", "type" "client"}]
[thread 0] INFO data - 0, 14, 3, 1487643542996, 0, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-15", "type" "client"}]
[thread 0] INFO data - 0, 15, 3, 1487643543000, 1, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-16", "type" "client"}]
[thread 0] INFO data - 0, 16, 3, 1487643543012, 1, 0, 0, 0, 0, 0, 0, 0, 0
calling: addEntry with args: [{"ownerId" "0-jhAir.local-1-0", "name" "load-17", "type" "client"}]
뭔가 마구 갈려나가고 있다.
꽤 괜찮아 보인다.
grinder.subscriptions = ["entry-count", {"latest-entries":[60]}]
grinder.calls = [{"addEntry":[{"ownerId":"CLIENTID","name":"load-RUNID","type":"client"}]}]
실제 subscription 하고 call을 어떻게 하나 했더니 JSON으로 말아놓는다. array 타입이니 여러개 지정할 수 있는데
["entry-count", {"latest-entries":[60]}]
먼저, subscription을 보자
서버쪽을 확인해보면 entry-count와 latest-entries 라는 publish를 발견할 수 있다.
[
  "entry-count",
  {
    "latest-entries": [60]
  }
]
이렇게 보면 좀 편할 것 같다.
인자가 없는 경우는 "entry-count" 식으로 publish 이름만 적고 인자가 있는 경우 publish명을 key로 적고 인자는 배열 형태로 넘겨준다.
grinder.calls = [{"addEntry":[{"ownerId":"CLIENTID","name":"load-RUNID","type":"client"}]}]
call도 크게 다르지 않은데
[
  {
    "addEntry": [
      {
        "ownerId": "CLIENTID",
        "name": "load-RUNID",
        "type": "client"
      }
    ]
  }
]
단순 call .. 인 경우는 없겠지만 있다면 마찬가지로 call 이름만 갈것 같고 인자가 있는 경우는 call 이름이 키, 인자가 배열형으로 넘어간다고 보면 되겠다.
여기서 주목할 부분이 있는데 CLIENTID와 RUNID처럼 대문자로 쓴 부분은 위 log 처럼 CLIENTID와 RUNID는 grinder가 지정해준다.
# These keywords will be replaced automatically:
#   CLIENTID - id unique to executing thread
#   RUNID - number corresponding to current test run
CLIENTID는 thread를 실행하는 ID. RUNID는 현제 테스트를 수행하고 있는 숫자와 같다.

그 다음으로 processes, threads, runs 을 살펴보자.
grinder.processes = 1
grinder.threads = 1
grinder.runs = 1000
총 실행 횟수는 processes*threads*runs 라고 보면 된다.
# Collection updates from server will always be logged
grinder.debug = true
# for some reason, the log directory required restating
grinder.logDirectory = log
한번 돌리고 잘 재연이 안되는데 log에 쌓이는 결과물들을 잘 관찰해보면 된다.
clojure는 잘 모르는데 clojure로 DDP 구현 해놓은 소스를 보니 재미있다. 

2017년 2월 20일 월요일

Meteor cordova app에서 splash 시간을 지연하고자 할때

최근 framework7(https://framework7.io/)을 비롯해 UI Framework들이 기존의 하이브리드 웹앱의 한계를 극복하고자 하는 움직임도 있고 cordova 버전도 올라갔고(WKWebview + Crosswalk 지원) 하이브리드 앱 환경이 상당히 개선이 되었다는 반가운 소식을 들어 다시 Meteor를 가지고 하이브리드 앱을 만지고 있다.

Cordova로 앱을 만들다 보면 가장 처음에 직면하는 것은 아무래도 어색한 Splash Screen인데.
https://atmospherejs.com/meteor/launch-screen 를 발견하고 미시적이지만 꽤 프로페셔널한 개선이
어서 소개하고자 한다.

모바일 앱에서 최초 splashScreen은 무의미하고 느린 UI의 일종이라고 생각하지만 subscription등이 오래걸리거나 화면 이미지 로딩등으로 준비가 아직 되지않은 상태를 보여주기 싫다면 의미가 있다고 본다.

가령, 별도로 손을 쓰지 않으면
Splash 노출 -> Splash 소멸 -> 첫화면 (빈목록) -> subscription 끝 -> 첫화면 - 목록 노출
과 같은 단계를 실행할 것이고 빈목록이 보이는 지루한 시간은 사용자에게 완성도가 떨어지는 앱이라는 느낌을 줄 수 있다.
Splash 노출 -> subscription 완료 -> Splash 소멸 -> 첫화면 - 목록 노출
처럼 작동한다면 splash화면을 봐줄만한 이유가 있고 제 역할을 하는 좋은 UI라고 생각한다.
먼저 package를 추가해보자.
meteor add launch-screen
namespace 가 없다! 그렇다. 서드파티가 아니고 공식지원이다!

일단 사용은 무척이나 간편한데
var handle = LaunchScreen.hold();
를 글로벌 scope 에 넣기만 하면 끝.
subscribe를 받아서 subs.ready() 같은 reactive data source를 감시한 뒤 handle.release(); 로 풀어볼까 했는데 그러지 않아도 되더라.

https://github.com/meteor/meteor/tree/devel/packages/launch-screen
구현은 꽤 간단하다. hold를 몇개를 넣어도 release 한방에 Splash를 제거해 주니 간편하다.
cordova-plugin-splashscreen 를 사용한 간단한 구현이니 소스도 한번 보자.

오늘도 이렇게 날로 먹는구나. 헛헛.

2017년 1월 31일 화요일

최근 확 떠오르는 micro 호스팅 서비스인 now에 meteor deploy가 가능하다고 해서 시도해봄.
먼저 now 와 meteor-now를 각각 설치하고
npm install -g now meteor-now
계정부터 만들어보자.
$ now --login
> Enter your email: 당신의@이메일.주소
> Please follow the link sent to 당신의@이메일.주소 to log in.
> Verify that the provided security code in the email matches Smooth Dormouse.
✔ Confirmed email address!
> Logged in successfully. Token saved in ~/.now.json
이게 전부다.
.now.json 에 있는 토큰을 잘 뒀다가 나중에 또 써먹자. 우리는 일반 유저가 아니라 프로그래머이므로 password 따위는 사치일 뿐. 좋은 인증 방법이다.

일단 작동 확인을 위해 아무거나 meteor 프로젝트를 만들어보자.
git clone https://github.com/meteor/clock
clock 예제 정도가 DB를 사용하지 않으니 연습상대로 적당하다.
cd clock해서 meteor 프로젝트 디렉토리로 진입한 뒤 meteor-now 하거나
MONGO_URL이 없으면 warning을 보여주니 빈거라도 넣어서 묻지마 deploy를 시작해보자.
$ meteor-now -e MONGO_URL=
✔ [METEOR-NOW] - building meteor app
✔ [METEOR-NOW] - preparing build
⠍⠉ [METEOR-NOW] - deploying build (this can take several minutes)
꽤 시간이 흐르고
✔ [METEOR-NOW] - meteor app deployed to https://clock-cseoxvdfzv.now.sh
뜨든!
주소가 뜬다.
아주 좋다.
https://clock-cseoxvdfzv.now.sh/
접근 된다.
galaxy만큼 편하고 한달에 20번씩이나 디플로이 가능한 무료 플랜도 있다.

오오. 혜자! 유료모델인 Pro도 꽤 괜찮다.

강의나 밋업 같은 걸 할때 모두 한번쯤 해볼 수 있는 무료 호스팅이 아쉬웠는데 예전 *.meteor.com의 아쉬움을 조금이나마 달래준다.