Skip to content

Latest commit

 

History

History
608 lines (506 loc) · 43.9 KB

File metadata and controls

608 lines (506 loc) · 43.9 KB

이글은 @andrestaltz의 "The introduction to Reactive Programming you've been missing" ( https://gist.github.com/staltz/868e7e9bc2a7b8c1f754 )를 번역한 글입니다. (번역 허락을 받은 것은 아니지만, 도움이 필요하신 분들을 위해서 올려봅니다. 문제가 될 경우 바로 삭제하겠습니다. ㅡㅜ)

여러분이 놓쳐왔던 Reactive Programming에 대한 소개 (@andrestaltz 작성)

동영상 튜터리얼

만약 여러분이 라이브 코딩을 포함한 동영상 튜터리얼을 보는 것을 더 선호한다면, 이 동영상 시리즈를 확인해보세요. 이 글과 동일한 내용으로 녹화했습니다. ( https://egghead.io/series/introduction-to-reactive-programming )

여러분은 Reactive Programming 이라고 불리는 이 새로운 것을 배우는데 관심이 있습니다. 특히 Reactive Programming 변형들은 Rx, Bacon.js, RAC 그리고 다른 것들을 구성하고 있죠.

배우는 것은 어렵습니다. 심지어 좋은 자료가 부족할수록 더 배우기 어렵지요. 제가 배우기 시작했을 때, 튜터리얼을 보려고 노력했습니다. 저는 겨우 몇개의 실용적인 가이드만 찾을 수 있었지요. 하지만 그 튜터리얼들은 겉표면만 긁는 수준이었고, 실제 전체 architecture를 구성할 때 겪는 문제들을 해결해주는 것들은 없었습니다. 라이브러리 문서들은 어떤 함수를 이해하려고 할때 종종 도움이 안되었습니다. 그러니까, 솔직히, 이런 걸 보세요.

Rx.Observable.prototype.flatMapLatest(selector, [thisArg])
Projects each element of an observable sequence into a new sequence of observable sequences by incorporating the element's index and then transforms an observable sequence of observable sequences into an observable sequence producing values only from the most recent observable sequence.

어이쿠 이런.

저는 책을 두권 읽었습니다. 하나는 큰 그림을 그리는 것이었고 반면에 다른 하나는 Reactive 라이브러리 사용법으로 곧장 뛰어드는 그런 것이었습니다.저는 결국 고생해서 Reactive Programming 학습을 마쳤습니다. 만들면서 배웠습니다. Futurice의 직장에서 실제 프로젝트에 Reactive Programming을 사용했고, 곤경에 처할 땐 동료의 도움( http://blog.futurice.com/top-7-tips-for-rxjava-on-android )도 있었습니다.

배움의 길에 가장 어려운 부분은 Reactive 방식으로 생각하는 것이었습니다. 전형적인 프로그래밍의 명령형이고 상태를 유지하는 옛날 방식을 떠나 보내는 것과 저의 뇌가 다른 패러다임으로 일하도록 하게 하는 것이 컸습니다. 저는 이런 관점에서의 어떠한 가이드도 찾을 수 없었습니다. 그리고 세상에 Reactive 방식으로 생각하는 방법에 대한 실용적인 튜터리얼이 하나쯤은 있어야 한다고 생각했습니다. 그래서 시작할 수 있었습니다. Reactive 방식으로 생각하는 것 이후에 라이브러리 문서가 여러분의 길을 밝혀줄 수 있습니다. 이 글이 여러분에게 도움이 되기를 희망합니다.

"Reactive Programming은 무엇인가요?"

인터넷에 나쁜 설명과 정의가 매우 많습니다. Wikipedia( https://en.wikipedia.org/wiki/Reactive_programming )는 보통 너무 일반적이고 이론적입니다. Stackoverflow( http://stackoverflow.com/questions/1028250/what-is-functional-reactive-programming )의 규범적인 답변은 명백히 초심자들에게는 적절하지 않습니다. Reactive Manifesto( http://www.reactivemanifesto.org/ )는 여러분의 프로젝트 매니저 혹은 여러분의 회사에 경영자 에게 보여주기 위한 것들 처럼 들립니다. 마이크로소프트의 Rx 용어( https://rx.codeplex.com/ ) "Rx = Observables + LINQ + Schedulers"는 너무 무겁고 너무 마이크로소프트스러워서 우리 대부분을 혼란 스럽게 만듭니다. "reative" 와 "변경사항의 전파"("propagation of change")같은 용어는 전형적인 MV* 와 인기있는 언어들이 이미 전파했던 것들과는 특별히 다른 것을 전달하지 못합니다. 물론 저의 framework view들은 model들에 반응(react)합니다. 물론 변경사항은 전파됩니다. 그렇지 않다면, 아무것도 그려지지 않을 겁니다.

그래서 헛소리 집어치웁시다.

Reactive programming은 비동기 data 흐름들(asynchronous data streams)로 프로그래밍하는 겁니다.

한편으로는 Reactive programming은 새로운 것이 아닙니다. 이벤트 버스들 혹은 여러분의 전형적인 클릭 이벤트들은 사실상 비동기 이벤트 흐름(stream)입니다. 여러분이 관찰할 수 있고, 몇가지 Side effect(함수의 입력과 출력사이 연관 관계 없이 동작하는 것)들을 할 비동기 이벤트 stream입니다.

여러분은 단순히 click하고 hover하는 이벤트뿐만 아니라 어떤 것이라도 data stream을 만들 수 있습니다. stream들은 연산 비용이 저렴하고 어디에나 존재합니다. 어떤 것이든 stream이 될 수 있습니다.:변수, 사용자 입력, property들, cache들, 데이터 구조들 기타등등. 예를 들어, click 이벤트들과 같은 방식으로 여러분의 Twitter 피드도 data stream일 것이라고 상상해보세요. 여러분은 그 stream을 듣고, 그에 따라 반응할 수 있습니다.

이에 더불어, 그런 stream들을 결합하고(combine), 생성하고, 거르는(filter) 함수들의 놀라운 도구상자가 여러분에게 주어졌습니다.

여기서 "functional"의 마술이 효과를 발휘합니다. stream은 다른 stream의 입력으로 사용될 수 있습니다. 심지어 여러개의 stream들이 다른 stream의 입력으로 사용될 수 있습니다. 여러분은 두개의 stream들을 합칠수(merge)도 있습니다. 여러분은 관심있는 이벤트만을 가지는 stream을 얻기 위해서 stream을 거를수(filter) 있습니다. 여러분은 한 stream으로 부터의 data 값을 다른 새로운 stream으로 사상할(map - 어떤 값(이벤트)를 다른 값으로 변환)수도 있습니다.

stream은 시간 순서대로 진행중인 이벤트들의 순서배열(sequence)입니다. stream은 세가지 다른 것들을 발생시킬(emit) 수 있습니다.: (어떤 타입의) 값, 오류(error), "완료됨" 신호("completed" signal). "completed" 신호가 발생했다고 가정해보세요. 예를 들어, 버튼을 포함한 현재 윈도우 혹은 view가 닫혔을때(closed)를 가정해 보세요.

우리는 이 발생한(emitted) 이벤트들을 단지 비동기로(asynchronously) 획득할 수 있습니다. 값이 발생할(emitted) 때 실행되는 함수, 오류(error)가 발생할(emitted) 때 실행되는 다른 함수, "completed" 신호가 발생할(emitted) 때 실행되는 다른 함수들을 정의 함으로써 이벤트를 획득할 수 있습니다. 마지막 두 가지(error, "completed" signal)은 가끔 발생하기(emitted) 때문에, 여러분은 값들에 대한 함수를 정의하는데만 집중할 수 있습니다. stream을 듣는("listening") 것을 subscribing 이라고 합니다. 우리가 정의한 함수들은 observer들입니다. stream은 관찰되는 subject(혹은 "observable")입니다. 이것이 확실히 Observer Design Pattern( https://en.wikipedia.org/wiki/Observer_pattern )입니다.

그런 다이어그램을 그리는 다른 방법은 ASCII를 이용하는 겁니다. 이 튜터리얼의 몇 부분에서 ASCII를 사용할 겁니다.

--a---b-c---d---X---|->

a, b, c, d 는 발생한(emitted) 된 값들입니다.
X 는 오류(error)입니다.
| 는 "completed" 신호입니다.
---> 는 "timeline" 입니다.

이런 것들은 이미 충분히 친숙하고, 지루해지고 싶지 않기 때문에, 새로운 것을 해봅시다. 원래의 click 이벤트 stream 로부터 변형된 새로운 click 이벤트 stream들을 생성할 겁니다.

첫번째로, counter stream을 만듭시다. counter stream은 버튼이 몇번 click되었는지 나타냅니다. 보통의 Reactive 라이브러리에는 각각의 stream에 연결할 수 있는 많은 함수들이 있습니다. map, filter, scan, 기타등등 같은 것들이 있습니다. 여러분이 이 함수들 중 하나(clickStream.map(f)같은 함수)를 호출할 때, 그 함수는 그 click stream에 기반한 새로운 stream을 리턴합니다. 그 함수들은 어떤 방식으로 든지 간에 원래 click stream을 변경하지 않습니다. 이것은 불변성(immutability)라고 불리우는 속성입니다. 그리고 불변성은 팬케이크와 시럽이 잘 어울리는 것처럼 Reactive stream들과 함께 잘 어울립니다. 원래 stream은 그대로 두고 새로운 stream을 생성하는 방식은 clickStream.map(f).scan(g)와 같이 함수를 체인으로 연결하는 것을 허용합니다.

clickStream: ---c----c--c----c------c-->
              vvvvv map(c becomes 1) vvvv
              ---1----1--1----1------1-->
              vvvvvvvvv scan(+) vvvvvvvvv
counterStream: ---1----2--3----4------5-->

map(f) 함수는 여러분이 제공한 f 라는 함수에 따라 발생한(emitted) 각각의 값을 치환합니다. 우리의 경우에는 각 click 에 대해서 숫자 1이 사상(map)되도록 합니다. scan(g) 함수는 stream의 모든 이전의 값들을 통합합니다. x = g(accumulated, current) 이라는 값이 생성됩니다. g는 이 예제에서는 간단히 덧셈 함수였습니다. 그런다음 counterStream 은 click 이벤트가 발생할 때마다 click 회수가 발생합니다.

Reactive 의 힘을 보여주기 위해서, "double click" 이벤트들의 stream을 가지기를 원한다고 해봅시다. 더 재미있게 하기 위해서, triple click을 double click으로 간주하는 새로운 stream을 원한다고 해봅시다. 혹은 일반적으로 multiple click들(두번혹은 그 이상)을 double click으로 간주한다고 해봅시다. 심호흡을 하고 전통적인 명령형에 상태를 유지하는 방식으로 그런 것들을 어떻게 해야 할지 상상해보세요. 그것은 꽤 불쾌하게 들릴 겁니다. 그리고 상태를 유지하기 위한 몇개의 변수와 time interval들을 이용한 몇가지 속임수를 포함해야 합니다.

글쎄, Reactive 방식으로는 꽤 간단합니다. 사실, 로직은 4줄짜리 간단한 코드입니다. 하지만 지금은 코드는 무시합시다. 초심자이거나 전문가이거나 할 것 없이, 다이어그램으로 생각하는 것이 stream들을 생성하고 이해하는데 최선의 방법입니다.

회색 상자들은 하나의 stream을 다른 stream으로 변형시키는 함수들입니다. 우선 우리는 250 밀리세컨드 단위의 "event silence"가 발생할 때마다 리스트들에서 click들을 누산합니다. (즉, 한마디로 buffer(stream.throttle(250ms))이 하는 행위입니다. 이 시점에서 자세한 것들을 이해하 못한다고 걱정하지 마세요. 우리는 지금 단지 Reactive 데모를 하고 있을 뿐입니다.) 결과는 리스트의 stream입니다. 그 stream은 우리가 map()을 적용시켜서 각각의 리스트를 리스트의 길이와 매칭되는 숫자로 사상(map)한 것입니다. 마지막으로 우리는 숫자 1들을 filter(x >= 2)를 이용해서 무시합니다. 의도된 stream을 생산하기 위한 3가지 연산들이 그겁니다. 그럼 다음, 우리가 원하는 대로 반응하도록 subscribe("listen")할 수 있습니다.

이런 방식의 아름다움을 즐기셨기를 희망합니다. 이 예제는 단지 빙산의 일각일 뿐입니다. 여러번은 같은 연산을 다른 종류의 stream들에 적용할 수 있습니다. 예를 들어 API 응답에 대한 stream 같은 것이 있습니다. 반면에 적용할 수 있는 많은 다른 함수도 있습니다.

RP를 왜 도입해야 하나요?

Reactive Programming은 여러분의 코드의 추상화 레벨을 올려줍니다. 그래서 여러분은 지속적으로 상당량의 상세사항을 작성해야 하기 보다는 비지니스 로직을 정의하는 이벤트들의 상호의존성에 집중할 수 있습니다. RP로 작성한 코드는 보다 더 간결해질 겁니다.

장점은 데이터 이벤트에 관련된 많은 UI 이벤트를 처리해야 하는 반응성이 높은 모든 웹앱이나 모바일 앱에서 두드러집니다. 10년 전에, 웹 페이지와의 상호작용은 기본적으로 긴 양식을 백엔드로 보내고 프론트엔드에 그리는 것이었습니다. 앱들은 점점 더 실시간으로 발전해왔습니다.: 하나의 양식 field를 수정하는 것은 자동으로 백엔드에 저장되도록 할 수 있습니다. 어떤 내용에 대한 "좋아요"는 접속되어 있는 다른 사용자에게 실시간으로 표시 될 수 있다는 것 등이 있습니다.

요즘 앱들은 사용자에게 매우 반응적인 경험을 가능하게 하는 모든 종류의 실시간 이벤트를 아주 많이 가지고 있습니다. 우리는 그런 것들을 적절하게 처리할 수 있는 툴이 필요합니다. 그리고 Reactive Programming이 그 답입니다.

예제를 가지고 Reactive Programming으로 생각하기

현실적인 것들로 뛰어 들어 봅시다. RP로 생각하는 방법에 대한 step-by-step 가이드를 통한 현실세계 예제. 억지로 만들어 낸 예제들이 아니고, 반쯤 설명하다가 마는 개념이 아닌 진짜 예제. 이 튜터리얼의 끝에서 우리는 각각의 것들을 하는 이유를 알면서 진짜 동작하는 코드를 만들어 낼 겁니다.

저는 JavascriptRxJS 를 이 예제를 위한 툴로 선택했습니다. 이유는 :Javascript는 지금 이 순간 세계에서 제일 인기있는 언어이고, Rx* library family 는 많은 언어와 플랫폼(.NET, Java, Scala, Clojure, Javascript, Ruby, Python, C++, object-C/Cocoa, Groovy, 기타등등)에서 폭 넓게 사용가능합니다. 그래서 여러분의 툴이 무엇이던지 간에, 이 튜터리얼을 따라함으로써 구체적으로 도움이 될 것입니다.

"follow 할 사람" 추천 상자 구현하기

트윗터에는 여러분이 follow할 계정을 추천하는 이런 UI 요소가 있습니다.

우리는 그것의 주 기능을 모방하는데 집중할 것입니다. 주 기능은 다음과 같습니다.

-시작할 때, API로 부터 계정 데이터를 로드하고 추천 3가지를 보여 줍니다.
-Refresh 버튼을 click할때, 3줄로 다른 3 계정의 추천을 로드합니다.
-x 버튼을 click할 때, 현재 계정은 지우고, 다른 계정을 보여줍니다.
-각 줄은 계정의 아바타를 보여주고 그들의 페이지로 연결되어 있습니다.

마이너한 버튼과 기능은 남겨놓기로 합니다. 그리고 최근 미승인된 일반에 대해 API를 닫은 트윗터 대신에 깃헙에 follow하고 있는 사람들에 대한 UI를 만듭시다. 여기 유저들을 얻을 수 있는 깃헙 API( https://developer.github.com/v3/users/#get-all-users )가 있습니다.

바로 결과를 보기를 원한다면, http://jsfiddle.net/staltz/8jFJH/48/ 에 완전한 코드가 있습니다.

Request 와 Response

이 문제에 대해서 Rx로 어떻게 접근할까요? 글쎄요. 우선, (거의) 모든 것은 stream이 될 수 있습니다. 그건 Rx 주문(mantra) 입니다. 가장 쉬운 기능으로 시작해 봅시다.:"시작할 때, API로 부터 3개의 계정 데이터를 로드 합니다." 딱히 특별한 것은 없습니다. 이것은 간단히 (1) 요청(request)를 하는 것 (2) 응답(response)를 받는 것 (3)응답(response)를 그리는 것 에 대한 것입니다. 그럼 좀 더 앞으로 나아가 우리의 요청(request)들을 stream으로 표현해 봅시다. 처음에는 과도하게 느껴질 수 있을 겁니다. 하지만 우리는 기초부터 시작하기를 원합니다. 그렇죠?

시작할 때 우리는 단지 하나의 요청(request)을 보낼 필요가 있습니다. 그래서 우리가 그것을 data stream 으로 모델링했다면, 그것은 단지 하나의 발생한(emitted) 값을 가지는 stream 이 될 것입니다. 나중에, 우리는 많은 요청(request)들을 가지게 될 것을 알지만 지금은 그냥 하나입니다.

--a------|->
a 는 'https://api.github.com/users' 문자열입니다.

이것은 우리가 요청(request)하기 원하는 URL들의 stream 입니다. 요청(request)이 발생할 때 마다, 요청(request)는 우리에게 두가지를 알려줍니다.:때(when) 와 대상(what)     이벤트가 발생할(emitted) 때가 요청(request)이 실행되어야 할 때(when)입니다. 그리고 요청되어야 할 것(what)은 발생한(emitted) 값입니다.:URL 을 포함하는 문자열  

Rx* 에서 하나의 값을 가지는 그런 stream 을 생성하는 것은 매우 간단합니다. stream 에 대한 공식 용어는 "Observable" 입니다, 관찰될 수 있다는 사실 때문에 "Observable" 이라고 하는데, 저는 그것이 바보 같은 이름이라는 것을 압니다. 그래서 저는 stream 이라고 부를 겁니다.

var requestStream = Rx.Observable.just('https://api.github.com/users');

그러나 지금은 그건 단지 문자열들의 stream일 뿐입니다. 아무런 다른 동작을 하지 않습니다. 그래서 우리는 값이 발생할(emitted) 때, 어쨋든 뭔가 발생하기를 원합니다. 그런 것은 stream 을 subscribe( https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md#rxobservableprototypesubscribeobserver--onnext-onerror-oncompleted ) 하면 됩니다.

requestStream.subscribe(function(requestUrl) {
  // execute the request
  jQuery.getJSON(requestUrl, function(responseData) {
    // ...
  });
}

우리는 요청(request) 연산의 비동기성을 다루기 위해서 jQuery Ajax callback(여러분이 이미 알고 있다고 가정합니다.) 을 사용하고 있다는 것을 명심하세요. 하지만 잠시만 기다리세요. Rx 는 비동기 data stream 을 다루기 위한 겁니다.

그 요청(request)에 대한 응답(response)은 미래의 어떤 시점에 도착할 데이터를 포함하는 stream 이 될 수 없을까요? 글쎄, 개념적으로는, 그렇게 될 것으로 보입니다. 그럼 그걸 시도 해봅시다.

requestStream.subscribe(function(requestUrl) {
  // execute the request
  var responseStream = Rx.Observable.create(function (observer) {
    jQuery.getJSON(requestUrl)
    .done(function(response) { observer.onNext(response); })
    .fail(function(jqXHR, status, error) { observer.onError(error); })
    .always(function() { observer.onCompleted(); });
  });
  
  responseStream.subscribe(function(response) {
    // do something with the response
  });
}

Rx.Observable.create()는 명시적으로 각각의 observer에게 알림으로써(혹은 다른 말로, "subscriber") data 이벤트들( onNext() 혹은 오류(onError() )에 대해 여러분의 custom stream 을 생성합니다. 우리가 한 것은 단지 jQuery Ajax Promise 를 감싸는 것이었습니다. 실례지만, 이건 Promise 는 Observable 이라는 말입니까? (*Promise 는 불변(immutable) 값을 캡슐화하는 thread-safe 객체로, 비동기 처리를 위한 것으로, 실제로 값이 생성될 때까지 그것을 참조하는 부분은 block되고 값이 생기면, block이 해제됩니다.)

네, 그렇습니다.

Observable 은 Promise++ 입니다. Rx에서 여러분은 var stream = Rx.Observable.fromPromise(promise)를 함으로써 Promise를 쉽게 Observale로 변환할 수 있습니다. 그럼 그것을 사용합시다. 유일한 차이점은 Observale 들은 Promises/A+( https://promisesaplus.com/ ) 순응(complicant)이 아니라는 점이다. 하지만 개념적으로 충돌은 없습니다. Promise 는 단순히 하나의 발생한(emitted) 값을 가지는 Observable 입니다. Rx stream 들은 많은 리턴된 값들을 허용함으로써 promise 들을 능가합니다.

이건 꽤 좋습니다. 그리고 이것은 Observable들이 최소한 Promise들 만큼 강력한지를 보여줍니다. 그래서 만약 여러분이 Promise 선전 광고를 믿는다면, Rx Observable들이 할 수 있는 것들에 대해서 관심을 가져보세요.

예제로 다시 돌아와서, 만약 여러분이 곧 알아 챘다면, 우리는 콜백 지옥(callback hell) 같은 다른 subscribe() 안에 하나의 subscribe() 호출을 가집니다. 또한, responseStream 은 requestStream 에 의존적입니다. 이전에도 들었던 것처럼, Rx 에는 stream들로부터 다른 stream을 새롭게 생성하고 변환하는 간단한 매커니즘이 있습니다. 그러니 우리도 그렇게 해야 합니다.

지금쯤 여러분이 알아야 하는 기본 함수 하나가 map(f) 입니다. map(f) 는 stream A 의 각각의 값들을 취해서 f()를 적용하고 stream B 에 대한 값을 출력합니다. 만약 우리가 map(f)를 우리의 요청(request) stream들에 적용한다면, 요청(request) URL들을 응답(response) Promise 들로 사상할(map) 수 있습니다.( stream 들로 위장됩니다.)

var responseMetastream = requestStream
  .map(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });

그런 다음 우리는 "mainstream" 이라고 불리는 생성된 괴물을 가지게 될 겁니다.: stream들 중에 stream입니다. 아직 당황하지 마세요. mainstream 은 각각의 발생한(emitted) 값이 아직 다른 stream 인 stream 입니다. 여러분은 그것을 포인터( https://en.wikipedia.org/wiki/Pointer_(computer_programming) )들로 생각할 수 있습니다. : 각각의 발생한(emitted) 값은 다른 stream에 대한 포인터입니다. 우리 예제에서는, 각각의 요청(request) URL 은 관계된 응답(response)을 포함하는 promise stream 에 대한 포인터로 사상(map)됩니다.

응답(response)들에 대한 mainstream 은 혼란스러워 보입니다. 그리고 우리에게 전혀 도움이 되지 않는 것처럼 보입니다. 우리는 단지 응답(response)들에 대한 간단한 stream 을 원합니다. 각각의 발생한(emitted) 값은 JSON 객체이고, JSON 의 'Promise'가 아닌 stream입니다. Flatmap씨에게 인사해보세요. 가지("branch") stream들에 발생한(emitted) 모든 것을 "trunk" stream 에 발생시킴(emitting)으로써 mainstream 을 넓게 펴는("flattens") map의 한 버전입니다. Flatmap은 수정("fix")이 아닙니다. 그리고 mainstream들은 버그가 아닙니다. 이런 것들은 Rx에서 사실상 비동기 응답들을 다루기 위한 도구들입니다.

var responseStream = requestStream
  .flatMap(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });

좋습니다. 그리고 응답(response) stream 은 요청(request) stream에 따라 정의됩니다. 만약 나중에 요청(request) stream 에 더 많은 이벤트가 생긴다면, 기대했던 대로, 응답(response) stream에 발생하는 관련된 응답(response) 이벤트를 가지게 될 겁니다.

requestStream:  --a-----b--c------------|->
responseStream: -----A--------B-----C---|->

(소문자는 요청(request)이고, 대문자는 그것의 응답(response)입니다.)

이제 우리는 마침내 응답(response) stream을 가지게 되었고, 우리가 받은 데이터를 그릴 수 있습니다.

responseStream.subscribe(function(response) {
  // render `response` to the DOM however you wish
});

이제까지 모든 코드를 합치면 이렇게 됩니다.

var requestStream = Rx.Observable.just('https://api.github.com/users');

var responseStream = requestStream
  .flatMap(function(requestUrl) {
    return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
  });

responseStream.subscribe(function(response) {
  // render `response` to the DOM however you wish
});

Refresh 버튼

저는 아직 응답 JSON이 100명의 유저를 가지는 리스트라고 이야기 하지 않았습니다. API는 우리가 페이지 오프셋를 지정하는 것은 허용하고, 페이지 사이즈를 지정하는 것은 허용하지 않습니다. 그래서 우리는 3개의 data object들은 사용하고 97개의 데이터는 버립니다. 나중에 응답(response)들을 cache하는 방법을 알아볼 것이기 때문에,지금은 그 문제는 무시합니다.

refresh button 이 click될 때마다, 요청(request) stream은 새로운 URL을 발생시켜야(emit) 하고 그렇게 해서 새로운 응답(response)를 받을 수 있습니다. 우리는 두가지가 필요합니다.: click 이벤트들의 stream(주문(mantra): 어떤 것이든 간에 stream이 될 수 있습니다.) 그리고 응답(request) stream이 refresh click stream을 의존하도록 변경해야 합니다. 기쁘게도, RxJS에는 Observables를 이벤트 리스너들로 만들기 위한 도구들이 딸려 있습니다.

var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');

refresh click 이벤트 그 자체로는 어떤 API URL을 운반하지 않기 때문에, 우리는 각각의 click 을 실제 URL 로 사상(map)할 필요가 있습니다. 이제 우리는 요청(request) stream를 매번 랜덤 오프셋 파라미터를 가지는 API endpoint 에 사상(map)된 refresh click stream으로 변환합니다.

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

저는 아둔하고, 자동화된 테스트를 가지고 있지 않기 때문에, 이전에 만든 기능중 하나를 망가뜨렸습니다. 시작할 때 요청(request)는 더이상 아무것도 발생시키지 않습니다. refresh 가 click 될 때만 발생합니다. 아아, 저는 두 경우 모두 동작이 필요합니다.: refresh 가 click 되거나 웹페이지가 열릴 때 요청(request)

우리는 다른 경우의 각각의 stream 위한 개별 stream 을 만드는 방법을 알고 있습니다.:

var requestOnRefreshStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });
  
var startupRequestStream = Rx.Observable.just('https://api.github.com/users');

하지만 어떻게 이들 두가지 stream을 하나의 stream 으로 합칠 수 있을까요? 글쎄, merge() 이 있습니다. 다이어그램 방언으로 설명된다면, 이것이 merge 가 하는 일입니다.:

stream A: ---a--------e-----o----->
stream B: -----B---C-----D-------->
          vvvvvvvvv merge vvvvvvvvv
          ---a-B---C--e--D--o----->

이제 쉬울 겁니다.:

var requestOnRefreshStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });
  
var startupRequestStream = Rx.Observable.just('https://api.github.com/users');

var requestStream = Rx.Observable.merge(
  requestOnRefreshStream, startupRequestStream
);

intermediate stream들을 제거한 더 깔끔한 다른 방법으로 작성한 것이 여기 있습니다.

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  })
  .merge(Rx.Observable.just('https://api.github.com/users'));

좀 더 짧고, 읽기 좋게 하면:

var requestStream = refreshClickStream
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  })
  .startWith('https://api.github.com/users');

startWith 함수는 우리가 생각하는 그것이 정확히 맞습니다. 여러분의 입력 stream 이 어떻게 생겼는지 상관없이, startWith(x)의 결과로 만들어지는 output stream은 처음에 x 를 가지게 될겁니다. 하지만, 저는 충분히 DRY( https://en.wikipedia.org/wiki/Don't_repeat_yourself )가 아니기 때문에, API endpoint 문자열을 반복하고 있습니다. 이것을 고치기 위한 한 방법은 근본적으로 시작시에 refresh click을 "emulate"하기 위해서 startWith() 를 refreshClickStream 가까이로 이동시키는 겁니다.

var requestStream = refreshClickStream.startWith('startup click')
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

좋습니다. 여러분이 "저는 자동화된 테스트를 망가뜨렸습니다."의 지점으로 돌아간다면, 여러분은 이 마지막 접근 방식이 단지 제가 startWith()를 추가했던 것만이 차이가 난다는 것을 알수 있어야 합니다.

3가지 추천을 stream 으로 모델링하기

지금까지, 우리는 responseStream의 subscribe()에서 발생하는 그리는 단계의 추천 UI element 를 다뤄왔습니다. 이제는 refresh button과 관련해서 문제가 있가 있습니다.:여러분이 'refresh'를 클릭하지 마자, 현재의 3가지 추천이 지워지지 않습니다. 응답(response)이 도착한 이후에 새로운 추천이 들어 옵니다. 하지만 UI를 멋지게 만들기 위해서, 우리는 refresh 가 click 될때 현재 추천들을 모두 지울 필요가 있습니다.

refreshClickStream.subscribe(function() {
  // clear the 3 suggestion DOM elements 
});

아니요, 여러분 너무 빠르지는 않습니다. 이것은 좋지 않습니다. 우리는 추천 DOM 요소들에 영향을 주는(하나는 responseStream.subscribe()) 두 개의 subscriber들을 가지고 있습니다. 그리고 그것은 Seperation of concerns( https://en.wikipedia.org/wiki/Separation_of_concerns ) 처럼 들리지 않습니다. Reactive 주문(mantra)를 기억하시나요?

그럼 추천을 stream으로 모델링 합시다. 이 stream에서 각각의 발생한(emitted) 값은 추천 데이터를 포함한 JSON 객체입니다. 우리는 각각 3개의 추천에 대해서 개별적으로 이것을 할 겁니다. 추천 #1 에 대한 stream이 어떠해야 하는지를 이것이 보여줍니다.

var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  });

나머지, suggestion2Stream 과 suggestion3Stream 는 간단히 suggestion1Stream 로 부터 복사될 수 있습니다. 이것은 DRY 가 아니지만 이 튜터리얼에 대해 우리의 예제를 간단하게 유지 시켜줄 겁니다. 게다가 이런 경우에 중복을 피하는 방법을 생각하는 것은 좋은 연습이라고 생각합니다.

responseStream의 subscribe() 에서 그리는 것(rendering)이 발생하는 대신에 우리는 여기서 발생시킵니다.

suggestion1Stream.subscribe(function(suggestion) {
  // render the 1st suggestion to the DOM
});

'리프레쉬 될 때, 추천을 지운다'로 돌아와서, 우리는 간단히 refresh click 을 null 추천 데이터로 사상(map)할 수 있습니다. 그리고 suggestion1Stream 에 이렇게 그걸 포함시킵니다.

var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  })
  .merge(
    refreshClickStream.map(function(){ return null; })
  );

그리고 그릴(rendering) 때, 우리는 null "데이터 없음"("no data")으로 해석합니다. 게다사 UI 요소를 숨깁니다.

suggestion1Stream.subscribe(function(suggestion) {
  if (suggestion === null) {
    // hide the first suggestion DOM element
  }
  else {
    // show the first suggestion DOM element
    // and render the data
  }
});

큰 그림은 이렇습니다.

refreshClickStream: ----------o--------o---->
     requestStream: -r--------r--------r---->
    responseStream: ----R---------R------R-->   
 suggestion1Stream: ----s-----N---s----N-s-->
 suggestion2Stream: ----q-----N---q----N-q-->
 suggestion3Stream: ----t-----N---t----N-t-->

여기서 N 은 null 을 의미합니다.

보너스로, 우리는 시작시 "empty" 추천을 그릴 수 있습니다. 그것은 추천 stream 에 startWith(null) 을 추가함으로써 처리됩니다.

var suggestion1Stream = responseStream
  .map(function(listUsers) {
    // get one random user from the list
    return listUsers[Math.floor(Math.random()*listUsers.length)];
  })
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);

결과는 이렇습니다.

refreshClickStream: ----------o---------o---->
     requestStream: -r--------r---------r---->
    responseStream: ----R----------R------R-->   
 suggestion1Stream: -N--s-----N----s----N-s-->
 suggestion2Stream: -N--q-----N----q----N-q-->
 suggestion3Stream: -N--t-----N----t----N-t-->

추천 닫기 와 캐쉬된 응답들

구현할 기능이 한가지 남았습니다. 각각의 추천은 추천을 닫고 그것의 위치에 다른 것을 로딩하기 위한 자신의 'x' 버튼을 가져야 합니다. 처음 생각했을 때, 여러분은 닫기 버튼이 click 되었을 때 새로운 요청(request)를 만드는 것으로 충분하다고 말할 수 있었습니다.:

var close1Button = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(close1Button, 'click');
// and the same for close2Button and close3Button

var requestStream = refreshClickStream.startWith('startup click')
  .merge(close1ClickStream) // we added this
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

그런데 그것은 제대로 작동하지 않습니다. 그것은 닫히고, click한 하나만 다시 로드는 것이 아니라, 모든 추천을 다시 로드 합니다. 이 문제를 푸는 다른 방법은 몇가지 있는데, 재미를 유지하기 위해서, 우리는 이전 응답(response)를 재사용해서 이문제를 해결할 것입니다. API 응답 페이지 크기는 100명의 유저 입니다. 반면에 우리는 그중에서 3명만 사용합니다. 그래서 상당량의 새로운 데이터가 있습니다. 더 요청할 필요가 없습니다.

다시, stream들을 생각해봅시다. 'close1' click 이벤트가 발생할 때, 우리는 응답(response) 리스트에서 랜덤 사용자 하나를 얻기 위해 가장 최근에 responseStream 에 발생한(emitted) 응답(response)을 사용하기를 원합니다.

    requestStream: --r--------------->
   responseStream: ------R----------->
close1ClickStream: ------------c----->
suggestion1Stream: ------s-----s----->

Rx 에는 combineLatest 라고 불리는 우리가 필요한 것을 해주는 것으로 보이는 결합 함수가 있습니다. 그것은 두개의 stream A, B를 입력으로 받습니다. 그리고 둘중 하나의 stream 이 값을 발생시킬(emit) 때 마다, combineLatest 는 두 stream 들로 부터 가장 최근에 발생한(emitted) 두개의 값 a, b 를 합칩니다. 그리고 c = f(x, y) 값을 출력합니다. f는 우리가 정의한 함수입니다. 다이어 그램으로 설명하는 것이 더 좋겠습니다.

stream A: --a-----------e--------i-------->
stream B: -----b----c--------d-------q---->
          vvvvvvvv combineLatest(f) vvvvvvv
          ----AB---AC--EC---ED--ID--IQ---->

where f is the uppercase function

우리는 close1ClickStream 과 responseStream 에 combineLatest() 를 적용할 수 있습니다. 그렇게 해서 close1 버튼이 click될 때마다, 우리는 발생한(emitted) 최신 응답(response)를 얻고, suggestion1Stream 에 새로운 값을 생산합니다. 반면에, combineLatest()는 대칭(symmetric)입니다.: responseStream 에 새로운 응답(response)가 발생할(emitted) 때마다, 그 응답(response)는 가장 최신의 'close 1' click 과 결합되어 새로운 추천을 생산합니다.

sugestion1Stream에 대한 우리의 이전 코드를 이와 같이 간단하게 만들 수 있도록 해주기 때문에 combineLatest() 는 흥미롭습니다.

var suggestion1Stream = close1ClickStream
  .combineLatest(responseStream,             
    function(click, listUsers) {
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);

퍼즐의 한 조각이 아직 완성되지 않았습니다. combineLatest()는 두 source들로 부터 가장 최신의 것들을 사용합니다. 하지만 그 두개의 source들 중에서 하나가 아무것도 발생하지(emitted) 않았다면, combineLatest() 는 output stream 에 data 이벤트를 생산할 수 없습니다. 여러분이 위의 ASCII 다이어 그램을 본다면, 첫 stream이 a 를 발생시켰(emit)을때 아무것도 출력되지 않았음을 알 수 있을 겁니다. 두번째 stream이 값 b 를 발생시킬(emit) 때, 출력값을 생산할 될 수 있습니다.

이 문제를 해결하기 위한 다른 방법들이 존재합니다. 그리고 우리는 시작할 때 'close1' 버튼에 대한 click을 흉내내는 가장 간단한 해결책으로 머무를 겁니다.

var suggestion1Stream = close1ClickStream.startWith('startup click') // we added this
  .combineLatest(responseStream,             
    function(click, listUsers) {l
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);

요약 정리

그리고 완료 되었습니다. 전체 코드는 이렇습니다.

var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');

var closeButton1 = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(closeButton1, 'click');
// and the same logic for close2 and close3

var requestStream = refreshClickStream.startWith('startup click')
  .map(function() {
    var randomOffset = Math.floor(Math.random()*500);
    return 'https://api.github.com/users?since=' + randomOffset;
  });

var responseStream = requestStream
  .flatMap(function (requestUrl) {
    return Rx.Observable.fromPromise($.ajax({url: requestUrl}));
  });

var suggestion1Stream = close1ClickStream.startWith('startup click')
  .combineLatest(responseStream,             
    function(click, listUsers) {
      return listUsers[Math.floor(Math.random()*listUsers.length)];
    }
  )
  .merge(
    refreshClickStream.map(function(){ return null; })
  )
  .startWith(null);
// and the same logic for suggestion2Stream and suggestion3Stream

suggestion1Stream.subscribe(function(suggestion) {
  if (suggestion === null) {
    // hide the first suggestion DOM element
  }
  else {
    // show the first suggestion DOM element
    // and render the data
  }
});

동작하는 예제를 여기( http://jsfiddle.net/staltz/8jFJH/48/ )에서 볼 수 있습니다.

저 코드 조작은 작고 견고합니다.: 저 코드들은 적절한 고려의 분리(seperation of concerns)로 여러 이벤트들을 관리하는 것과
각각의 응답(response)들을 캐쉬하는 것으로 특징을 이룹니다. functional 스타일은 좀 더 명령형보다는 선언적으로 보이게 합니다.: 우리는 실행할 명령의 순서배열을 주지 않고, stream들간의 관계를 정의함으로써 어떤 것이 무엇인지를 이야기하고 있습니다. 예를 들어, 우리는 Rx를 가지고 컴퓨터에게 suggestion1Stream 은 최신 응답(response)으로 부터 한명의 사용자와 결합시킨 'close 1' stream 이라고 이야기 해주었습니다. 게다가 그 최신 응답(response)은 프로그램이 시작하거나 refresh 가 발생할 때 null 이 된다고 이야기 해주었습니다.

게다가 if, for, while 같은 명령형 control flow 요소와 여러분이 JavaScript 애플리케이션에서 기대하는 전형적인 callback-based control flow 도 없다는 것을 알아 두세요. 심지어 위 코드의 subscribe() 안에 있는 if 와 else 도 여러분이 원한다면, filter() 를 사용해서 제거할 수 있습니다. (디테일한 구현은 여러분에게 연습으로 남겨두겠습니다.) Rx 에는 map, filter, scan, merge, combineLatest, startWith 그리고 흐름을 제어하기(control the flow) 위한 더 많은 함수들이 있습니다. 함수 도구 모음은 여러번에게 더 작은 코드로 더 큰 힘을 줍니다.

다음에 오는 것

만약 여러분이 Reactive Programming 을 위한 라이브러리로 Rx* 를 생각하고 있다면, tranform, combine, Observable들 생성에 대한 많은 함수( https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/observable.md ) 에 익숙해지기 위해서 시간을 내세요. 만약 여러분이 이들 함수를 stream 다이어그램으로 이해하고 싶다면, 'RxJava's very useful documentation with marble diagrams'( https://github.com/ReactiveX/RxJava/wiki/Creating-Observables )를 보세요. 뭔가를 하려고 시도하는데 어려움을 겪을 때마다, 다이어 그램을 그리고 다이어그램을 기반으로 생각하고, 함수들의 긴 리스트를 보고, 그리고 다시 한번더 생각해 보세요. 이런 작업흐름이 제 경험상 효과적이었습니다.

Rx* 를 이용한 프로그래밍의 사용법을 시작하면, Cold vs Hot Observales ( https://github.com/Reactive-Extensions/RxJS/blob/master/doc/gettingstarted/creating.md#cold-vs-hot-observables )에 대한 개념을 이해하는 것이 절대적으로 요구됩니다. 만약 여러분이 이걸 무시한다면 나중에 후회하게 될 겁니다. 경고했습니다. 실제 함수형 프로그래밍 언어를 공부하고 Rx* 에 영향을 미치는 Side effect들과 같은 이슈와 친숙 해짐으로써 여러분의 기술을 더 갈고 닦으세요.

하지만, Reactive Programming은 단순히 Rx가 아닙니다. 때때로 Rx 에서 맞닥뜰리게 되는 변덕없이 작업하기에 직관적인 Bacon.js( http://baconjs.github.io/ )도 있습니다. Elm Language( http://elm-lang.org/ )는 그 자신의 카테고리에 존재합니다.: Functional Reactive Programming 언어를 Javascript + HTML + CSS 로 컴파일합니다. time travelling debugger( http://debug.elm-lang.org/ )로 특징됩니다. 꽤 괜찮습니다.

Rx 는 event-heavy frontends 와 앱들에 대해 훌륭하게 동작합니다. 그러나 이런 건 단지 클라이언트 사이트만의 것이 아닙니다. 백엔드에서도 훌륭하게 동작합니다. 그리고 데이터 베이스 근처에서도요. 사실 RxJava 는 Netflix의 API에서 서버 사이드 동시성 처리를 가능하게 하는 핵심 컴포넌트입니다.( http://techblog.netflix.com/2013/02/rxjava-netflix-api.html ) Rx 는 특정 언어혹은 하나의 지정된 타입의 애플리케이션에 국한된 framework가 아닙니다. 그것은 사실 event-driven 소프트웨어를 프로그래밍할 때 여러분이 사용할 수 있는 패러다임입니다.

이 튜터리얼이 여러분에게 도움이 되었다면, 트윗해주세요.( https://twitter.com/intent/tweet?original_referer=https%3A%2F%2Fgist.github.com%2Fstaltz%2F868e7e9bc2a7b8c1f754%2F&text=The%20introduction%20to%20Reactive%20Programming%20you%27ve%20been%20missing&tw_p=tweetbutton&url=https%3A%2F%2Fgist.github.com%2Fstaltz%2F868e7e9bc2a7b8c1f754&via=andrestaltz )