compose async with rxjs
TRANSCRIPT
시작하기�전에
[..Event{}..Event{}...Event{}...] .map(e => e.clientX * 2) .filter(x => x > 4) .forEach(updateUI);
6
시작하기�전에
[..Event{}..Event{}...Event{}...] .map(e => e.clientX * 2) .filter(x => x > 4) .forEach(updateUI);
7
시작하기�전에
[..Event{}..Event{}...Event{}...] .map(e => e.clientX * 2) .filter(x => x > 4) .forEach(updateUI);
8
시작하기�전에
[..Event{}..Event{}...Event{}...] .map(e => e.clientX * 2) .filter(x => x > 4) .forEach(updateUI);
9
시작하기�전에
[..Event{}..Event{}...Event{}...] .map(e => e.clientX * 2) .filter(x => x > 4) .forEach(updateUI);
[1, 2, 3, 4] .map(d => d * 2) .filter(d => d > 4) .forEach(console.log);
10
Array�와�Event
• 차이점�
- array�는�각�요소를�동기적�(sync)�으로�조회할�수�있고,�끝이�있다.�
- event�는�각�요소를�비동기적�(async)�으로�조회할�수�있고,�끝이�없다.(하지만�event�listening�을�취소�할�수�있다.)�
• 비동기적으로�생성되는�요소들을�표현하고,�원하는�시점에�취소할�수�있는��타입
13
pull�vs�push
[1, 2, 3].forEach(console.log);
[1..2..3...].forEach(console.log);
16
> 1> 2> 3
> 1> 2> 3// ...
pull�vs�push
[1, 2, 3].forEach(console.log);
[1..2..3...].forEach(console.log);
16
> 1> 2> 3
> 1> 2> 3// ...
pull
pull�vs�push
[1, 2, 3].forEach(console.log);
[1..2..3...].forEach(console.log);
16
> 1> 2> 3
> 1> 2> 3// ...
pull
push
Ben�Lesh,�RxJS�In-Depth�@AngularConnect�2015
“RxJS is LoDash or Underscore for async”
18
Observable�vs�Promise
Promise Observable
single�value multiple�value
not�lazy lazy
not�cancelable cancelable
no�completion�callback completion�callback
20
single�value�vs�multiple�value
• DOM�/�Event�Emitter�events�(0�-�N�values)�
• Animations�(cancelable)�
• REST�API�(1�value)�
• WebSockets�(0�-�N�values,�retry)�
• node.js�core�API�(1�-�N�values)
21
single�value�vs�multiple�value
• DOM�/�Event�Emitter�events�(0�-�N�values)�
• Animations�(cancelable)�
• REST�API�(1�value)�
• WebSockets�(0�-�N�values)�
• node.js�core�API�(1�-�N�values)
22
Promise:�not�lazy
const p = new Promise(resolve => { setTimeout(() => { resolve(‘run'); }, 1000); console.log('started'); });
23
Promise:�not�lazy
const p = new Promise(resolve => { setTimeout(() => { resolve(‘run'); }, 1000); console.log('started'); });
24
executor
Promise:�not�lazy
const p = new Promise(resolve => { setTimeout(() => { resolve(‘run'); }, 1000); console.log('started'); });
25 https://goo.gl/tK39aS
Promise:�not�lazy
const p = new Promise(resolve => { setTimeout(() => { resolve(‘run'); }, 1000); console.log('started'); });
25
> "started"
https://goo.gl/tK39aS
const source = Rx.Observable.create(observer => { setTimeout(() => { observer.next('run'); }, 1000); console.log('started'); });
Observable:�lazy
26
const source = Rx.Observable.create(observer => { setTimeout(() => { observer.next('run'); }, 1000); console.log('started'); });
Observable:�lazy
26
// no console output
const source = Rx.Observable.create(observer => { setTimeout(() => { observer.next('run'); }, 1000); console.log('started'); });
source.subscribe(val => console.log(val));
Observable:�lazy
27 https://goo.gl/qh24FK
const source = Rx.Observable.create(observer => { setTimeout(() => { observer.next('run'); }, 1000); console.log('started'); });
source.subscribe(val => console.log(val));
Observable:�lazy
27
> "started"
https://goo.gl/qh24FK
const source = Rx.Observable.create(observer => { setTimeout(() => { observer.next('run'); }, 1000); console.log('started'); });
source.subscribe(val => console.log(val));
Observable:�lazy
27
> "started"> "run"
https://goo.gl/qh24FK
Promise:�not�cancelable
const p = new Promise(resolve => { var val = 0; setInterval(() => { console.log(`val: ${val}`) resolve(++val); }, 1000); console.log('started'); });
p.then(val => console.log(`result: ${val}`));
28
Promise:�not�cancelable
const p = new Promise(resolve => { var val = 0; setInterval(() => { console.log(`val: ${val}`) resolve(++val); }, 1000); console.log('started'); });
p.then(val => console.log(`result: ${val}`));
28
> "started"
Promise:�not�cancelable
const p = new Promise(resolve => { var val = 0; setInterval(() => { console.log(`val: ${val}`) resolve(++val); }, 1000); console.log('started'); });
p.then(val => console.log(`result: ${val}`));
28
> "started"> "val: 0"
Promise:�not�cancelable
const p = new Promise(resolve => { var val = 0; setInterval(() => { console.log(`val: ${val}`) resolve(++val); }, 1000); console.log('started'); });
p.then(val => console.log(`result: ${val}`));
28
> "started"> "val: 0"> "result: 0” // promise result> "val: 1"> "val: 2"> "val: 3" // ??????????
Observable:�cancelable
const source = Rx.Observable.create(observer => { var val = 0; const id = setInterval(() => { console.log(`val: ${val}`) observer.next(++val); }, 1000); console.log('started'); return () => { clearInterval(id); console.log('cancelled'); }; });
const subscription = source.subscribe(val => console.log(`result: ${val}`));
setTimeout(() => subscription.unsubscribe(), 5000);
29 https://goo.gl/qJ1zRi
Observable:�cancelable
const source = Rx.Observable.create(observer => { var val = 0; const id = setInterval(() => { console.log(`val: ${val}`) observer.next(++val); }, 1000); console.log('started'); return () => { clearInterval(id); console.log('cancelled'); }; });
const subscription = source.subscribe(val => console.log(`result: ${val}`));
setTimeout(() => subscription.unsubscribe(), 5000);
> "started" > "val: 0" > "result: 0" > "val: 1" > "result: 1" // ...
30 https://goo.gl/qJ1zRi
Observable:�cancelable
const source = Rx.Observable.create(observer => { var val = 0; const id = setInterval(() => { console.log(`val: ${val}`) observer.next(++val); }, 1000); console.log('started'); return () => { clearInterval(id); console.log('cancelled'); }; });
const subscription = source.subscribe(val => console.log(`result: ${val}`));
setTimeout(() => subscription.unsubscribe(), 5000);
> "started" > "val: 0" > "result: 0" > "val: 1" > "result: 1" // ...
30
teardown�logic
https://goo.gl/qJ1zRi
Observable:�cancelable
const source = Rx.Observable.create(observer => { var val = 0; const id = setInterval(() => { console.log(`val: ${val}`) observer.next(++val); }, 1000); console.log('started'); return () => { clearInterval(id); console.log('cancelled'); }; });
const subscription = source.subscribe(val => console.log(`result: ${val}`));
setTimeout(() => subscription.unsubscribe(), 5000);
> "started" > "val: 0" > "result: 0" > "val: 1" > "result: 1" // ...
30
> "cancelled"
teardown�logic
https://goo.gl/qJ1zRi
Operators
• observable�타입을�변환�/�조합할�수�있음�
• RxJS@5�기준�+200개�
• 종류�
- creation��
- transformation�
- filtering�
- composition�
- error�
- ...
33
from
const source = Rx.Observable.from([1,2,3,4,5])
source.subscribe(val => console.log(val));
> 1 > 2 > 3 > 4 > 5
35
Array�function�vs�RxJS
const source = [1,2,3,4,5];
const result = source .filter(d => d % 2 === 0) .map(d => d + '!') .reduce((p, d) => p + d)
console.log(result);
> "2!4!"
36
Array�function�vs�RxJS
const source =[1,2,3,4,5];
const result = source .filter((d, i, arr) => { console.log(`filter: ${d}`); return d % 2 === 0; }) .map((d, i, arr) => { console.log(`map: ${d}`); return d + '!'; }) .reduce((p, d, i, arr) => { console.log(`reduce: ${d}`); return p + d; })
console.log(result);
> "filter: 1" > "filter: 2" > "filter: 3" > "filter: 4" > "filter: 5" > "map: 2" > "map: 4" > "reduce: 2!" > "reduce: 4!" > "2!4!"
37 https://goo.gl/ma2vxb
Array�function�vs�RxJS
const source = Rx.Observable.from[1,2,3,4,5];
source .filter(d => d % 2 === 0) .map(d => d + '!') .reduce((p, d) => p + d) .subscribe(result => console.log(result));
> "2!4!"
38
Array�function�vs�RxJS
const source = Rx.Observable.from([1,2,3,4,5]);
source .filter(d => { console.log(`filter: ${d}`); return d % 2 === 0; }) .map(d => { console.log(`map: ${d}`); return d + '!'; }) .reduce((p, d) => { console.log(`reduce: ${d}`); return p + d; }, '') .subscribe(result => console.log(result));
> "filter: 1" > "filter: 2" > "map: 2" > "reduce: 2!" > "filter: 3" > "filter: 4" > "map: 4" > "reduce: 4!" > "filter: 5" > "2!4!"
39 https://goo.gl/5dtLXF
Array�function�vs�RxJS
const source = Rx.Observable.from([1,2,3,4,5]);
source .filter(d => { console.log(`filter: ${d}`); return d % 2 === 0; }) .map(d => { console.log(`map: ${d}`); return d + '!'; }) .reduce((p, d) => { console.log(`reduce: ${d}`); return p + d; }, '') .subscribe(result => console.log(result));
> "filter: 1" > "filter: 2" > "map: 2" > "reduce: 2!" > "filter: 3" > "filter: 4" > "map: 4" > "reduce: 4!" > "filter: 5" > "2!4!"
40 https://goo.gl/5dtLXF
fromEvent
const source = Rx.Observable.fromEvent(document, ‘click');
source.subscribe(val => console.log(val));
43
fromEvent
const source = Rx.Observable.fromEvent(document, 'click'); .map(event => event.x) .filter(x => x > 100);
source.subscribe(val => console.log(val));
44
fromPromise
const request = axios.get(URL);
const source = Rx.Observable.fromPromise(request); .map(res => res.data);
source.subscribe(val => console.log(val));
45
interval
const source = Rx.Observable.interval(1000);
source.subscribe(val => { console.log(val); }, err => { console.error(err); }, () => { console.log('completed'); });
46 https://goo.gl/ybHNDZ
interval
const source = Rx.Observable.interval(1000);
source.subscribe(val => { console.log(val); }, err => { console.error(err); }, () => { console.log('completed'); });
46
> 0> 1> 2> 3// ...
https://goo.gl/ybHNDZ
interval
const source = Rx.Observable.interval(1000);
source.subscribe(val => { console.log(val); }, err => { console.error(err); }, () => { console.log('completed'); });
46
> 0> 1> 2> 3// ...// "completed" will not be printed
https://goo.gl/ybHNDZ
map
const source = Rx.Observable.from([1,2,3,4]) .map(d => d * 10);
source.subscribe(val => console.log(val));
> 10 > 20 > 30 > 40
48
mergeMap�(flatMap)
_.flatMap([1, 2], d => [d, d]);
// [[1, 1], [2, 2]]
> [1, 1, 2, 2]
_.flatten([[1, 1], [2, 2]]);
> [1, 1, 2, 2]
49
mergeMap�(flatMap)
[..1..2..3].mergeMap(d => [...d])
[[...1]..[...2]..[...3]]
[....1......2......3]
50
source projection
result
mergeMap�(flatMap)
[..1..2..3].mergeMap(d => [...d])
[[...1]..[...2]..[...3]]
[....1......2......3]
50
source projection
result
mergeMap�(flatMap)
[..1..2..3].mergeMap(d => [...d])
[[...1]..[...2]..[...3]]
[....1......2......3]
50
source projection
result
mergeMap�(flatMap)
[..1..2..3].mergeMap(d => [...d])
[[...1]..[...2]..[...3]]
[....1......2......3]
50
source projection
result
mergeMap�(flatMap)
[..1..2..3].mergeMap(d => [...d])
[[...1]..[...2]..[...3]]
[....1......2......3]
50
source projection
result
mergeMap�(flatMap)
[..1..2..3].mergeMap(d => [...d])
[[...1]..[...2]..[...3]]
[....1......2......3]
50
source projection
result
mergeMap�(flatMap)
[..1..2..3].mergeMap(d => [...d])
[[...1]..[...2]..[...3]]
[....1......2......3]
50
source projection
result
mergeMap�(flatMap)
[..1..2..3].mergeMap(d => [...d])
[[...1]..[...2]..[...3]]
[....1......2......3]
50
source projection
result
mergeMap�(flatMap)
[..1..2..3].mergeMap(d => [...d])
[[...1]..[...2]..[...3]]
[....1......2......3]
50
source projection
result
mergeMap�(flatMap)
[..1..2..3].mergeMap(d => [...d])
[[...1]..[...2]..[...3]]
[....1......2......3]
50
source projection
result
mergeMap�(flatMap)
const source = requestUser$() .mergeMap(res => { const {data: {userId}} = res; return requestPosts$({userId}); });
source.subscribe(res => console.log(res));
51 https://goo.gl/SHgVZ4
mergeMap�(flatMap)
const source = requestUser$() .mergeMap(res => { const {data: {userId}} = res; return requestPosts$({userId}); });
source.subscribe(res => console.log(res));
52 https://goo.gl/SHgVZ4
mergeMap�(flatMap)
const source = requestUser$() // [.....user] .mergeMap(res => { const {data: {userId}} = res; return requestPosts$({userId}); // [....[..posts]] });
source.subscribe(res => console.log(res)); // [.....posts]
53 https://goo.gl/SHgVZ4
mergeMap�(flatMap)
const source = requestUser$() .mergeMap(res => { const {data: {userId}} = res; return axios.get(`${URL}/posts`, {params: {id: userId}}); });
source.subscribe(res => console.log(res.data));
54 https://goo.gl/FKaQLr
filter
const source = Rx.Observable.from([1,2,3,4]) .filter(d => d > 3);
source.subscribe(val => console.log(val));
> 4
56
take
const source = Rx.Observable.interval(1000) .take(3);
source.subscribe(val => { console.log(val); }, err => { console.error(err); }, () => { console.log('completed'); });
57 https://goo.gl/NkGcDo
take
const source = Rx.Observable.interval(1000) .take(3);
source.subscribe(val => { console.log(val); }, err => { console.error(err); }, () => { console.log('completed'); });
57
> 0> 1> 2> "completed"
https://goo.gl/NkGcDo
takeUntil
const source = Rx.Observable.interval(1000) .takeUntil(Rx.Observable.timer(3500));
source.subscribe(val => { console.log(val); }, err => { console.error(err); }, () => { console.log('completed'); });
59 https://goo.gl/ZaXJFS
takeUntil
const source = Rx.Observable.interval(1000) .takeUntil(Rx.Observable.timer(3500));
source.subscribe(val => { console.log(val); }, err => { console.error(err); }, () => { console.log('completed'); });
60 https://goo.gl/ZaXJFS
takeUntil
const source = Rx.Observable.interval(1000) .takeUntil(Rx.Observable.timer(3500));
source.subscribe(val => { console.log(val); }, err => { console.error(err); }, () => { console.log('completed'); });
61 https://goo.gl/ZaXJFS
takeUntil
const source = Rx.Observable.interval(1000) .takeUntil(Rx.Observable.timer(3500));
source.subscribe(val => { console.log(val); }, err => { console.error(err); }, () => { console.log('completed'); });
61
> 0> 1> 2> "completed"
https://goo.gl/ZaXJFS
combineLatest
combineLatest( [...0...1...2...3...4...], // source1
[.......@], // source2
[...........#] // source3 )
[ ...........[2, @, #]...[3, @, #]...[4, @, #]... ]
63
combineLatest
combineLatest( [...0...1...2...3...4...], // source1
[.......@], // source2
[...........#] // source3 )
[ ...........[2, @, #]...[3, @, #]...[4, @, #]... ]
63
combineLatest
combineLatest( [...0...1...2...3...4...], // source1
[.......@], // source2
[...........#] // source3 )
[ ...........[2, @, #]...[3, @, #]...[4, @, #]... ]
63
combineLatest
combineLatest( [...0...1...2...3...4...], // source1
[.......@], // source2
[...........#] // source3 )
[ ...........[2, @, #]...[3, @, #]...[4, @, #]... ]
63
@
combineLatest
combineLatest( [...0...1...2...3...4...], // source1
[.......@], // source2
[...........#] // source3 )
[ ...........[2, @, #]...[3, @, #]...[4, @, #]... ]
63
@ @
#
combineLatest
combineLatest( [...0...1...2...3...4...], // source1
[.......@], // source2
[...........#] // source3 )
[ ...........[2, @, #]...[3, @, #]...[4, @, #]... ]
63
@ @ @
# #
combineLatest
const source1 = Rx.Observable.interval(1000); const source2 = Rx.Observable.timer(2000).mapTo(‘@'); const source3 = Rx.Observable.timer(3000).mapTo(‘#’);
Rx.Observable.combineLatest(source1, source2, source3) .subscribe(result => console.log(result));
64 https://goo.gl/t9vpLF
combineLatest
const source1 = Rx.Observable.interval(1000); const source2 = Rx.Observable.timer(2000).mapTo(‘@'); const source3 = Rx.Observable.timer(3000).mapTo(‘#’);
Rx.Observable.combineLatest(source1, source2, source3) .subscribe(result => console.log(result));
64
> [2, “@", “#"]> [3, “@", “#"]> [4, “@", “#"]// ...
https://goo.gl/t9vpLF
zip
zip( [...0...1...2...3...4...5], // source1
[.....0.....1.....2...] // source2 )
[ ....[0, 0]....[1, 1]....[2, 2]... ]
65
zip
zip( [...0...1...2...3...4...5], // source1
[.....0.....1.....2...] // source2 )
[ ....[0, 0]....[1, 1]....[2, 2]... ]
65
zip
zip( [...0...1...2...3...4...5], // source1
[.....0.....1.....2...] // source2 )
[ ....[0, 0]....[1, 1]....[2, 2]... ]
65
zip
zip( [...0...1...2...3...4...5], // source1
[.....0.....1.....2...] // source2 )
[ ....[0, 0]....[1, 1]....[2, 2]... ]
65
zip
const source1 = Rx.Observable.interval(1000); const source2 = Rx.Observable.interval(2000);
Rx.Observable.zip(source1, source2) .subscribe(result => console.log(result));
66 https://goo.gl/wmQW7m
zip
const source1 = Rx.Observable.interval(1000); const source2 = Rx.Observable.interval(2000);
Rx.Observable.zip(source1, source2) .subscribe(result => console.log(result));
66
> [0, 0]> [1, 1]> [2, 2]// ...
https://goo.gl/wmQW7m
merge
const source1 = Rx.Observable.interval(1000); const source2 = Rx.Observable.timer(2500).mapTo('source2');
Rx.Observable.merge(source1, source2) .subscribe(result => console.log(result));
> 0 > 1 > 2 > "source2" > 3 // ...
67 https://goo.gl/OqaQf9
retry
Rx.Observable.interval(1000) .map(val => { if (val > 2) { throw new Error('over 2'); } return val; }) .retry(2) .subscribe(result => { console.log(result); }, err => { console.error(`error: ${error.message}`); });
69 https://goo.gl/5zFqOl
retry
Rx.Observable.interval(1000) .map(val => { if (val > 2) { throw new Error('over 2'); } return val; }) .retry(2) .subscribe(result => { console.log(result); }, err => { console.error(`error: ${error.message}`); });
70 https://goo.gl/5zFqOl
retry
Rx.Observable.interval(1000) .map(val => { if (val > 2) { throw new Error('over 2'); } return val; }) .retry(2) .subscribe(result => { console.log(result); }, err => { console.error(`error: $error.message}`); });
71 https://goo.gl/5zFqOl
retry
Rx.Observable.interval(1000) .map(val => { if (val > 2) { throw new Error('over 2'); } return val; }) .retry(2) .subscribe(result => { console.log(result); }, err => { console.error(`error: $error.message}`); });
71 https://goo.gl/5zFqOl
> 0> 1> 2
> “error: over 2"
> 0> 1> 2> 0> 1> 2
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
💥
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
💥
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
💥
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
💥
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
retryWhen
[...0...1...2...3].retryWhen(err => [......0])
[...0...1...2........0...1...2........0...1...2.]
72
source notifier
result
retryWhen
73
Rx.Observable.interval(1000) .map(val => { if (val > 2) { throw new Error('over 2'); } return val; }) .retryWhen(error => { return error .do(err => console.error(err.message)) .delay(2000); }) .subscribe(result => console.log(result));
https://goo.gl/9bhNHh
retryWhen
74
Rx.Observable.interval(1000) .map(val => { if (val > 2) { throw new Error('over 2'); } return val; }) .retryWhen(error => { return error .do(err => console.error(err.message)) .delay(2000); }) .subscribe(result => console.log(result));
https://goo.gl/9bhNHh
retryWhen
75
Rx.Observable.interval(1000) .map(val => { if (val > 2) { throw new Error('over 2'); } return val; }) .retryWhen(error => { return error .do(err => console.error(err.message)) .delay(2000); }) .subscribe(result => console.log(result));
https://goo.gl/9bhNHh
retryWhen
75
Rx.Observable.interval(1000) .map(val => { if (val > 2) { throw new Error('over 2'); } return val; }) .retryWhen(error => { return error .do(err => console.error(err.message)) .delay(2000); }) .subscribe(result => console.log(result));
https://goo.gl/9bhNHh
> "over 2"
> 0> 1> 2
> "over 2"
> 0> 1> 2
> "over 2"
> 0> 1> 2
// ...
operators
• switchMap 🌟
• concatMap 🌟
• publish
• share
• skip
• bufferCount
• bufferTime
• Throttle
• debounce
76
• concat
• catch
• bindNodeCallback
• +others
Compose�async�with�RxJS
• 여러�이벤트를�조합해야�할�때�(Drag�&�Drop)�
• 다수의�비동기�함수를�조합하는�상황�(Cache�or�Db,�AWS�Lambda)�
• 재시도가�필요한�경우�(on�/�offline)
77
Drag�&�Drop
• mousedown,�mousemove,�mouseup�3가지�유저�이벤트를�적절하게�조합해야�함�
- mousedown�이벤트가�발생하면�현재�엘리먼트의�초기�위치를�저장�
- mousemove�이벤트가�발생하면�초기�위치�+�변한�위치를�계산해�새로운�위치로�엘리먼트를�옮김�
- 언제까지?�mouseup�이벤트가�발생할�때까지
78
Drag�&�Drop
const target = document.getElementById('target');
const down = Rx.Observable.fromEvent(target, 'mousedown'); const move = Rx.Observable.fromEvent(document, 'mousemove'); const up = Rx.Observable.fromEvent(target, 'mouseup');
const drag = down.flatMap(downEvent => { const downX = downEvent.clientX; const downY = downEvent.clientY; const downLeft = parseInt(downEvent.target.style.left, 10) || 0; const downTop = parseInt(downEvent.target.style.top, 10) || 0; return move.map(moveEvent => { moveEvent.preventDefault(); return { left: moveEvent.clientX + (downLeft - downX), top : moveEvent.clientY + (downTop - downY) }; }) .takeUntil(up); });
drag.subscribe(result => { target.style.top = result.top + 'px'; target.style.left = result.left + 'px'; });
79 https://goo.gl/nyeK8l
Drag�&�Drop
const target = document.getElementById('target');
const down = Rx.Observable.fromEvent(target, 'mousedown'); const move = Rx.Observable.fromEvent(document, 'mousemove'); const up = Rx.Observable.fromEvent(target, 'mouseup');
const drag = down.flatMap(downEvent => { const downX = downEvent.clientX; const downY = downEvent.clientY; const downLeft = parseInt(downEvent.target.style.left, 10) || 0; const downTop = parseInt(downEvent.target.style.top, 10) || 0; return move.map(moveEvent => { moveEvent.preventDefault(); return { left: moveEvent.clientX + (downLeft - downX), top : moveEvent.clientY + (downTop - downY) }; }) .takeUntil(up); });
drag.subscribe(result => { target.style.top = result.top + 'px'; target.style.left = result.left + 'px'; });
80 https://goo.gl/nyeK8l
Drag�&�Drop
const target = document.getElementById('target');
const down = Rx.Observable.fromEvent(target, 'mousedown'); const move = Rx.Observable.fromEvent(document, 'mousemove'); const up = Rx.Observable.fromEvent(target, 'mouseup');
const drag = down.flatMap(downEvent => { const downX = downEvent.clientX; const downY = downEvent.clientY; const downLeft = parseInt(downEvent.target.style.left, 10) || 0; const downTop = parseInt(downEvent.target.style.top, 10) || 0; return move.map(moveEvent => { moveEvent.preventDefault(); return { left: moveEvent.clientX + (downLeft - downX), top : moveEvent.clientY + (downTop - downY) }; }) .takeUntil(up); });
drag.subscribe(result => { target.style.top = result.top + 'px'; target.style.left = result.left + 'px'; });
81 https://goo.gl/nyeK8l
Drag�&�Drop
const target = document.getElementById('target');
const down = Rx.Observable.fromEvent(target, 'mousedown'); const move = Rx.Observable.fromEvent(document, 'mousemove'); const up = Rx.Observable.fromEvent(target, 'mouseup');
const drag = down.flatMap(downEvent => { const downX = downEvent.clientX; const downY = downEvent.clientY; const downLeft = parseInt(downEvent.target.style.left, 10) || 0; const downTop = parseInt(downEvent.target.style.top, 10) || 0; return move.map(moveEvent => { moveEvent.preventDefault(); return { left: moveEvent.clientX + (downLeft - downX), top : moveEvent.clientY + (downTop - downY) }; }) .takeUntil(up); });
drag.subscribe(result => { target.style.top = result.top + 'px'; target.style.left = result.left + 'px'; });
82 https://goo.gl/nyeK8l
Drag�&�Drop
const target = document.getElementById('target');
const down = Rx.Observable.fromEvent(target, 'mousedown'); const move = Rx.Observable.fromEvent(document, 'mousemove'); const up = Rx.Observable.fromEvent(target, 'mouseup');
const drag = down.flatMap(downEvent => { const downX = downEvent.clientX; const downY = downEvent.clientY; const downLeft = parseInt(downEvent.target.style.left, 10) || 0; const downTop = parseInt(downEvent.target.style.top, 10) || 0; return move.map(moveEvent => { moveEvent.preventDefault(); return { left: moveEvent.clientX + (downLeft - downX), top : moveEvent.clientY + (downTop - downY) }; }) .takeUntil(up); });
drag.subscribe(result => { target.style.top = result.top + 'px'; target.style.left = result.left + 'px'; });
83 https://goo.gl/nyeK8l
Drag�&�Drop
const target = document.getElementById('target');
const down = Rx.Observable.fromEvent(target, 'mousedown'); const move = Rx.Observable.fromEvent(document, 'mousemove'); const up = Rx.Observable.fromEvent(target, 'mouseup');
const drag = down.flatMap(downEvent => { const downX = downEvent.clientX; const downY = downEvent.clientY; const downLeft = parseInt(downEvent.target.style.left, 10) || 0; const downTop = parseInt(downEvent.target.style.top, 10) || 0; return move.map(moveEvent => { moveEvent.preventDefault(); return { left: moveEvent.clientX + (downLeft - downX), top : moveEvent.clientY + (downTop - downY) }; }) .takeUntil(up); });
drag.subscribe(result => { target.style.top = result.top + 'px'; target.style.left = result.left + 'px'; });
84 https://goo.gl/nyeK8l
Cache�or�DB
85
• cache�또는�DB�에서�데이터를�가져오는�상황�
- getFromCache$(), getFromDB$()
- 둘�중에�먼저�도착한�데이터만�사용하고�싶다.�
- 나머지�요청은�취소되어야�함�
- getFromOtherServer$()
- 도착한�데이터의�값을�사용해�다른�요청�수행
Cache�or�Db
Rx.Observable.merge(getFromCache$(), getFromDB$()) .take(1) .flatMap(data => getFromOtherServer$(data.id)) .subscribe(result => { res.json(result) }, err => { res.status(500).send(err.message) });
86
Cache�or�Db
Rx.Observable.merge(getFromCache$(), getFromDB$()) .take(1) .flatMap(data => getFromOtherServer$(data.id)) .subscribe(result => { res.json(result) }, err => { res.status(500).send(err.message) });
87
Cache�or�Db
Rx.Observable.merge(getFromCache$(), getFromDB$()) .take(1) .flatMap(data => getFromOtherServer$(data.id)) .subscribe(result => { res.json(result) }, err => { res.status(500).send(err.message) });
88
AWS�Lambda
90
• Task�function�
- getTasks$()
- 태스크�큐에�쌓여있는�태스크�정보�가져오기�
- checkNotified$()
- renderTaskCount$()
- 실패한�태스크가�존재하면�vega-renderer�function�호출�
• vega-renderer�function�
- upload$()
- 렌더링된�이미지를�s3�에�저장�
- post$()
- 저장된�이미지�경로와�함께�슬랙에�알림�전송
Task�function
export default (e, ctx, cb) => { const tasks$ = getTasks$(); const notified$ = checkNotified$();
combineLatest(tasks$, notified$, (tasks, notified) => { if (notified) { return O.return('already notified'); } else if (tasks.length === 0) { return O.return('queue empty'); } return renderTaskCount$(tasks); }) .concatAll() .subscribe(result => { ctx.callbackWaitsForEmptyEventLoop = false; cb(null, result); }, err => { ctx.callbackWaitsForEmptyEventLoop = false; cb(err); }); };
91
Task�function
export default (e, ctx, cb) => { const tasks$ = getTasks$(); const notified$ = checkNotified$();
combineLatest(tasks$, notified$, (tasks, notified) => { if (notified) { return O.return('already notified'); } else if (tasks.length === 0) { return O.return('queue empty'); } return renderTaskCount$(tasks); }) .concatAll() .subscribe(result => { ctx.callbackWaitsForEmptyEventLoop = false; cb(null, result); }, err => { ctx.callbackWaitsForEmptyEventLoop = false; cb(err); }); };
92
Task�function
export default (e, ctx, cb) => { const tasks$ = getTasks$(); const notified$ = checkNotified$();
combineLatest(tasks$, notified$, (tasks, notified) => { if (notified) { return O.return('already notified'); } else if (tasks.length === 0) { return O.return('queue empty'); } return renderTaskCount$(tasks); }) .concatAll() .subscribe(result => { ctx.callbackWaitsForEmptyEventLoop = false; cb(null, result); }, err => { ctx.callbackWaitsForEmptyEventLoop = false; cb(err); }); };
93
Task�function
export default (e, ctx, cb) => { const tasks$ = getTasks$(); const notified$ = checkNotified$();
combineLatest(tasks$, notified$, (tasks, notified) => { if (notified) { return O.return('already notified'); } else if (tasks.count === 0) { return O.return('queue empty'); } return renderTaskCount$(tasks); }) .concatAll() .subscribe(result => { ctx.callbackWaitsForEmptyEventLoop = false; cb(null, result); }, err => { ctx.callbackWaitsForEmptyEventLoop = false; cb(err); }); };
94
Task�function
95
const lambda = new Aws.Lambda();
const invoke$ = (FunctionName, Payload, InvocationType = 'RequestResponse') => { return Rx.Observable.bindNodeCallback(lambda.invoke, lambda)({ FunctionName, Payload, InvocationType }); };
export const renderTaskCount$ = values => { return invoke$('vega-renderer_png', JSON.stringify({ isLite: true, spec: Object.assign(TASK_SPEC, {data: {values}}) })) .map(res => JSON.parse(res.Payload)); };
Task�function
96
const lambda = new Aws.Lambda();
const invoke$ = (FunctionName, Payload, InvocationType = 'RequestResponse') => { return Rx.Observable.bindNodeCallback(lambda.invoke, lambda)({ FunctionName, Payload, InvocationType }); };
export const renderTaskCount$ = values => { return invoke$('vega-renderer_png', JSON.stringify({ isLite: true, spec: Object.assign(TASK_SPEC, {data: {values}}) })) .map(res => JSON.parse(res.Payload)); };
Task�function
97
const lambda = new Aws.Lambda();
const invoke$ = (FunctionName, Payload, InvocationType = 'RequestResponse') => { return Rx.Observable.bindNodeCallback(lambda.invoke, lambda)({ FunctionName, Payload, InvocationType }); };
export const renderTaskCount$ = values => { return invoke$('vega-renderer_png', JSON.stringify({ isLite: true, spec: Object.assign(TASK_SPEC, {data: {values}}) })) .map(res => JSON.parse(res.Payload)); };
vega-renderer�function
98
const s3 = new Aws.S3(); const upload$ = Rx.Observable.bindNodeCallback(s3.upload, s3);
export default (event, ctx, cb) => { const {spec, isLite} = event; const filename = hash(spec);
spec$(spec, isLite) .map(chart => png(chart, filename)) .flatMap(image => upload$(image)) .flatMap(data => post$(event, data.Location)) .subscribe(result => { cb(null, result); }, err => { console.log(err); cb(err); }); };
vega-renderer�function
99
const s3 = new Aws.S3(); const upload$ = Rx.Observable.bindNodeCallback(s3.upload, s3);
export default (event, ctx, cb) => { const {spec, isLite} = event; const filename = hash(spec);
spec$(spec, isLite) .map(chart => png(chart, filename)) .flatMap(image => upload$(image)) .flatMap(data => post$(event, data.Location)) .subscribe(result => { cb(null, result); }, err => { console.log(err); cb(err); }); };
vega-renderer�function
100
const s3 = new Aws.S3(); const upload$ = Rx.Observable.bindNodeCallback(s3.upload, s3);
export default (event, ctx, cb) => { const {spec, isLite} = event; const filename = hash(spec);
spec$(spec, isLite) .map(chart => png(chart, filename)) .flatMap(image => upload$(image)) .flatMap(data => post$(event, data.Location)) .subscribe(result => { cb(null, result); }, err => { console.log(err); cb(err); }); };
vega-renderer�function
101
const s3 = new Aws.S3(); const upload$ = Rx.Observable.bindNodeCallback(s3.upload, s3);
export default (event, ctx, cb) => { const {spec, isLite} = event; const filename = hash(spec);
spec$(spec, isLite) .map(chart => png(chart, filename)) .flatMap(image => upload$(image)) .flatMap(data => post$(event, data.Location)) .subscribe(result => { cb(null, result); }, err => { console.log(err); cb(err); }); };
vega-renderer�function
102
const s3 = new Aws.S3(); const upload$ = Rx.Observable.bindNodeCallback(s3.upload, s3);
export default (event, ctx, cb) => { const {spec, isLite} = event; const filename = hash(spec);
spec$(spec, isLite) .map(chart => png(chart, filename)) .flatMap(image => upload$(image)) .flatMap(data => post$(event, data.Location)) .subscribe(result => { cb(null, result); }, err => { console.log(err); cb(err); }); };
on�/�offline
Rx.Observable.interval(1000) .map(val => { if (val > 5) { throw new Error('too high'); } return val; }) .retryWhen(errors => { return errors.delayWhen(() => { const online = window.navigator.onLine; return online ? Rx.Observable.timer(1000) : Rx.Observable.fromEvent(window, 'online') }); }) .subscribe(val => console.log(val))
105 https://goo.gl/SAmmsD
on�/�offline
106 https://goo.gl/SAmmsD
Rx.Observable.interval(1000) .map(val => { if (val > 5) { throw new Error('too high'); } return val; }) .retryWhen(errors => { return errors.delayWhen(() => { const online = window.navigator.onLine; return online ? Rx.Observable.timer(1000) : Rx.Observable.fromEvent(window, 'online') }); }) .subscribe(val => console.log(val))
on�/�offline
107 https://goo.gl/SAmmsD
Rx.Observable.interval(1000) .map(val => { if (val > 5) { throw new Error('too high'); } return val; }) .retryWhen(errors => { return errors.delayWhen(() => { const online = window.navigator.onLine; return online ? Rx.Observable.timer(1000) : Rx.Observable.fromEvent(window, 'online') }); }) .subscribe(val => console.log(val))
on�/�offline
108 https://goo.gl/SAmmsD
Rx.Observable.interval(1000) .map(val => { if (val > 5) { throw new Error('too high'); } return val; }) .retryWhen(errors => { return errors.delayWhen(() => { const online = window.navigator.onLine; return online ? Rx.Observable.timer(1000) : Rx.Observable.fromEvent(window, 'online') }); }) .subscribe(val => console.log(val))
Websocket�on�/�offline
const socket = Rx.Observable.webSocket('wss://echo.websocket.org') const send = document.querySelector('#send');
socket.multiplex(() => { console.log('connected') return JSON.stringify({msg: 'hi'}); }, () => { console.log('disconnected') return JSON.strinfify({msg:'all'}); }, data => { console.log(`sending: ${data.msg}`); return true; }) .retryWhen(err => { return err .do(val => console.log(`error with ${val}`)) .delayWhen(() => { return window.navigator.onLine ? Rx.Observable.timer(1000) : Rx.Observable.fromEvent(window, 'online'); }); }) .subscribe(result => console.log(`received: ${result.msg}`));
send.onclick = () => socket.next(JSON.stringify({msg: 'wow'}));
109 https://goo.gl/FCS2ae
Websocket�on�/�offline
const socket = Rx.Observable.webSocket('wss://echo.websocket.org') const send = document.querySelector('#send');
socket.multiplex(() => { console.log('connected') return JSON.stringify({msg: 'hi'}); }, () => { console.log('disconnected') return JSON.strinfify({msg:'all'}); }, data => { console.log(`sending: ${data.msg}`); return true; }) .retryWhen(err => { return err .do(val => console.log(`error with ${val}`)) .delayWhen(() => { return window.navigator.onLine ? Rx.Observable.timer(1000) : Rx.Observable.fromEvent(window, 'online'); }); }) .subscribe(result => console.log(`received: ${result.msg}`));
send.onclick = () => socket.next(JSON.stringify({msg: 'wow'}));
110 https://goo.gl/FCS2ae
Websocket�on�/�offline
const socket = Rx.Observable.webSocket('wss://echo.websocket.org') const send = document.querySelector('#send');
socket.multiplex(() => { console.log('connected') return JSON.stringify({msg: 'hi'}); }, () => { console.log('disconnected') return JSON.strinfify({msg:'all'}); }, data => { console.log(`sending: ${data.msg}`); return true; }) .retryWhen(err => { return err .do(val => console.log(`error with ${val}`)) .delayWhen(() => { return window.navigator.onLine ? Rx.Observable.timer(1000) : Rx.Observable.fromEvent(window, 'online'); }); }) .subscribe(result => console.log(`received: ${result.msg}`));
send.onclick = () => socket.next(JSON.stringify({msg: 'wow'}));
111 https://goo.gl/FCS2ae
Dan�Abramov,�@JavaScript�Air�025
“Rx can solve many real world tasks. And once you get used to it,
you can apply it pretty much everywhere.”
112
장점
• 데이터의�변환�/�흐름에만�집중�할�수�있다.�
- 함수가�동기�/�비동기인지는�중요하지�않음�
- Operator�는�마치�파이프�같음.��
- 파이프(Operator)만�잘�연결하면,�물(데이터)은�파이프를�따라�흐른다.�
- 이것이�Reactive�Programming?�
- 불가능하다고�생각했던�것들을�만들�수�있게�되었다.�
• Observable�을�인터페이스의�중심으로�
- Observable�은�거의�모든�비동기�상황을�표현할�수�있음.�
- 쉬워진�새로운�기능�추가�
- 쉬워진�테스트�(mocking�이�수월한�경우에...)
113
단점
• 러닝커브�
- 커브�정도가�아니라�절벽�수준�😱 �
- 코드�+�사고방식까지�바꾸어야�함�
- +200�Operators,�Subject,�Scheduler...�
- 처음에는�무섭지만,�막상�사용하다보면�많이�사용하게�되는�건�7�~�8�개�정도?�
• 디버깅�
- 길고,�복잡한�call�stack�
• RxJS�의�내부�타입�/�구현을�모른다면�거의�쓸모없는�정보들�😥 �
• operator�를�제대로�이해하고�사용한다면�거의�볼�일이�없음�
• RxJS@5�에서는�그나마�조금�줄었음�
- do�operator�를�활용하세요!!�
• 서버�보다는�UI�개발에�더�많은�도움을�줄�수�있다고�생각함�🙏
114
Who�to�follow
• Erik�Meijer�
- creator�of�reactive-extensions�
- 📹�One�hacker�way�
• Matthew�Podwyski�
- initial�creator�of�RxJS�
• Ben�Lesh�
- RxJS@5�main�contributor
115
• André�Staltz�
- RxJS@5�main�contributor�
- creator�of�cycle.js�
• kris�kowal�
- creator�of�Q�
- 📰�general�theory�of�reactivity
What�to�see
• https://github.com/reactivex/rxjs�
•📰�official�RxJS@5�doc�
•📹�💵�egghead�RxJS�series�
- RxJS�Beyond�the�Basics:�Creating�Observables�from�scratch�
- RxJS�Beyond�the�Basics:�Operators�in�Depth�
•📖�learn-rxjs
116