2016년 10월 22일 토요일

RxJS Basic - 기초

http://reactivex.io/rxjs/manual/tutorial.html 글을 대충 번역해서 쓰는 글입니다.
jsbin/codepen 같은 곳에서 RxJS 라이브러리를 추가한 후 연습해봅시다.
현재 가장 최근 버전은 https://cdnjs.com/libraries/rxjs/5.0.0-rc.1 에서 확인하세요.

  1. Observable 로 변환하기
    1. 하나 혹은 n 개의 값으로부터

      Rx.Observable.of('foo', 'bar');
      =>foo
      =>bar
    2. 배열로부터

      Rx.Observable.from([1,2,3]);
      =>1
      =>2
      =>3
    3. 이벤트로부터

      Rx.Observable.fromEvent(document.querySelector('button'), 'click');
      =>button을 누를 때 마다 event로 변환
    4. Promise로부터

      Rx.Observable.fromPromise(fetch('/users'));
      =>/user 주소의 내용을 fetch한 것을 변환
    5. 마지막 인자가 콜백인 경우 콜백으로부터

      /* node.js */
      var exists = Rx.Observable.bindCallback(fs.exists);
      exists('file.txt').subscribe(exists => console.log('Does file exist?', exists));

      var rename = Rx.Observable.bindNodeCallback(fs.rename);
      rename('file.txt', 'else.txt').subscribe(() => console.log('Renamed!'));
  2. Observable 만들기
    1. 외부로부터 새로운 이벤트를 생성한다.

      var myObservable = Rx.Subject.create();
      myObservable.subscribe(value => console.log(value));
      myObservable.next('foo');
    2. 내부에서 새로운 이벤트를 생성한다.

      var myObservable = Rx.Observable.create(observer => {
        observer.next('foo');
        setTimeout(() => observer.next('bar'), 1000);
      });
      myObservable.subscribe(value => console.log(value));
    • 어떤 것을 선택할지는 시나리오에 따라 선택할 수 있습니다.
      일반적인 Observable은 당신이 시간이 흘러가면서 값을 생성하는 기능을 구현할때 좋습니다.
      일례로 websocket 연결 같은 것을 들 수 있다.
      Subject를 사용하면 어디서나 새로운 이벤트를 작동할 수 있으며 기존의 Observable과 연결할 수 있습니다.
  3. 흐름(Flow)을 제어하기

    // "hello world"라고 쳐봅니다.
    var input = Rx.Observable.fromEvent(document.querySelector('input'), 'keypress');

    // 2글자 이하로는 필터링합니다
    input.filter(event => event.target.value.length > 2)
      .subscribe(value => console.log(value));
    // "hel"

    // 이벤트를 지연시킵니다.
    input.delay(200)
      .subscribe(value => console.log(value));
    // "h" -200ms-> "e" -200ms-> "l" ...

    // 매번 200ms 동안 한번만 받아들입니다.
    input.throttleTime(200)
      .subscribe(value => console.log(value));
    // "h" -200ms-> "w"

    // 마지막 200ms 후에 발생한 것만 통과시킵니다.
    input.debounceTime(200)
      .subscribe(value => console.log(value));
    // "o" -200ms-> "d"

    // 3개의 이벤트만 받습니다.
    input.take(3)
      .subscribe(value => console.log(value));
    // "hel"

    // 다른 옵저버블이 이벤트를 발생시킬 때 통과시킵니다.
    var stopStream = Rx.Observable.fromEvent(document.querySelector('button'), 'click');
    input.takeUntil(stopStream)
      .subscribe(value => console.log(value));
    // "hello" (click)
  4. 값(Value)을 만들기

    // "hello world" 라고 쳐봅니다.
    var input = Rx.Observable.fromEvent(document.querySelector('input'), 'keypress');
    // 새로운 값을 내보냅니다.
    input.map(event => event.target.value)
      .subscribe(value => console.log(value));
    // "h"

    // 값 뽑아내기
    input.pluck('target', 'value')
      .subscribe(value => console.log(value));
    // "h"

    // 값을 쌍으로 뽑아내기
    input.pluck('target', 'value').pairwise()
      .subscribe(value => console.log(value));
    // ["h", "e"]

    // 중복 값 제거하기
    input.pluck('target', 'value').distinct()
      .subscribe(value => console.log(value));
    // "helo wrd"

    // 이전 값과 다를 때만 내보내기
    input.pluck('target', 'value').distinctUntilChanged()
      .subscribe(value => console.log(value));
    // "helo world"
  5. 응용프로그램(Application) 만들기
    RxJS는 코드의 오류를 줄이기에 탁월한 도구입니다.
    이에 순수함수와 상태 없는(stateless) 함수를 사용합니다.
    하지만 응용프로그램들은 상태를 가지고 있습니다(stateful).
    그렇다면 우리는 stateless한 RxJS 와 stateful한 응용프로그램을 어떻게 연결할 수 있을까요?
    0 이라는 값으로부터 단순한 상태 저장(State Store)을 만들어봅시다.
    매번 클릭할 때 마다 우리는 State Store의 값이 증가하길 원합니다.

    var button = document.querySelector('button');
    Rx.Observable.fromEvent(button, 'click')

      // 카운트의 스트림을 합산(scan-reduce)합니다.
      .scan(count => count + 1, 0)
      // 매번 그것이 변할 때 마다 요소(element)의 갯수를 설정합니다.
      .subscribe(count => document.querySelector('#count').innerHTML = count);

    이렇게 RxJS에서 상태를 만들어 보았습니다. 하지만 DOM을 변경하는 것은 마지막 줄에서 일어나는 부작용(Side-effect)입니다.
  6. 상태 저장소(State store)
    응용 프로그램에서는 상태를 유지하기를 위해 State Store를 사용합니다. 이들은 프레임워크마다 store나 reducer, model 등 각각 다른 이름으로 불립니다. 그러나 그것들은 단지 객체(Object)일 뿐입니다.

    var increaseButton = document.querySelector('#increase');
    var increase = Rx.Observable.fromEvent(increaseButton, 'click')

      // 우리는 함수에 대입하여 상태를 변하게 합니다.
      .map(() => state => Object.assign({}, state, {count: state.count + 1}));
    우리가 여기서 하는 것은 클릭 이벤트를 상태 변경 함수에 매핑 하는 것입니다.
    그래서 값을 매핑하는 것 대신 함수를 매핑하였습니다.
    함수는 State Store 의 상태를 변화시킬 것입니다.
    그러면, 실제로 어떻게 변하는지 살펴봅니다.

    var increaseButton = document.querySelector('#increase');
    var increase = Rx.Observable.fromEvent(increaseButton, 'click')
      .map(() => state => Object.assign({}, state, {count: state.count + 1}));

    // 우리는 초기 상태를 갖는 객체를 만들었습니다. 새로운 상태 변화가 있을 때 마다
    이를 불러내고 상태를 전달합니다.
    // 새로운 상태를 반환하고 다음 클릭에 대해 변경할 준비를 합니다.
    var state = increase.scan((state, changeFn) => changeFn(state), {count: 0});
    우리는 이제 같은 State store를 변경할 더 많은 Observables 를 추가할 수 있습니다.

    var increaseButton = document.querySelector('#increase');
    var increase = Rx.Observable.fromEvent(increaseButton, 'click')

      // 카운트를 증가시키는 함수를 매핑합니다.
      .map(() => state => Object.assign({}, state, {count: state.count + 1}));

    var decreaseButton = document.querySelector('#decrease');
    var decrease = Rx.Observable.fromEvent(decreaseButton, 'click')

      // 또한 카운트를 감소시키는 함수도 매핑합니다.
      .map(() => state => Object.assign({}, state, {count: state.count - 1}));

    var inputElement = document.querySelector('#input');
    var input = Rx.Observable.fromEvent(inputElement, 'keypress')
      // keypress 이벤트로 inpupValue 상태를 만듭니다.
      .map(event => state => Object.assign({}, state, {inputValue: event.target.value}));

    // 이 세가지 상태를 생성하는 Observables들을 병합(Merge)합니다.
    var state = Rx.Observable.merge(
      increase,
      decrease,
      input
    ).scan((state, changeFn) => changeFn(state), {
      count: 0,
      inputValue: ''
    });

    // 상태 변화에 대해 subscribe 하고 DOM을 갱신합니다.
    state.subscribe((state) => {
      document.querySelector('#count').innerHTML = state.count;
      document.querySelector('#hello').innerHTML = 'Hello ' + state.inputValue;
    });


    // 실제로 상태가 바뀌었을 때를 확인하여 렌더링을 최적화합니다.
    var prevState = {};
    state.subscribe((state) => {
      if (state.count !== prevState.count) {
        document.querySelector('#count').innerHTML = state.count;
      }
      if (state.inputValue !== prevState.inputValue) {
        document.querySelector('#hello').innerHTML = 'Hello ' + state.inputValue;
      }
      prevState = state;
    });
원문에는 immutable JS 나 React 와 같이 실제로 응용프로그램 차원에 적용하는 법도 있지만 이 글에선 범위 밖이라 다루지 않습니다.
각자 자신이 사용하는 환경에 맞게 적용해봅시다.