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

Compose Async with RxJS

  • 1.
    Compose Async withRxJS @chitacan, Riiid 1
  • 2.
  • 3.
    RxJS ? Reactive extensionslibrary for JavaScript 3
  • 4.
    시작하기 전에 [1, 2,3, 4] .map(d => d * 2) .filter(d => d > 4) .forEach(console.log); 4
  • 5.
    시작하기 전에 [1, 2,3, 4] .map(d => d * 2) .filter(d => d > 4) .forEach(console.log); 4 > 6 > 8
  • 6.
    Event 를 이렇게처리한다면 어떨까요? 5
  • 7.
    시작하기 전에 [..Event{}..Event{}...Event{}...] .map(e =>e.clientX * 2) .filter(x => x > 4) .forEach(updateUI); 6
  • 8.
    시작하기 전에 [..Event{}..Event{}...Event{}...] .map(e =>e.clientX * 2) .filter(x => x > 4) .forEach(updateUI); 7
  • 9.
    시작하기 전에 [..Event{}..Event{}...Event{}...] .map(e =>e.clientX * 2) .filter(x => x > 4) .forEach(updateUI); 8
  • 10.
    시작하기 전에 [..Event{}..Event{}...Event{}...] .map(e =>e.clientX * 2) .filter(x => x > 4) .forEach(updateUI); 9
  • 11.
    시작하기 전에 [..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
  • 12.
    Array 와 Event는 모두 Collections 11
  • 13.
    Event 는 데이터의모음이고, Array 처럼 처리할 수 있다. 12
  • 14.
    Array 와 Event •차이점 - array 는 각 요소를 동기적 (sync) 으로 조회할 수 있고, 끝이 있다. - event 는 각 요소를 비동기적 (async) 으로 조회할 수 있고, 끝이 없다.
 (하지만 event listening 을 취소 할 수 있다.) • 비동기적으로 생성되는 요소들을 표현하고, 원하는 시점에 취소할 수 있는 타입 13
  • 15.
  • 16.
  • 17.
    [ ] time collections overtime 15 1....2..3..
  • 18.
    pull vs push [1,2, 3].forEach(console.log); [1..2..3...].forEach(console.log); 16
  • 19.
    pull vs push [1,2, 3].forEach(console.log); [1..2..3...].forEach(console.log); 16 > 1 > 2 > 3
  • 20.
    pull vs push [1,2, 3].forEach(console.log); [1..2..3...].forEach(console.log); 16 > 1 > 2 > 3 > 1
  • 21.
    pull vs push [1,2, 3].forEach(console.log); [1..2..3...].forEach(console.log); 16 > 1 > 2 > 3 > 1 > 2
  • 22.
    pull vs push [1,2, 3].forEach(console.log); [1..2..3...].forEach(console.log); 16 > 1 > 2 > 3 > 1 > 2 > 3 // ...
  • 23.
    pull vs push [1,2, 3].forEach(console.log); [1..2..3...].forEach(console.log); 16 > 1 > 2 > 3 > 1 > 2 > 3 // ... pull
  • 24.
    pull vs push [1,2, 3].forEach(console.log); [1..2..3...].forEach(console.log); 16 > 1 > 2 > 3 > 1 > 2 > 3 // ... pull push
  • 25.
    “RxJS 는 Observable타입을 활용해 Event를 Array 처럼 처리 할 수 있는 라이브러리” 17
  • 26.
    ­Ben Lesh, RxJSIn-Depth @AngularConnect 2015 “RxJS is LoDash or Underscore for async” 18
  • 27.
    RxJS • Observable 타입 •Operators (map, filter ...) • Scheduler 19
  • 28.
    Observable vs Promise PromiseObservable single value multiple value not lazy lazy not cancelable cancelable no completion callback completion callback 20
  • 29.
    single value vsmultiple 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
  • 30.
    single value vsmultiple 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
  • 31.
    Promise: not lazy constp = new Promise(resolve => { setTimeout(() => { resolve(‘run'); }, 1000); console.log('started'); }); 23
  • 32.
    Promise: not lazy constp = new Promise(resolve => { setTimeout(() => { resolve(‘run'); }, 1000); console.log('started'); }); 24 executor
  • 33.
    Promise: not lazy constp = new Promise(resolve => { setTimeout(() => { resolve(‘run'); }, 1000); console.log('started'); }); 25 https://goo.gl/tK39aS
  • 34.
    Promise: not lazy constp = new Promise(resolve => { setTimeout(() => { resolve(‘run'); }, 1000); console.log('started'); }); 25 > "started" https://goo.gl/tK39aS
  • 35.
    const source =Rx.Observable.create(observer => { setTimeout(() => { observer.next('run'); }, 1000); console.log('started'); }); Observable: lazy 26
  • 36.
    const source =Rx.Observable.create(observer => { setTimeout(() => { observer.next('run'); }, 1000); console.log('started'); }); Observable: lazy 26 // no console output
  • 37.
    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
  • 38.
    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
  • 39.
    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
  • 40.
    Promise: not cancelable constp = 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
  • 41.
    Promise: not cancelable constp = 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"
  • 42.
    Promise: not cancelable constp = 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"
  • 43.
    Promise: not cancelable constp = 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" // ??????????
  • 44.
    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
  • 45.
    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
  • 46.
    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
  • 47.
    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
  • 48.
  • 49.
  • 50.
    Operators • observable 타입을변환 / 조합할 수 있음 • RxJS@5 기준 +200개 • 종류 - creation - transformation - filtering - composition - error - ... 33
  • 51.
    creation • create • from •fromEvent • fromPromise • interval 34
  • 52.
    from const source =Rx.Observable.from([1,2,3,4,5]) source.subscribe(val => console.log(val)); > 1 > 2 > 3 > 4 > 5 35
  • 53.
    Array function vsRxJS 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
  • 54.
    Array function vsRxJS 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
  • 55.
    Array function vsRxJS 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
  • 56.
    Array function vsRxJS 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
  • 57.
    Array function vsRxJS 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
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
    fromEvent const source =Rx.Observable.fromEvent(document, ‘click'); source.subscribe(val => console.log(val)); 43
  • 63.
    fromEvent const source =Rx.Observable.fromEvent(document, 'click'); .map(event => event.x) .filter(x => x > 100); source.subscribe(val => console.log(val)); 44
  • 64.
    fromPromise const request =axios.get(URL); const source = Rx.Observable.fromPromise(request); .map(res => res.data); source.subscribe(val => console.log(val)); 45
  • 65.
    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
  • 66.
    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
  • 67.
    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
  • 68.
  • 69.
    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
  • 70.
    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
  • 71.
    mergeMap (flatMap) [..1..2..3].mergeMap(d =>[...d]) [[...1]..[...2]..[...3]] [....1......2......3] 50 source projection result
  • 72.
    mergeMap (flatMap) [..1..2..3].mergeMap(d =>[...d]) [[...1]..[...2]..[...3]] [....1......2......3] 50 source projection result
  • 73.
    mergeMap (flatMap) [..1..2..3].mergeMap(d =>[...d]) [[...1]..[...2]..[...3]] [....1......2......3] 50 source projection result
  • 74.
    mergeMap (flatMap) [..1..2..3].mergeMap(d =>[...d]) [[...1]..[...2]..[...3]] [....1......2......3] 50 source projection result
  • 75.
    mergeMap (flatMap) [..1..2..3].mergeMap(d =>[...d]) [[...1]..[...2]..[...3]] [....1......2......3] 50 source projection result
  • 76.
    mergeMap (flatMap) [..1..2..3].mergeMap(d =>[...d]) [[...1]..[...2]..[...3]] [....1......2......3] 50 source projection result
  • 77.
    mergeMap (flatMap) [..1..2..3].mergeMap(d =>[...d]) [[...1]..[...2]..[...3]] [....1......2......3] 50 source projection result
  • 78.
    mergeMap (flatMap) [..1..2..3].mergeMap(d =>[...d]) [[...1]..[...2]..[...3]] [....1......2......3] 50 source projection result
  • 79.
    mergeMap (flatMap) [..1..2..3].mergeMap(d =>[...d]) [[...1]..[...2]..[...3]] [....1......2......3] 50 source projection result
  • 80.
    mergeMap (flatMap) [..1..2..3].mergeMap(d =>[...d]) [[...1]..[...2]..[...3]] [....1......2......3] 50 source projection result
  • 81.
    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
  • 82.
    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
  • 83.
    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
  • 84.
    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
  • 85.
  • 86.
    filter const source =Rx.Observable.from([1,2,3,4]) .filter(d => d > 3); source.subscribe(val => console.log(val)); > 4 56
  • 87.
    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
  • 88.
    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
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
    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
  • 95.
    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
  • 96.
    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
  • 97.
    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
  • 98.
  • 99.
    combineLatest combineLatest( [...0...1...2...3...4...], // source1 [.......@],// source2 [...........#] // source3 ) [ ...........[2, @, #]...[3, @, #]...[4, @, #]... ] 63
  • 100.
    combineLatest combineLatest( [...0...1...2...3...4...], // source1 [.......@],// source2 [...........#] // source3 ) [ ...........[2, @, #]...[3, @, #]...[4, @, #]... ] 63
  • 101.
    combineLatest combineLatest( [...0...1...2...3...4...], // source1 [.......@],// source2 [...........#] // source3 ) [ ...........[2, @, #]...[3, @, #]...[4, @, #]... ] 63
  • 102.
    combineLatest combineLatest( [...0...1...2...3...4...], // source1 [.......@],// source2 [...........#] // source3 ) [ ...........[2, @, #]...[3, @, #]...[4, @, #]... ] 63 @
  • 103.
    combineLatest combineLatest( [...0...1...2...3...4...], // source1 [.......@],// source2 [...........#] // source3 ) [ ...........[2, @, #]...[3, @, #]...[4, @, #]... ] 63 @ @ #
  • 104.
    combineLatest combineLatest( [...0...1...2...3...4...], // source1 [.......@],// source2 [...........#] // source3 ) [ ...........[2, @, #]...[3, @, #]...[4, @, #]... ] 63 @ @ @ # #
  • 105.
    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
  • 106.
    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
  • 107.
    zip zip( [...0...1...2...3...4...5], // source1 [.....0.....1.....2...]// source2 ) [ ....[0, 0]....[1, 1]....[2, 2]... ] 65
  • 108.
    zip zip( [...0...1...2...3...4...5], // source1 [.....0.....1.....2...]// source2 ) [ ....[0, 0]....[1, 1]....[2, 2]... ] 65
  • 109.
    zip zip( [...0...1...2...3...4...5], // source1 [.....0.....1.....2...]// source2 ) [ ....[0, 0]....[1, 1]....[2, 2]... ] 65
  • 110.
    zip zip( [...0...1...2...3...4...5], // source1 [.....0.....1.....2...]// source2 ) [ ....[0, 0]....[1, 1]....[2, 2]... ] 65
  • 111.
    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
  • 112.
    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
  • 113.
    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
  • 114.
    error • retry 🌟 •retryWhen 🌟 68
  • 115.
    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
  • 116.
    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
  • 117.
    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
  • 118.
    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
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
    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
  • 134.
    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
  • 135.
    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
  • 136.
    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 // ...
  • 137.
    operators • switchMap 🌟 •concatMap 🌟 • publish • share • skip • bufferCount • bufferTime • Throttle • debounce 76 • concat • catch • bindNodeCallback • +others
  • 138.
    Compose async withRxJS • 여러 이벤트를 조합해야 할 때 (Drag & Drop) • 다수의 비동기 함수를 조합하는 상황 (Cache or Db, AWS Lambda) • 재시도가 필요한 경우 (on / offline) 77
  • 139.
    Drag & Drop •mousedown, mousemove, mouseup 3가지 유저 이벤트를 적절 하게 조합해야 함 - mousedown 이벤트가 발생하면 현재 엘리먼트의 초기 위치를 저장 - mousemove 이벤트가 발생하면 초기 위치 + 변한 위치를 계산해 새로운 위치로 엘리먼트를 옮김 - 언제까지? mouseup 이벤트가 발생할 때까지 78
  • 140.
    Drag & Drop consttarget = 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
  • 141.
    Drag & Drop consttarget = 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
  • 142.
    Drag & Drop consttarget = 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
  • 143.
    Drag & Drop consttarget = 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
  • 144.
    Drag & Drop consttarget = 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
  • 145.
    Drag & Drop consttarget = 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
  • 146.
    Cache or DB 85 •cache 또는 DB 에서 데이터를 가져오는 상황 - getFromCache$(), getFromDB$() - 둘 중에 먼저 도착한 데이터만 사용하고 싶다. - 나머지 요청은 취소되어야 함 - getFromOtherServer$() - 도착한 데이터의 값을 사용해 다른 요청 수행
  • 147.
    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
  • 148.
    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
  • 149.
    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
  • 150.
    AWS Lambda 89 • 매일아침에 태스크 큐를 확인해서 실패한 태스크 워커가 있으면, 슬랙을 통해 알림을 받고 싶다.
  • 151.
    AWS Lambda 90 • Taskfunction - getTasks$() - 태스크 큐에 쌓여있는 태스크 정보 가져오기 - checkNotified$() - renderTaskCount$() - 실패한 태스크가 존재하면 vega-renderer function 호출 • vega-renderer function - upload$() - 렌더링된 이미지를 s3 에 저장 - post$() - 저장된 이미지 경로와 함께 슬랙에 알림 전송
  • 152.
    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
  • 153.
    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
  • 154.
    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
  • 155.
    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
  • 156.
    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)); };
  • 157.
    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)); };
  • 158.
    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)); };
  • 159.
    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); }); };
  • 160.
    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); }); };
  • 161.
    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); }); };
  • 162.
    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); }); };
  • 163.
    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); }); };
  • 164.
  • 165.
    on / offline 104 •에러가 발생했을때 - online 상태인 경우, 1초 뒤에 재시도 - offline 상태인 경우, online 상태가 되면 재시도
  • 166.
    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
  • 167.
    on / offline 106https://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))
  • 168.
    on / offline 107https://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))
  • 169.
    on / offline 108https://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))
  • 170.
    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
  • 171.
    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
  • 172.
    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
  • 173.
    ­Dan Abramov, @JavaScriptAir 025 “Rx can solve many real world tasks. And once you get used to it, you can apply it pretty much everywhere.” 112
  • 174.
    장점 • 데이터의 변환/ 흐름에만 집중 할 수 있다. - 함수가 동기 / 비동기인지는 중요하지 않음 - Operator 는 마치 파이프 같음. - 파이프(Operator)만 잘 연결하면, 물(데이터)은 파이프를 따라 흐른다. - 이것이 Reactive Programming? - 불가능하다고 생각했던 것들을 만들 수 있게 되었다. • Observable 을 인터페이스의 중심으로 - Observable 은 거의 모든 비동기 상황을 표현할 수 있음. - 쉬워진 새로운 기능 추가 - 쉬워진 테스트 (mocking 이 수월한 경우에...) 113
  • 175.
    단점 • 러닝커브 - 커브정도가 아니라 절벽 수준 😱 - 코드 + 사고방식까지 바꾸어야 함 - +200 Operators, Subject, Scheduler... - 처음에는 무섭지만, 막상 사용하다보면 많이 사용하게 되는 건 7 ~ 8 개 정도? • 디버깅 - 길고, 복잡한 call stack • RxJS 의 내부 타입 / 구현을 모른다면 거의 쓸모없는 정보들 😥 • operator 를 제대로 이해하고 사용한다면 거의 볼 일이 없음 • RxJS@5 에서는 그나마 조금 줄었음 - do operator 를 활용하세요!! • 서버 보다는 UI 개발에 더 많은 도움을 줄 수 있다고 생각함 🙏 114
  • 176.
    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
  • 177.
    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
  • 178.