2016년 9월 30일 금요일

RxJS 에서 GameScene 을 구현하고 있다.

결과물은 여기에
http://jsbin.com/wuhega/edit?js,output

Reactive Programming 의 장점+효용이 극대화되는 분야는 아무래도 게임이지 않을까 싶어서 짬을 내어 RxJS로 슈팅게임을 만들고 있다.

  1. 배경+플레이어+총알: https://jsbin.com/worewow/edit?js,output
  2. 1+적+폭파에니메이션: https://jsbin.com/modedic/edit?js,output
  3. 2+coffee+적 움직임등 : https://jsbin.com/yobuka/
대략, 이런 흐름으로 살을 조금씩 붙여가면서 올리고 있는데
어느정도 튜토리얼을 만들려는 생각으로 ECMA6를 가지고 시작을 했지만 {}, (), [] 괄호노동을 통한 손목터널증후근과 코딩하는 시간 만큼 쌍 맞추는 시간이 소모되는 자신을 발견하고 개인의 행복을 위해(...) 다시 coffeescript로 바꿔서 진행하고 있다. 살 것 같다. 아효.

아마도 곧 합쳐질 것 같지만 어느정도 내용물이 만들어졌으니 이것들을 감싸는 껍데기를 어떻게 만들지 코드를 짜면서 고민해보았다.

일단 크게 나누어 보기를
  1. Splash Scene - 한번만 노출. 게임명과 제작자에 대해 알리는 화면
  2. Title Scene - 게임시작을 위해 대기하는 화면
  3. Game Scene - 실제 게임 화면
  4. GameOver Scene - 게임을 더 이상할 수 없는 상황.
이정도로 잡고 1>2>3>4>2>... 의 반복 구조로 설계를 해보기로 했다.

아무런 레퍼런스도 없고 검색해 본 것도 없어서 매우 삽질이 예상되지만 그래도 FRP(Functional Reactive Programming)원칙에 충실하려고 노력했다.

먼저, 화면을 준비한다.
'use strict'
### setup canvas ###
c = document.createElement 'canvas'
document.body.style.margin = 0
document.body.style.height = '100%'
document.body.style.overflow = 'hidden'
document.body.appendChild c
c.width = window.innerWidth
c.height = window.innerHeight
ctx = c.getContext '2d'

캔버스를 만들고 scroll이 되지 않게 margin을 0, 높이를 100%, 그리고 스크롤바도 숨겼다.
캔버스의 크기는 윈도우의 크기만큼 꽉 차게.

두번째로 각각의 Scene을 Observable로 준비한다.
FPS = 16
SplashScene = Rx.Observable.interval FPS
TitleScene = Rx.Observable.interval FPS
GameScene = Rx.Observable.interval FPS
GameOverScene = Rx.Observable.interval FPS
FPS(Frame per Second)를 60frame으로 잡기 위해 1000msec/60frame 한 값이 16.666을 거칠게 16으로 주고 각 Scene 들이 16msec 에 한번씩 canvas 를 그리는 구조를 잡기 위해 위와 같이 설정했다.

이번엔 말 그대로 "껍데기"를 만들 것이므로 최대한 각 Scene들을 단순하게 구현하고자 했다.

시작은 Splash부터
paintSplash = (t)->
  int2hex = (i)-> "0#{i.toString(16)}".toString(16).substr(-2)
  q = int2hex (255-t*3)>0 && 255-t*3 || 0
  ctx.fillStyle = "##{q}#{q}#{q}"
  ctx.fillRect 0, 0, c.width, c.height
  ctx.fillStyle = "#eaeaea"
  ctx.font = "3rem arial"
  ctx.textAlign = "center"
  [x,y]=[c.width/2, c.height/2]
  ctx.fillText "Appsoulute", x, y - 25
  ctx.fillText "games", x, y + 25 if t>150
goSplash = ->
  console.log "enter splash"
  SplashScene
  .takeUntil Rx.Observable.timer 5000
  .subscribe paintSplash, (->), goTitle
goSplash()
각 Scene들은 여기에서 크게 다르지 않은 구조다.
  1. 해당 Scene을 subscribe 하도록 하는 함수를 만들어 호출할 것이며 (goSplash())
  2. subscribe 시 canvas에 요소들을 그릴 것이며 (paintSplash)
  3. 각 Scene을 종료하는 제약조건을 가질 것이며 (takeUntil)
  4. 3의 제약조건으로 종료되고 난 뒤 다음 Scene을 생성할 함수를 호출할 것이다. (goTitle)
이게 전부고 큰 뼈대라고 보면 되겠다.
SplashScene 을 Rx.Observable.interval FPS 로 정의했다.
FPS값, 즉 16msec (0.016초) 마다 subscribe에선 주기적으로 증가하는 값을 받을 것이며 이는 paintSplash 에 산술적으로 증가하는 정수값을 전달할 것이다.
Rx.Observable.interval(100).takeUntil(Rx.Observable.timer(5000)).subscribe(t=>console.log(t))
하고 콘솔에 한 번 입력해보자. 5000msec(5초)동안 0부터 100msec 마다 1씩 값이 증가하면서 출력되는 것을 확인할 수 있다.

paint시리즈의 시작은 canvas 영역만큼 페인트를 확 들이 붓는 것부터 시작하는데 이건 매 frame (정확히 말해 각 Stream의 interval)마다 딱 한번씩만 실행한다.
paintSplash의 인자 t를 기억하자. 애니메이션을 할때 키가 되는 값이다.
  int2hex = (i)-> "0#{i.toString(16)}".toString(16).substr(-2)
  q = int2hex (255-t*3)>0 && 255-t*3 || 0
  ctx.fillStyle = "##{q}#{q}#{q}"
  ctx.fillRect 0, 0, c.width, c.height
배경색을 지정할때 "#000000"식으로 RGB를 표현하는데
255부터 거꾸로 세어서 #ffffff , #fefefe , #fcfcfc .... #020202, #010101, #00000 에 이르도록 배경을 처리하였다.
"#{variable}"은 ECMA6의 `${variable}`과 완전히 같다.
fillStyle 에서 색을 지정하고 fillRect로 0,0 에서 canvas 높이와 너비 만큼 칠한다.

폰트의 경우
  ctx.fillStyle = "#eaeaea"
  ctx.font = "3rem arial"
  ctx.textAlign = "center"
의 세가지 속성을 사용했다.
fillStyle로 글씨에 사용할 색을 변경하고
font로 폰트의 크기(3rem: 해당페이지 기본 폰트에 x3배)와 이름(arial: 기본폰트)을 지정한 뒤
textAlign로 가로 정렬을 잡아준다.
textAlign은

이와 같은 특성을 가지고 있어서 매우 유용하다. start, end, left, center, right 속성을 바꿔가면서 비교해보자.
다시, subscribe 쪽으로 돌아와서 보면
.takeUntil Rx.Observable.timer 5000
takeUntil는 인자로 들어오는 Observable에 Stream이 발생할 때 해당 Observable을 종료하는 Operator이다.
subscribe가 시작되고 5000msec(5초가 지나면) takeUntil이 complete 를 발생시켜 Observable을 중단하고 subscribe에 지정한 complete 콜백을 실행한다.

.subscribe paintSplash, (->), goTitle

subscribe는 세개의 인자를 갖는데 각각 next, error, complete 이다. 세번째 인자인 goTitle 을 지정하여 결과적으로 5초 지나고 난 뒤 해당 함수로 이동한다.
https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/subscribe.md
내용을 참조하여 사용예를 익히자.

Splash가 5초 지나고 Title을 부르면 나머지는 다 비슷한데 시간을 기다리는 것이 아니라 클릭을 하면 다음으로 넘어가도록 구현한다.
goTitle = ->
  TitleStream = Rx.Observable.fromEvent c, "mouseup"
  TitleScene
  .takeUntil TitleStream
  .subscribe paintTitle, (->), goGame
goSplash와 거의 같은 데 제약 조건이 canvas에서 mouseup 이벤트를 fromEvent를 통해 스트림으로 받았다.
그러면 Title Scene은 마우스 클릭이 있기까지 Observable이 유효한 상태이고 마우스를 눌렀다 떼는 순간 complete을 하고 goGame으로 향한다.
goGame = ->
  console.log "enter game"
  GameStream = Rx.Observable.fromEvent document, "keydown"
  .bufferCount 3
  GameScene
  .takeUntil GameStream
  .subscribe paintGame, (->), goGameOver
goGame에선 제약 조건을 다르게 해봤는데 bufferCount 라는 Operator를 사용했다.
bufferCount는 buffer의 일종으로 해당 Observable이 3번 일어날 때 활성화가 되어 takeUntil을 실행한다.
그렇다. 이는 Life 카운트를 구현하기 위한 것으로 적과 충돌 같은 사망 스트림이 3번 발생한 경우로 goGameOver로 향하게 하는 장치라고 보면 되겠다.
구현을 단순화 하기 위해 keydown을 통해 세번 아무 키나 누르면 goGameOver 로 가게 했다.

마지막으로 GameOver를 보자.
사실상 GameOver는 Splash랑 거의 같다.

goGameOver = ->
  console.log "enter gameOver"
  GameOverScene
  .takeUntil Rx.Observable.timer 3500
  .subscribe paintGameOver, (->), goTitle

3500msec(3.5초)만큼 기다리고 그저 goTitle로 향한다.

이와 같이 하나의 루프가 완성되었다.
더 좋은 방법이 있을 법도 하지만 거꾸로 하나하나 Scene을 만드는 것도 꽤 재밌는 작업이었다.
import 나 외부 script 참조를 통해 적절하게 Scene 별로 파일을 나누면 더욱 보기 좋을 것이다.

기실 어떤 프로그램이든 좋은 껍데기 구조는 수익+평판이랑 직결된다. 좀 더 예리하게 다듬어보자.

2016년 9월 27일 화요일

Rx 에서 drag and drop 구현.

http://jsbin.com/geqige/edit?js,console,output
선 소스.
( 조금 더 자세한 구현 : http://jsbin.com/ziraga/edit?html,js,output)

먼저 Drag and Drop이라는게 어떤 절차인지 생각해보면
  1. 이동하고자 하는 대상에 MouseDown
  2. 이동하고자 하는 지점까지 MouseMove
  3. 이동하고자 하는 지점에서 MouseUp
하는 세개의 동작을 순서대로 실행하는 것이라 할 수 있겠다.
100,100 위치에 있는 A라는 대상을 200,200까지 Drag and Drop을 하는 과정을 표로 옮기면

시간(seq) 행동 좌표
1 mousedown (150,150)
2 mousemove (150,150)
3 mousemove (160,160)
4 mousemove (170,170)
5 mousemove (..., ...)
(n-1)th mousemove (250, 250)
(n)th mouseup (250, 250)

과 같은 과정을 수행할 것이다.
눈치챘을지도 모르겠지만 mousemove 구간이 바로 Stream이다!
Rx 기준으로 그러면 다시 Drag and Drop을 재정의하면
mousedown 이후 시점으로부터 mouseup이 되기전까지 mousemove 를 Observe 하는 것
인것이다.

그러면 재료를 모아보자.

mousedown
mouseup
mousemove
이 세 개를 Observable 로 만들자.

  const down$ = Rx.Observable.fromEvent(box, 'mousedown');
  const move$ = Rx.Observable.fromEvent(document, 'mousemove');
  const up$ = Rx.Observable.fromEvent(document, 'mouseup');

별거 없다. 이게 전부.
move$랑 up$은 왜 box가 아닌가 싶은데 실제로 순간 움직임이 box의 범위를 넘어갈 수도 있기 때문에 document 전체와 바인딩하는 편이 좋다.
실제로 해야하는 것은 move$를 subscribe 하는 것이다.
subscribe 해서 뭘 해야하나? 당연히 대상의 좌표를 수정해야한다.

move$.subscribe(e=>{
  box.style.left = e.clientX + 'px';
  box.style.top = e.clientY + 'px';
});

물론 이렇게만 하면 마우스의 위치가 대상의 왼쪽 끝을 가르키기 때문에 오른쪽 아래로 밖에 움직일 수 없다.
약간의 보정이 필요한데 최초 mousedown시점에 x,y와 대상의 x,y와 거리만큼 대상은 좌상으로 이동하면 된다.
이는 down$ 시점에 한번만 계산하면 되니

  const down$ = Rx.Observable.fromEvent(box, 'mousedown')
    .map(({clientX, clientY})=>({
      deltaX: clientX - box.offsetLeft,
      deltaY: clientY - box.offsetTop
    }));

으로 map을 한번 해주자. 그러면 down$ 을 move$ 에 적용할 차례인데
그전에 앞서 mousemove stream은 언제부터 해야할까?
당연히 down$ 이후 시점 부터 Observe 해야할 것이다.

그럼, 지금 필요한 것은
  1. move$ 를 down$ 이후 것만 filter 하기
  2. move$ subscriber 에서 down$ 의 값도 사용하기
인데, 이 두 가지를 한방에 해결해주는 Operator 가 있다.
오오 combineLatest 오오

위의 그림을 보면 가로축을 시간축으로 보고 x 는 1,2,3,4... , y는 A, B, C, D 와 같이 값이 들어온다고 했을 때 combineLatest(x,y,(x,y)=>(x+y)) 로 한 예다.

동작을 보면 A, B 의 상태는 mousedown이 일어나기 전의 mousemove의 스트림이라고 볼 수 있다. 이는 무시해야한다.

1을 mousedown 시점의 deltaX, deltaY라고 하면 move$에서도 쓸 수 있을 것이다.
  Rx.Observable.combineLatest(
    down$,
    move$,
    (down, move)=>({down, move})
  )
이렇게 모으면
  .subscribe(({down, move})=> {
    box.style.left = (move.clientX - down.deltaX) + 'px';
    box.style.top = (move.clientY - down.deltaY) + 'px';
  });
여기서 쓸 수 있다.
mousedown 이후 시점으로부터 mouseup이 되기전까지 mousemove 를 Observe 하는 것
남은 건 combineLatest를 포함한 이 drag작업을 언제까지 할 것인가인데 당연히 up$ 스트림에 "무언가가" 들어오는 시점이 되겠다.

이를 위한 Operator 가 있다. takeUntil 이다.

우리는 항상 그만 두어야할 때를 꼭 알아야한다.
takeUntil은 Observable을 인자로 갖는데 여기에 무언가 들어올 때 Subscribe를 종료하고 Complete을 호출한다.

마지막으로 이 모든 과정이 완료되고 난 다음 Repeat()을 사용해 재가입하는 것을 잊지말자.


2016년 9월 22일 목요일

spectrum 에게 물어보세요. multiple option을 flat 하게 하고 싶어요.

질문:
쇼핑몰에서 옵션이 여러개 있는데 이걸 조합해서 보여주고 싶어요.
사이즈: s, m, l
색상: blue, red
길이: short, long
대상: girl, boy, woman, man

이라고 했을 때
s blue short woman
이런 식으로요.

답변:
reduce 를 사용하세요.
풀이 예제 << 클릭하면 coffee/js 소스랑 결과값 모두 볼 수 있어요.

Array::flatMap = (lambda) ->
  @concat.apply [], @.map lambda
v=[
 ['s','m','l']
 ['blue','red']
 ['short', 'long']
 ['girl', 'boy', 'woman', 'man']
]
console.log "result\n",
  v.reduce (x1,x2)->
    x1.map (o)->
      x2.map (p)->
        "#{o} #{p}"
    .flatMap (o)->o
분류의 갯수는 상관없어요.
두개를 순서대로 합쳐야한다! 싶을 때는 reduce로 어떻게 해봐야지!
하고 시작하는게 좋아요.

먼저 ['s', 'm', 'l'] 하고 ['blue', 'red'] 만 놓고 보자구요.
map으로 이중 루프를 돌면서 둘을 조합하면 되겠지요?
['s', 'm', 'l'].map (o)->
  ['blue', 'red'].map (p)->
    "#{o} #{p}"
근데 이렇게 하면
[["s blue","s red"],["m blue","m red"],["l blue","l red"]]

아마 이렇게 되겠지요.
['blue', 'red'].map 이 하나의 array 가 되고
그것들의 array 가 ['s', 'm', 'l'].map (o)->이니까요.

물론, 
temp=[]
  ['s', 'm', 'l'].map (o)->
    ['blue', 'red'].map (p)->
      temp.push "#{o} #{p}"
temp
이런 식으로 push 할 수도 있겠지요.
하지만, temp 변수를 할당하고 메모리를 낭비했네요.

  ['s', 'm', 'l'].map (o)->
    ['blue', 'red'].map (p)->
      "#{o} #{p}"
  .reduce (x,y)->
    [].concat x,y

혹은 concat 을 사용해서 앞뒤 어레이를 []를 벗겨주는 것도 방법이겠네요.
근데 이럴때 쓰는 좋은 게 있어요.

flatMap 이라고요. [ ] 상자에서 까주는 것이지요.
RxJS 나 lodash 같은 걸 쓰면 바로 해결할 수도 있지만 만들어 쓰는 것도 좋겠져.

Array::flatMap = (lambda) -> 
  @concat.apply [], @.map lambda

이렇게 만들면 되겠네요.
근데 나는 Map 은 안하고 Flat ([]까기)만 하겠다 싶으면
flatMap (o)->o 라고 쓰는 것 조차 귀찮을 거여요.

Array::flatMap = (lambda) -> 
  @concat.apply [], @.map if lambda? then lambda else (o)->o

이렇게 lambda 가 없을 경우엔 (o)->o 해서 flatMap() 로도 쓸 수 있게 하면 편리하겠죠?

자 flat 한 array 를 만들었으면 이걸 reduce 로 눈덩이 처럼 불려봅니다.
다음 reduce 가 돌때엔 앞서 조합한 모든 array 와 합치겠죠?

string 이 아니라 object를 사용할 땐 [].concat o,p 한다던가 재주를 부려봅시다.

2016년 9월 20일 화요일

SVG의 path tag 에 대해 araboja

시작은 SVG에서 호(弧:arc)를 그리는 것에서 비롯하였다.
SVG는 정비율 원인 circle 과 ellipse는 있는데 arc 가 따로 있는 건 아니다.
pie graph 류를 그릴 땐 꼭 필요한데 path 말곤 뾰족한 수가 없어 보인다.

뭐 이렇게 된거 path 에 대해 전부 다 알아보도록 하자.

path는 "명령 숫자 숫자 ..."의 조합인데 각각 의도에 맞는 명령(M,L,H,V,Z,C,S,Q,T,A)와 거기에 맞는 인자들을 잘 넣어서 한방에 그릴 수 있게 한다.
참고로, 각각의 커맨드를 소문자로 쓰면 상대 좌표계로 바뀐다. 인자가 없는 Z의 경우는 어느쪽이던 똑같다.
그냥 생각해봐도 일일이 line 으로 모든 걸 다 그리는 건 비효율적이긴 하다.
어짜피 이런 건 아는 것 보다 "하는" 것이 중요하니까 한번 직접 크롬 개발 환경이라도 열고 해보는 게 좋다.


  1. 이동 명령 (M)
  2. 직선 명령 (L, H, V)
    1. L x y - 현재 점에서 x,y 까지 직선을 긋는다.
    2. H x - 현재 점에서 x 위치까지 가로로 직선을 긋는다.
    3. V y - 현재 점에서 y 위치까지 세로로 직선을 긋는다.
  3. 폐쇄 명령 (Z) - 맨 처음 점으로 되돌아가게끔 직선을 그어 닫힌 면을 만든다.
    인자 없는 것이 특징.
  4. 곡선명령 (C,S,Q,T)
    1. C x1 y1, x2 y2, x y - 현재 점에서 x,y 까지 직선을 그은 것을 Bezier 곡선을 만든다. 현재 점에서 x1,y1만큼, x,y 에서 x2,y2 만큼 핸들을 사용한다.
      c 즉 상대좌표일 때 주의할 점은 x2 y2는 x,y 기준이 아니라 현재 점 기준.
    2. S x2, y2, x y - 이전의 핸들을 연장한다. 아래의 경우를 비교해보자. 둘은 완전히 같은 형태의 곡선을 그리지만 S를 쓰는 쪽이 인자가 하나 적다.
      <path d="M100 500 C 150 400, 200 500, 200 500 C 250 600, 300 500, 300 500"/>
      <path d="M100 600 C 150 500, 200 600, 200 600 S 250 700, 300 600"/>
    3. Q x1 y1, x y - C와는 다른 타입(Quadratic curve)이다. 곡선이 x1,y1 점을 지나는 점을 유의하여 보자.
      <path d="M50 600 Q 100 500, 150 600 Q 200 700 250 600"/>
    4. T x y - S와 마찬가지로 연장하는 커맨드. Q의 x1 y1을 생략하였다.
      <path d="M50 600 Q 100 500, 150 600 Q 200 700 250 600"/>
      <path d="M50 700 Q 100 600, 150 700 T 250 700"/>
      둘은 완전히 같은 곡선
  5. Arc 호
    A rx ry x-axis-rotation large-arc-flag sweep-flag x y
    이거 이야기 하려고 밑밥을 너무 깔았다. C,S,Q,T에 대해선 너무 대충 이야기 한 감이 좀 있는데 사실 나도 잘 모른다. 그어보다 보니 감각적으로 알게된 것은 있는데 정확하게는 수학을 건들지 않고 이야기할 수 없을 것 같다.

    여튼 A를 자세히 보아야한다. 이건 Bezier Curve 가 아니다.
    1. rx, ry, x, y
      중요한 부분은 rx ry 와 x y는 ellipse 와 완전히 같다.
      rx ry는 분명하다. 가로측 반지름과 세로측 반지름.
      x y가 조금 이상하다 싶은데
      <path d="M 250 100 A 50 50 0 0 0 350 100"></path>
      실제로 그려보면 이런 형태가 된다.
      이전 점인 250,100 에서 출발하여 rx, ry만큼 크기인 호를 350,100까지 그린다.
    2. sweep-flag
      여기서 주목해야할 점은 sweep-flag 인데 0 일 경우 반시계 방향 1일 경우 시계 방향으로 그린다.
      즉,
      <path d="M 250 100 A 50 50 0 0 0 350 100"></path>
      <path d="M 250 100 A 50 50 0 0 1 350 100"></path>
      이럴 경우
      <ellipse rx="50" ry="50" cx="300" cy="100" />
      와 완전히 같다.
      원의 중심인 cx, cy 대신 실제 선이 지나가는 위치인 x y를 사용한 점을 주목하자.
    3. large-arc-flag
      또 다른 flag 인 large-arc-flag 도 보자.
      만일 호의 일부를 그리기 위해
      <path d="M 400 100 A 50 50 0 0 0 450 150"></path>
      이런 식으로 한다면 어떨까? 400,100 으로부터 50, 50 만큼 떨어진 450, 150 까지 그리는 것이므로 1/4 호를 그릴 수 있다.
      large-arc-flag 를 1로 설정하면 반대 방향으로 크게 돌아 그리는 것을 볼 수 있다.
      마치 팔이 길어서 오른손으로 왼쪽 목을 돌려치는 박진영씨 같은 형국이라고 보면 되겠다.

      ...라고 본인은 부인하시긴 하지만 (죄송)
      두 개의 호의 sweepflag 와 large-arc-flag 가 각각 다르기만 하면(0,0 1,1 혹은 0,1 1,0 등) 완전한 원이된다.
    4. x-axis-rotation
      이것도 참 미묘하다.
      타원일 경우 x축을 기준으로 n만큼 회전(단위는 degree:각도)한다.
      rotate가 있는데 이걸 쓸 필요가 있나 싶긴 하다.
    5. 사실 다 필요없고 이거 한번 보는게 낫다.
      http://codepen.io/lingtalfi/full/yaLWJG/
      x-axis-rotation 이 제대로 작동하는 것을 보기 위해 Radius X와 Radius Y를 다르게 설정하는 것을 추천.
실제로 호를 그리려면 결국 굉장히 암울한데 sin, cos을 이용하여 호의 시작점과 끝점은 미리 알고만 있으면 어떻게든 그릴 수 있긴 하겠다. 그래도 저걸 루프써서 선으로 안긋는게 어딥니다.
사실은 "A 시작각도 끝각도 rx ry cx cy" 였으면 얼마나 좋아 제기랄
http://jsbin.com/rowivid/edit?html,output 같이 하나 열고 연습해보아요.

sin, cos으로 원 그리는 건 다들 알고 계시죠?
r 이 반지름, cx,cy가 중심점일 때 degree 각도 만큼인 곳의 점의 위치는
cos(2*Math.PI*degree/360)*r + cx
sin(2*Math.PI*degree/360)*r + cy
이니까 약분하면
[cos(Math.PI*degree/180)*r + cx , sin(Math.PI*degree/180)*r + cy]
으로 x,y를 구할 수 있다. 호의 x,y는 이걸 사용하면 된다.



중간중간 빵꾸난 지식으로 글 쓰려니 힘들다. 그러니까 여러분도 같이 힘들어요~

2016년 9월 17일 토요일

openshift 에 nodebb 를 올려보자.

rhc 그러니까 옛날 버전 기준이다.
요즘껀 도대체 뭐가 뭔지 모르겠어. 잊지 않겠다 Kubernetes.. o><

rhc app create --no-scaling nodebb https://raw.githubusercontent.com/icflorescu/openshift-cartridge-nodejs/master/metadata/manifest.yml

rhc cartridge add https://raw.githubusercontent.com/icflorescu/openshift-cartridge-mongodb/master/metadata/manifest.yml -a nodebb

https://github.com/icflorescu/openshift-cartridge-nodejs/issues/55
참조.
node.js 버전을 지정하려면 semver.io를 참조. NODE_VERSION_URL 환경 변수도 바꿔줘야한다.
rhc env set -a [어플리케이션 이름] NODE_VERSION_URL=https://semver.io/node/resolve/4
(https://semver.io/node 가 안정버전, https://semver.io/node/unstable 이 최신이다)

일단, NODE_VERSION_URL 을 지정하지 않았으므로 최신(unstable) node.js 설치를 할거다.

app create를 하면 nodebb 디렉토리를 만드는데

cd nodebb
git remote add upstream -m master https://github.com/NodeBB/NodeBB.git
rm -rf `ls` .eslintrc && git commit -a -m 'Cleaned up for NodeBB'
git pull --no-edit -s recursive -X theirs upstream master

순으로 하자. upstream에서 가져올 때 master 로 했는데 원래는 v0.9.x 까지가 권장사항.

git remote add openshift -m master https://github.com/ahwayakchih/openshift-nodebb.git
git pull --no-edit -s recursive -X theirs openshift master

rhc env set NODEBB_ADMIN_EMAIL=[youremail@example.com] -a nodebb

까지해서 기본 메일을 설정하고

git push origin master
로 마무리.

현재 v1.1.2인데 별 문제없이 잘 된다.
 
update 시엔 
git pull --no-edit -s recursive -X theirs upstream master
다시 하면 될 것으로 보인다.

2016년 9월 7일 수요일

외부에서 REST로 Meteor 서버에 접근할 때 인증(Authentication) 팁 - Picker 사용

Meteor 서버와 외부 서버끼리 연동할 일이 간혹 있다.
혹은, 정적 웹페이지를 구축하고 REST로 Meteor 서버에 접근하고 싶을 수도 있을 것이다.

가령 /image 라는 경로에 POST로 그림 파일을 올리고자 하면 클라이언트 쪽에서 보내는 것은 문제가 아니지만 사용자를 어떻게 인증할 것인가에 대한 문제가 생긴다.

그렇다고 매번 POST를 보낼 때 마다 사용자 아이디와 패스워드를 올려서 검증하는 것도 번거로울 뿐만 아니라 보안상 좋지 않다.
Meteor 의 경우 최초 사용자 인증이 끝나면 loginToken이라는 값을 반환하여 이 값으로 다음번에 인증을 할 수 있는데 이것은 사용자 계정 정보

curl -XPOST 'localhost:3000/image' -H "x-user-auth:<지난번 로그인 때 저장한 loginToken>" -F "image=@/xxx/xxx/xxx/xxx.gif"

로 호출한다고 가정했을때
POST에 대한 filter 를 설정하고
API=
  GET: Picker.filter (req, res)-> req.method is "GET"
  POST: Picker.filter (req, res)-> req.method is "POST"
  DELETE: Picker.filter (req, res)-> req.method is "DELETE"
서버 라우팅을 해준다.
API.POST.route '/image', (params, req, res)->
  token = req.headers['x-user-auth']
  hashedToken = Accounts._hashLoginToken(token)
  user = Meteor.users.findOne 'services.resume.loginTokens.hashedToken': hashedToken
  check user, Object
check까지 문제 없으면 req.files 에서 upload 내용을 다뤄주면 오케이.
loginToken을 그냥 처리하는게 아니라 Accounts._hashLoginToken을 한번 해주는 것에 주의하자.
만일 일치하는 token이 없으면 user는 undefined 를 반환하므로 check에서 걸린다.

Meteor 에선 이미 검증된 인증 체계가 있기 때문에 외부 인터페이스 시 별도의 구조를 만들거나 하는 삽질을 하지 말자.

2016년 9월 1일 목요일

ReactiveX / RxJS - 상태(State)에 대한 두 가지 접근

RxJS, BaconJS, Highland, 등등 정말 Stream 전성시대. FRP(Functional Reactive Programming)가 흥하는 시대를 살고 있는 느낌이다.
기존의 Array에 람다 함수들에 시간 개념이 들어간 정도인데 그렇다면 데이터가 이렇게 흘러가고 있다면 어떻게 상태가 변할때 마다 표현할지 고민하는데
그걸 State Store 라는 걸 사용해서 상태의 변화를 Reduce (Rx에선 scan) 하여 반영한다.

가령 기존의 Control Flow 기반 프로그래밍에선
1. 상태 변수를 생성한다.
2. 이벤트가 발생한다.
3. 이벤트 콜백을 실행하여 상태 변수를 갱신한다.
4. 상태 변수를 UI에 반영한다.
였다면

RxJS같은 Data Flow 기반 프로그래밍에선
1. 이벤트를 발행한다.
2. 이벤트를 상태 변화로 변환(map)한다.
3. 2를 reduce 한 스트림을 구독한다.
4. 3의 스트림이 발생할 때마다 UI에 반영한다.
정도의 차이라고 볼 수 있겠다.

그러니까 오늘의 나는 태어날 때의 나부터 어제의 나까지를 리듀스 한 것이라고 (.....)

... 같은 건 없다.
(출처: twitter)

http://jsbin.com/bowumar 에 클릭 카운터를 구현해보았다.

let $=(e=>document.querySelector(e));
let approach1=()=>{
  let clickStream=new Rx.BehaviorSubject(0);
  $("#inc").addEventListener('click',e=>clickStream.next(1));
  $("#dec").addEventListener('click',e=>clickStream.next(-1));
  clickStream.scan((x,y)=>x+y).subscribe(o=>$("#counter").innerText=o);
};
let approach2=()=>{
  let incStream=Rx.Observable.fromEvent($("#inc"), "click").map(o=>1);
  let decStream=Rx.Observable.fromEvent($("#dec"), "click").map(o=>-1);
  incStream.merge(decStream).scan((x,y)=>x+y, 0).subscribe(o=>$("#counter").innerText=o);
};
approach1();

두 가지로 접근해보았는데 다른 관점도 있을 수 있겠지만 현재 내가 아는 범위 내에서 정리해보면
첫쨰로, 초기값을 0을 갖는 Subject로 clickStream 이라는 걸 만들고 이벤트가 발생할 때 마다 clickStream 에 변화값을 inc/dec에 따라 1, -1를 각각 넘겨서

event(inc/dec) 0 i i d i d d i i
clickstream    0+1+1-1+1-1-1+1+1 
scan(x,y)      0 1 2 1 2 1 0 1 2
            (초기값)

이렇게 누적하는 구조라고 볼 수 있다. 물론 Stream이기 때문에 매 스텝마다 다 더하는게 아니라 마지막 reduce(scan)한 값에서 부터 반영하는 것이라 문제 없다.

둘쨰로, 상태 변화에 영향을 주는 이벤트들을 Observable로 만들고 그 각각을 뭉쳐서 reduce하는 것이다.

incstream . c c . c . . c c
.map x    . 1 1 . 1 . . 1 1 -|
decstream . . . c . c c . .  |--|
.map y    . . .-1 .-1-1 . . -|  |
merge     . 1 1-1 1-1-1 1 1 ----|
scan(x,y) 0 1 2 1 2 1 0 1 2  
       (초기값)

좀 더 복잡한 것 같지만 다양한 상황이 발생할 경우 이벤트의 콜백에 로직을 넣는 것보다 이벤트 별로 스트림을 만드는 것이 나을 수도 있다.

실제로 상태를 알아내는 것은 전후 관계가 중요하기 때문에 combineLatest 같은 것으로 간편하게 할 수도 있고 필터를 정교하게 한다던가 시간 제한을 둔다던가 여러가지 상황이 있을 수 있지만 시간에 따라 흘러가는 상태값이 발생할 때 마다 scan으로 누적해서 처리한다라는 개념만 잘 잡혀있으면 어렵지 않을 것이다.