"리액티브 프로그래밍이 뭔가?"
한 스트림은 시간 순서로 정렬된 이벤트 순서이다. 이 스트림 값(어떤 타입), 에러, '완료'신호 3개의 상태를 내뱉을 수 있다. "완료" 지점이 되었다고 하자. 예를들어 버튼이 있는 현재 윈도우나 뷰가 닫혔을 때 이다.
--a---b-c---d---X---|->
a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline
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-->
"왜 RP 적용시키기를 고려해야할까?"
예제와함께 RP처럼 생각하기
"Who to follow" 제안 박스 구현하기
- 시작하면 API로부터 계정 데이터를 불러오고 3가지 제안을 띄운다.
- "Refresh"를 클릭하면 3개의 또다른 계정을 띄운다.
- 계정중에 'x' 버튼을 누르면 그 계정은 사라지고 다른 계정을 띄운다.
- 각 줄은 계정의 아바타를 띄우고, 누르면 그들 페이지로 이동한다.
요청과 응답
--a------|->
Where a is the string 'https://api.github.com/users'
var requestStream = Rx.Observable.just('https://api.github.com/users');
이제부터는 이것이 문자열의 스트림이고 다른 동작을 하지 않으며, 값이 나올때 우리가 필요한대로 어떻게 처리할 수 있다. 이것을 subscribing으로 스트림에 할 수 있다.
requestStream.subscribe(function(requestUrl) {
// execute the request
jQuery.getJSON(requestUrl, function(responseData) {
// ...
});
}
우리는 요청 오퍼레이션의 비동기 처리를 위해 jQuery Ajax 콜백(여러분은 이미 알고 있다고 가정한다)을 사용한다. 그런데 가만보자. Rx는 비동기 데이터 스트림을 다루기위해 있다. 그 요청에 대한 응답이 곧 있다가 받을 데이터를 담은 스트림일 수는 없을까? 음, 개념상으로는 가능해보이니 한번 시도해보자.
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(혹은 다른말로 '구독자')에게 데이터 이벤트(onNext())나 에러(onError())를 명시적으로 알리는 커스텀 스트림을 만든다. 우리가 한 일은 그냥 jQuery Ajax Promise을 감싼 것이다. 잠시만요, 이게 Promise이란게 Observable을 의미하는 건가요?
var responseMetastream = requestStream
.map(function(requestUrl) {
return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
});
그다음 우리는 스트림의 스트림인 "metastream"을 하나 만들 것이다. 아직 당황하지 마라. metastream은 각각 발생했던 값이 있는 또다른 스트림이다. 여러분은 이것을 '포인터'라 생각할 수도 있겠다. 각 발생한 값은 다른 스트림을 가리키는 포인터이다. 우리 예제에서는 각 요청 URL이 Promise 스트림을 가리키는 포인터로 매핑되는데, 이 promise 스트림은 해당되는 응답을 가지고 있다.
var responseStream = requestStream
.flatMap(function(requestUrl) {
return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
});
좋다 응답 스트림이 요청 스트림에 맞춰 정의되었으므로, 후에 요청 스트림에서 이벤트가 발생하면 예상한대로 응답 스트림에서 발생한 응답 이벤트들을 가질 수 있을 것이다.
requestStream: --a-----b--c------------|->
responseStream: -----A--------B-----C---|->
(lowercase is a request, uppercase is its response)
이제 마침내 응답 스트림을 가지게 되었다. 우리가 받은 데이터를 화면에 띄우면 된다.
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
});
새로고침 버튼
var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');
새로고침 클릭 이벤트가 그 자신의 API URL을 관리하지 않으므로 우리는 각 클릭을 실제 URL로 매핑해주어야한다. 이제 요청 스트림을 새로고침 클릭 스트림으로 바꾸었다. 새로고침 클릭 스트림은 매번 랜덤의 페이지 값으로 API endpoint를 바꾸어 매핑한다.
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
내가 좀 어리석고 자동 테스트를 못해서 이전에 만든 기능 중 하나를 가져왔다. 한 요청은 더이상 시작할 때 일어나지 않고 새로고침을 눌렀을때만 일어난다. 아아. 나는 이 요청이 새로고침을 누를때나 사이트를 켰을때나 둘 다 동작하게 하고 싶다.
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');
그러나 이제 '쪼개진' 두 스트림을 어떻게 하나로 합칠까? 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
);
매개 스트림 없이 만들 수 있는 깔끔한 방법의 대안이다.
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() 함수는 당신이 생각하는데로 정확히 그렇게 동작할 것이다. 입력 스트림이 어덯게 생겼든 상관없이 startWith(x)로부터 나온 결과 스트림은 시작부분에서 x를 가질 것이다. 그러나 나는 아직 DRY 하지 못하다. 나는 API endpoint 문자열을 반복에서 쓰고 있다. 이것을 고치기 위한 한가지 방법은 startWith()를 refresgClickStream에 붙이는 것이다. 이것은 시작 시점에 새로고침 클릭을 강제로 시행하기 위함이다.
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가지 제안을 모델링하기
refreshClickStream.subscribe(function() {
// clear the 3 suggestion DOM elements
});
아니다.. 빠르지 않다. 우리는 제안의 DOM 요소에 영향을 주는 두 구독자가 있기때문에 그렇다. 그리고 이것은 일을 쪼개는 것(sepration of concerns)처럼 보이지도 않는다. 리액티브의 주문이 기억나는가?
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는 아니나 튜토리얼의 예제를 간단하게 만들기위해 이렇게 하였고, 추가로 이 경우에는 어떻게 중복을 피할수 있을지 생각해볼수 있는 좋은 기회라 생각된다.
suggestion1Stream.subscribe(function(suggestion) {
// render the 1st suggestion to the DOM
});
"새로고침을 누르면 제안들을 지운다"로 돌아가서, 우리는 간단하게 새로고침 클릭을 제안 데이터에 null로 매핑하여 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; })
);
그리고 화면에 띄울때는 null은 따로 분기처리하여 "데이터가 없음"이라하고 그 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을 의미한다.
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-->
제안을 닫고 캐싱된 응답을 사용하기
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;
});
위의 것은 제대로 동작하지 않는다. 이것은 하나만 눌렀는데 모두 닫으면서 모든 제안을 다시 갱신할 것이다. 이 문제를 해결하기 위한 방법에는 여러개가 있을 것이다. 우리는 이전 응답을 재사용 함으로서 이 문제를 해결할 것이다. API 응답의 페이지 크기는 100명의 사용하지만 우리는 딱 3명만 사용하고 있으므로 남은 데이터를 사용할 수 있을 것이다. 새로운 요청 없이 말이다.
requestStream: --r--------------->
responseStream: ------R----------->
close1ClickStream: ------------c----->
suggestion1Stream: ------s-----s----->
Rx*에서는 우리에게 필요한 것 처럼 보이는 결합함수, combineLatest가 있다. 이 함수는 인풋으로 A, B 스트림을 받고, 어떤 스트림이 값을 발생시키든 그때마다 combineLatest는 가장 최근에 두 스트림으로부터 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()를 적용시킬 수 있으므로, 닫기버튼1이 클릭될 때 마다 마지막에 발생된 응답을 얻어와서 suggestion1Stream에 새 값으로 만든다. 한편 combineLatest()는 대칭적인데, responseStream에서 새 응답이 나올 때 마다 새 제안을 만들어내기위해 마지막 '닫기1' 클릭을 합친다. 이 부분이 재미있는데, 이전에 suggestion1Stream 코드를 간단하게 만들어준다.
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()는 두 소스 중 가장 최신의 것만 사용한다. 그러나 이 소스 중 하나가 아직 아무것도 발생시키지 않았다면, combineLatest()는 아웃풋 스트림에 데이터 이벤트를 만들 수 없을 것이다. 위의 아스키 다이어그램을 보면 첫번째 스트림이 a를 만들었을때 아웃풋에 아무것도 없음을 확인할 수 있을 것이다. 두번째 스트림에서 b 값을 만들었을때 비로소 아웃풋 값이 만들어질 수 있다.
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/ 여기서 예제가 돌아가는 것을 확인할 수 있을 것이다.
다음으로 해야할 것은 무엇인가
'Swift와 iOS' 카테고리의 다른 글
[번역]WWDC 2017에서 새로운 것들 (0) | 2017.08.14 |
---|---|
[번역]WWDC 2017 보는 법 (0) | 2017.08.13 |
(번역) 픽셀아트(Pixel Art)를 만들어보자 - 입문용 (with Cocos2d iOS Framework) (0) | 2015.07.17 |
[iOS] iOS에서 푸시 받는거 놓히지 않기 - Push Notification (0) | 2015.07.04 |
[iOS] 맥에서 구글 드라이브를 이용한 URL 앱 배포 (adhoc) (4) | 2015.05.06 |
WRITTEN BY
- tucan.dev
개인 iOS 개발, tucan9389