본문으로 바로가기

1) 기본적인 map, mapTo

xxxMap이 붙은 녀석들은 observable을 flatten하게 만들어주는 역할을 합니다.

mergeMap 부분에서 자세하게 설명해놓았습니다.

 

map 

이건 뭐 배열 사용할 때 많이 사용해보셨죠? 그겁니다.

interval(1000)
  .pipe(take(5))
  .pipe(map(v => v + 100))
  .subscribe({
    next: v => console.log(v),
    complete: () => console.log('done'),
  });

 

pluck을 map으로 대체해보자

Use map and optional chaining: pluck('foo', 'bar') is map(x => x?.foo?.bar). Will be removed in v8.
- https://rxjs-dev.firebaseapp.com/api/operators/pluck

 

일반적으로 pluck은 특정 내용만 솎아내는 녀석인데, 8버전부터는 없어질 예정입니다.

당연히 map에서 특정 값만 내려주면 되기 때문에 pluck이 존재할 이유 자체가 없습니다.

다만, 옵셔널체이닝에 주의합시다. 

const data = [
  { name: 'darren', age: 100 },
  { name: 'archy', age: 120 },
];

// pluck (deprecated)
from(data)
  .pipe(pluck('name'))
  .subscribe({ next: v => console.log(v) });
  
// map으로 pluck 대체
from(data)
  .pipe(map(v => v?.name))
  .subscribe({ next: v => console.log(v) });

 

 

mapTo

단순하게 그냥 어떤 값을 일정한 값으로 변형하는 겁니다.

of(1, 2, 3, 4)
  .pipe(mapTo("what"))
  .subscribe({ next: (x) => console.log(x) }); // what을 4번 출력함

 

 

2) mergeMap, concatMap, switchMap, exhaustMap 친구들

이 녀석들의 핵심은. flatten의 기능과 동시에, 새롭게 추가된 구독들을 어떻게 핸들링할 것인지입니다.

 

mergeMap : 각 구독을 병행하여 처리함

concatMap : 한 구독이 끝날 때 까지 기다렸다가 다른 구독을 곧바로 연달아 합침

switchMap : 새 구독이 들어오면 예전 구독을 취소시킴

exhaustMap : 새 구독을 무시하고 예전 구독을 끝까지 진행시킴

 

 

mergeMap

Projects each source value to an Observable which is merged in the output Observable.

 

그러니까, map의 역할을 하는 동시에 옵저버블의 출력값을 다시 옵저버블의 값으로 세팅합니다.

예전에 이 녀석의 이름이 flatMap 인 이유가 있죠. Flatten inner observablestten inner observables

xxxMap이란 이름이 붙은 오퍼레이터들은 실은 다 이 녀석을 기본적으로 가져가는데, 추가적인 기능이 붙은 것들입니다. 

 

이게 무슨 말인가 싶죠? 코드로 봅시다.

import { mergeMap } from 'rxjs/operators';

// 옵저버블의 출력값을 다시 옵저버블로 사용하려고 하니 내부 콜백 형태로 작성하게 됨. 구림.
of('Hello').subscribe({
  next: v => of(v + ' World').subscribe({ next: v => console.log(v) }),
});

// mergeMap을 통해서 옵저버블의 출력값을 옵저버블로 깔끔하게 재사용 가능
of('Hello')
  .pipe(mergeMap(v => of(v + ' World')))
  .subscribe({ next: v => console.log(v) });

 

그런데 mergeMap은 주의해야 할 점이 있습니다. cleanup을 해줘야 하는 겁니다.

아래 코드의 경우 클릭할 때마다 interval 옵저버블이 생깁니다.

별도의 cleanup이 없으므로 클릭할때마다 옵저버블이 생기므로, 이를 가만히 내버려두고 다른 페이지로 이동하는 등 잊어버리면

메모리 누수가 생길 수도 있습니다.

const click$ = fromEvent(document, "click");
const interval$ = interval(1000);

click$
  .pipe(mergeMap(() => interval$))
  .subscribe({ next: (v) => console.log(v) });

 

 

 

concatMap

Projects each source value to an Observable which is merged in the output Observable, in a serialized fashion waiting for each one to complete before merging the next.

 

이 녀석의 핵심은 '순서가 유지된다'는 겁니다.

그러니까, 다른 구독이 complete 될 때까지 기다린다는 겁니다. 구독과 구독을 합치는(concat) 겁니다. 

 

여튼 아래 코드는 클릭할 때마다 1초 간격으로 0부터 3까지 출력합니다.

2번 누르면, 일단 첫 구독 (0~3 출력)이 끝날 때까지 기다렸다가, 끝나면 다음 구독 0~3을 출력합니다.

fromEvent(document, 'click')
  .pipe(concatMap(ev => interval(1000).pipe(take(4))))
  .subscribe(x => console.log(x));

 

 

 

switchMap : 서버에 요청 보낼 때 좋습니다.

Projects each source value to an Observable which is merged in the output Observable, emitting values only from the most recently projected Observable.

 

이거 잘 설명하신 다른 분의 글을 좀 가져와보겠습니다.

구독중이던 Observable이 끝나기 전에 새로운 Observable을 구독하게 되면 이전에 구독중이던 Observable 의 구독을 취소하고 다음 Observable 구독을 시작합니다. 서버에 요청을 했다가 취소하고 다른 요청을 보내야 하는 상황처럼 진행중인 작업을 취소하는 경우에 적합하다고 합니다.
- 출처 : boxfoxs.tistory.com/413

 

이해는 되는데 실제로 동작하는 코드를 작성해보도록합시다. 

 

아래 코드를 봅시다. btn을 누르면 1초 간격으로 0부터 정수가 출력되기 시작합니다.

그런데 이 녀석을 시간 차를 두고 여러번 누르면 어떻게 될까요? 전의 옵저버블의 구독이 취소되지 않아서 문제가 됩니다.

const btn = $('.btn');
fromEvent(btn, 'click').subscribe({
  next: e => interval(1000).subscribe({ next: v => console.log(v) }),
});


아래 출력물을 보시면, 4를 출력한 시점에 또 버튼을 눌러서 또 다른 구독이 시작된 것을 확인할 수 있습니다.

이런 문제는 switchMap을 통해서 쉽게 해결할 수 있습니다.

 

switchMap을 이용하여, 재클릭이 되었을 때 전의 구독은 취소하고 새로운 구독을 시작한 것을 확인할 수 있습니다.

fromEvent(btn, 'click')
  .pipe(switchMap(e => interval(1000)))
  .subscribe({ next: v => console.log(v) });

 

3까지 갔을 때 다시 누르면, 4로 진행되어야 할 구독이 취소되고, 0부터 재시작됨을 확인할 수 있습니다.

 

검색창을 구현할 때 쓰면 좋습니다. 이미 요청을 보낸 와중에 값이 바뀌었다면, 전의 값의 응답을 굳이 받을 필요는 없으니까요.

 

github 유저를 찾아오는 걸 구현해보았습니다.

const search$ = fromEvent(inputDom, "keyup");

search$
  .pipe(
    debounceTime(500),
    map((e) => e.target.value),
    distinctUntilChanged(),
    switchMap((term) => ajax.getJSON(`https://api.github.com/users/${term}`))
  )
  .subscribe({ next: (x) => console.log(x) });

 

exhaustMap : 전의 구독을 유지하고, 새 구독은 무시함.

 

어... 너무 간단해서 코드는 생략하겠습니다. 대신 지금까지 살펴본 Map 친구들을 최종적으로 정리해볼까요?

const click$ = fromEvent(document, "click");
const interval$ = interval(1000).pipe(take(4));

// mergeMap : 다시 클릭하면 평행하듯 그냥 구독이 새로 추가됨
click$
  .pipe(mergeMap(() => interval$))
  .subscribe({ next: (x) => console.log(x) });

// concatMap : 다시 클릭하면 전의 작동이 끝날 때까지 기다렸다가 새 구독이 시작됨
click$
  .pipe(concatMap(() => interval$))
  .subscribe({ next: (x) => console.log(x) });

// switchMap : 다시 클릭하면 전의 구독은 끊고 새 걸로 switch 됨
click$
  .pipe(switchMap(() => interval$))
  .subscribe({ next: (x) => console.log(x) });
  
// exhaustMap : 다시 클릭하면 전의 구독을 유지하고 새 구독은 무시함.
click$
  .pipe(exhaustMap(() => interval$))
  .subscribe({ next: (x) => console.log(x) });

 

2) scan

reduce랑 똑같다. 다른 점은, cur가 돌 때마다 출력한다는 점입니다.

생각보다 매우 자주 사용되므로 익숙해집시다.

of(1, 2, 3, 4)
  .pipe(scan((acc, cur) => acc + cur, 0))
  .subscribe(console.log);

 

 

 

darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체