compose async with rxjs

178
Compose Async with RxJS @chitacan, Riiid 1

Upload: kyung-yeol-kim

Post on 12-Apr-2017

2.107 views

Category:

Software


0 download

TRANSCRIPT

Compose�Async�with�RxJS@chitacan, Riiid

1

https://goo.gl/W1YChu

2 JSBin�for�example

RxJS�?Reactive�extensions�library�for�JavaScript

3

시작하기�전에

[1, 2, 3, 4] .map(d => d * 2) .filter(d => d > 4) .forEach(console.log);

4

시작하기�전에

[1, 2, 3, 4] .map(d => d * 2) .filter(d => d > 4) .forEach(console.log);

4

> 6> 8

Event�를�이렇게�처리한다면�어떨까요?

5

시작하기�전에

[..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�는�모두�Collections

11

Event�는�데이터의�모음이고,�Array�처럼�처리할�수�있다.

12

Array�와�Event

• 차이점�

- array�는�각�요소를�동기적�(sync)�으로�조회할�수�있고,�끝이�있다.�

- event�는�각�요소를�비동기적�(async)�으로�조회할�수�있고,�끝이�없다.(하지만�event�listening�을�취소�할�수�있다.)�

• 비동기적으로�생성되는�요소들을�표현하고,�원하는�시점에�취소할�수�있는��타입

13

Observable

14

[ ]time

collections�over�time

15

[ ]time

collections�over�time

15

1....2..3..

pull�vs�push

[1, 2, 3].forEach(console.log);

[1..2..3...].forEach(console.log);

16

pull�vs�push

[1, 2, 3].forEach(console.log);

[1..2..3...].forEach(console.log);

16

> 1> 2> 3

pull�vs�push

[1, 2, 3].forEach(console.log);

[1..2..3...].forEach(console.log);

16

> 1> 2> 3

> 1

pull�vs�push

[1, 2, 3].forEach(console.log);

[1..2..3...].forEach(console.log);

16

> 1> 2> 3

> 1> 2

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

“RxJS 는 Observable 타입을 활용해 Event를 Array 처럼 처리 할 수 있는 라이브러리”

17

­Ben�Lesh,�RxJS�In-Depth�@AngularConnect�2015

“RxJS is LoDash or Underscore for async”

18

RxJS

• Observable�타입�

• Operators�(map,�filter�...)�

• Scheduler

19

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

promise.then(valueFn, errorFn);

Completion

31

Completion

32

observable.subscribe(nextFn, errorFn, completeFn);

Operators

• observable�타입을�변환�/�조합할�수�있음�

• RxJS@5�기준�+200개�

• 종류�

- creation��

- transformation�

- filtering�

- composition�

- error�

- ...

33

creation

• create

• from

• fromEvent

• fromPromise

• interval

34

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

Array�function

41

Array�function

41

RxJS

42

RxJS

42

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

transformation

• map

• mergeMap (flatMap) 🌟

47

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

filtering

• filter

• take

• takeUntil 🌟

55

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

[...0...1...2...3].takeUntil( [.............0])

[...0...1...2.]

58

source

notifier

result

takeUntil

[...0...1...2...3].takeUntil( [.............0])

[...0...1...2.]

58

source

notifier

result

takeUntil

[...0...1...2...3].takeUntil( [.............0])

[...0...1...2.]

58

source

notifier

result

takeUntil

[...0...1...2...3].takeUntil( [.............0])

[...0...1...2.]

58

source

notifier

result

takeUntil

[...0...1...2...3].takeUntil( [.............0])

[...0...1...2.]

58

source

notifier

result

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

composition

• combineLatest 🌟

• zip

• merge

62

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

error

• retry 🌟

• retryWhen 🌟

68

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

89

• 매일�아침에�태스크�큐를�확인해서�실패한�태스크�워커가�있으면,�슬랙을�통해�알림을�받고�싶다.

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); }); };

103

on�/�offline

104

• 에러가�발생했을때�

- online�상태인�경우,�1초�뒤에�재시도�

- offline�상태인�경우,�online�상태가�되면�재시도

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

Q�&�A

117