FE
[RxJS] 중첩 Observable을 처리하는 3가지 방법 - mergeMap(), concatMap(), switchMap()
개발공주
2022. 2. 28. 10:38
728x90
이전까지는 여러 스트림의 출력을 동시에 하나로 결합하는 방법 살펴봄.
이번 내용에서는 옵저버블 자체에서 다른 옵저버블을 방출하는 “중첩 옵저버블" 처리하는 방법 살펴볼 예정.
1. Intro
(1) 중첩 옵저버블이란?
const search = Rx.Observable.fromEvent(inputText, 'keyup')
//...
.map(query => sendRequest(testData, query)) // sendRequest는 비동기작업하는 옵저버블
// ...
.subscribe(console.log)
- 사용자가 입력한 키워드를 포함하는 스트림을, 키워드에 대한 검색결과의 배열로 변환함.
- 근데 그 sendRequest 옵저버블 자체가 옵저버블 객체를 반환하는 상황.
// sendRequest()가 또다른 옵저버블 객체를 반환하는 경우 생성되는 값의 타입
Observable<Observable<Array>>
- 구독자는 중첩 옵저버블 값의 계층에 반응하여 Observable<Array>를 직접 처리해선 안됨.
- 이 중첩 옵저버블을 단일 계층으로 평탄화해야 함
(2) 데이터 평탄화란?
다차원 배열의 수준을 줄여야 하는 배열에서 개념이 비롯됨.
[[0, 1],[2, 3],[4, 5]].flatten(); // [0, 1, 2, 3, 4, 5]
[[0, 1],[2, 3],[4, 5]].reduce((a, b) => a.concat(b), []) // [0, 1, 2, 3, 4, 5]
- 중첩 옵저버블에서 값을 추출하고, 중첩 구조를 통합하여 사용자가 한 수준만 보게 함.
- flatten 하고 나서 결과 배열이 일차원이면 작업이 훨씬 쉬워짐.
2. mergeMap()
함수를 반환하는 옵저버블을 소스 옵저버블에 매핑하고 + 출력 옵저버블을 평탄화해서 반환
활용예시: 검색어 입력 후 결과 반환.
const search$ = Rx.Observable.fromEvent(searchBox, 'keyup')
.pluck('target','value')
.debounceTime(500)
.filter(notEmpty)
.do(term => console.log(`Searching with term ${term}`))
.map(query => URL + query)
.mergeMap(query => Rx.Observable.ajax(query) // 옵저버블 매핑 + 소스 옵저버블로 평탄화
.pluck('response', 'query', 'search')
.defaultIfEmpty([]))
.map(R.map(R.prop('title'))) // 응답결과 배열의 모든 제목속성 추출
.do(arr => count.innerHTML = `${arr.length} results`)
.subscribe(arr => {
clearResults(results);
appendResults(arr, results);
});
- fromEvent, pluck, map - 검색어를 입력 후 keyup 이벤트가 일어나면, 해당 검색어로 URL이 만들어짐
- mergeMap - 서비스 조회하는 옵저버블을 소스 옵저버블에 매핑 후 평탄화하고 있음
- map, subscribe - ajax로 서비스 조회한 결과를 구독함
- 비동기 호출 간의 의존성에 대한 걱정 없이, 키 입력이 검색 결과에 직접 매핑된 것처럼 키 입력에 관해 추론할 수 있음.
3. switchMap()
mergeMap과 비슷하지만, 최근에 매핑된 옵저버블 값만 표시함. (인터벌 2초라 하면 2초내에 응답 안오면 걍 다음걸로 기다림.)
활용예시: 주식시세 데이터 받아와서 표시하기
const requestQuote$ = (symbols, fields) =>
Rx.Observable
.fromPromise(ajax(makeQuotesUrl(symbols,fields)))
.pluck('quoteResponse','result');
const twoSecond$ = Rx.Observable.interval(2000);
const fetchDataInterval$ = symbol =>
twoSecond$
.switchMap(() => requestQuote$([symbol],['currency']))
.map(extract);
fetchDataInterval$('FB')
.subscribe(logResult);
- requestQuote - 주식 심볼 이름으로 필요한 주식 시세 값을 가져오는 옵저버블
- twoSecond - 2초 인터벌 옵저버블
- fetchDataInterval - 키워드를 가지고, 2초 인터벌 옵저버블에 검색 결과를 요청하는 옵저버블을 매핑함.
(1) 모든 이벤트 마다 DOM을 불필요하게 갱신하고 있다면?
Rx.Observable.of("a", "b", "c", "d", "d", "e", "e", "e")
.distinctUntilChanged()
.subscribe(console.log); // a b c d e
const fetchDataInterval$ = (symbol) =>
twoSecond$
.switchMap(() => requestQuote$([symbol], ["currency"]))
.distinctUntilChanged(([symbol, price]) => price);
- 주가를 기준으로 비교를 수행하므로, 주가가 변경될 때만 DOM 갱신할 수 있음.
4. concatMap()
각 옵저버블은 이전 옵저버블이 완료될 때까지 대기함.
활용예시: 위젯 드래그 앤 드롭
const panel = document.querySelector('#dragTarget');
const mouseDown$ = Rx.Observable.fromEvent(panel, 'mousedown');
const mouseUp$ = Rx.Observable.fromEvent(document, 'mouseup');
const mouseMove$ = Rx.Observable.fromEvent(document, 'mousemove');
const drag$ = mouseDown$.concatMap(() => mouseMove$.takeUntil(mouseUp$));
drag$.forEach(event => {
panel.style.left = event.clientX + 'px';
panel.style.top = event.clientY + 'px';
});
- panel - 드래그하는 타깃
- mouseDown, mouseUp, mouseMove - 각 이벤트의 옵저버블
- drag - 마우스 이벤트를 방출하는 일련의 스트림.
- takeUntil - mouseUp 이벤트가 발생할 때까지, mouseDown과 연결된 이전 모든 mouseMove 이벤트를 가져옴.
- 세 가지 마우스 이벤트 결합.
- 순서 유지 + 중첩 옵저버블 시퀀스 평탄화.
- takeUntil 사용하면, mouseUp이 값을 방출하는 즉히 mouseMove가 취소된다.
(1) concatMap() vs concatAll()
// 둘이 작동 방식이 동일함
const drag$ = mouseDown$.concatMap(() => mouseMove$.takeUntil(mouseUp$));
const drag$ = mouseDown$.map(() => mouseMove$.takeUntil(mouseUp$)).concatAll();
- concatMap()은 실제로는 map()...concatAll() 이다.
(2) map() + concat() ≠ concatMap()
const drag$ = mouseDown$.map(() => mouseMove$.takeUntil(mouseUp$)).concat();
- concat만으로는 중첩 옵저버블을 평탄화할 수 없음. 여러 옵저버블을 가져와서 순서대로 연결만 할 뿐, 고차 옵저버블끼리 작동하도록 설계되지 않음.
728x90