본문 바로가기
RxJS(Reactive X)

(기록용) 결합(combine/join)과 관련된 샘플 코드/상황 설명

by 찬찬2 2023. 12. 8.

1. mergeMap, forkJoin

 

- Resolve를 통해 FOUC를 해결한 사례.

 

페이지에서 페이지로 이동할때  FOUC가 항상 발생한다. 페이지 랜딩 후 컴포넌트는 다양한 작업을 순차적으로 처리하는데 이때 초기값에 의한 초기상태가 사용자에게 잠시 비춰지게 된다. 불과 몇 밀리초에 불과하지만 상당히 거슬린다.

프로젝트의 완성도가 낮아보이기도 한다.

이를 해결하기 위해 앵귤러에서 제공하는 Resovlve를 사용했다.

routerLink 또는 router.navigate 등, router로 이동할때 Resolve는 컴포넌트가 로드되기 전 필요한 작업들을 수행하는 것을 도와준다. Resolve를 사용해 미리 필요한 정보들을 받아온뒤 컴포넌트가 실행되기때문에 FOUC 상황을 피할 수 있다.

즉, 컴포넌트가 로드된 뒤 다양한 작업을 순차적으로 진행하는데 Resolve는 그 작업 순서를 최우선순으로 바꿔주는 셈이다.

 

나는 resolve#1, resolve#2로 데이터를 받는다. resolve#2에서 API 요청을하기 위해 resolve#1의 반환값이 필요했고, 코드의 간결함을 위해 컴포넌트에서 두개의 다른 resolve들을 하나로 합치고 싶었다.

 

- mergeMap에서 반환한 Observable과 최초 Observable을 forkJoin을 통해 배열형태로 합쳤다.

forkJoin은 인자로 받은 모든 Observable 들의 발행이 끝날때까지 기다린 뒤 하나로 합쳐준다. 합쳐지는 형태는 배열 또는 객체로 표현할 수 있다.

 

import { of, forkJoin } from 'rxjs';
import { mergeMap, map } from 'rxjs/operators';

this._route.data.pipe(
  mergeMap((resolves: any) => {
    const { selected } = resolves;
    const cropIdx = selected['croppingSeason']['cropIdx'];

    // Assuming this._apiService.req returns an observable
    const breeds$ = this._apiService.req("crop-breed/list", { idx: cropIdx }).pipe(
      map(cropBreed => cropBreed.map(elem => elem['breed']))
    );

    return forkJoin([of(resolves), breeds$]).pipe(
      map(combined => Object.assign({}, ...combined))
    );
  })
).subscribe((resolve: any) => {
  console.log("resolve: ", resolve);
});

 

 

2. combineLatest

 

settingsSubscription: Subscription;

fanTemp$: BehaviorSubject<number> = new BehaviorSubject(null);
heaterTemp$: BehaviorSubject<number> = new BehaviorSubject(null);
coolerTemp$: BehaviorSubject<number> = new BehaviorSubject(null);
  

this.settingsSubscription = combineLatest({
    fan: this.fanTemp$,
    heater: this.heaterTemp$,
    cooler: this.coolerTemp$
}).subscribe();

// 컴포넌트 마운트 시 output: [fan: null, heater: null, cooler: null]

 

- 위 코드에서 만약 옵저버블을 담고있는 fan, heater, cooler의 값이 null일 경우 구독했을 때 값이 있는 것 만을 가져오고 싶다.

 

첫 번째로 든 생각은 각각 fan, heater, cooler 에게 filter 연산자를 pipe에서 붙혀주는 것.

 

settingsSubscription: Subscription;

fanTemp$: BehaviorSubject<number> = new BehaviorSubject(null);
heaterTemp$: BehaviorSubject<number> = new BehaviorSubject(null);
coolerTemp$: BehaviorSubject<number> = new BehaviorSubject(null);
  

this.settingsSubscription = combineLatest({
    fan: this.fanTemp$.pipe(filter(data => data)),
    heater: this.heaterTemp$.pipe(filter(data => data)),
    cooler: this.coolerTemp$.pipe(filter(data => data))
}).subscribe();


this.fanTemp$.next(12);

// 12를 옵저버블에 넣었을때, combineLatest에서는 아무 값도 반환하지 않는다.

 

생각해보면, fanTemp$ 에 12 라는 값이 들어갔다고 하더라도, 나머지 heaterTemp$, coolerTemp$에서는 filter 에 의해 이미 디폴트값으로 선언된 null 때문에 아무 데이터도 내보니지 않을 것이다.

 

combineLatest의 특성상 해당 연산자 안에 있는 fan, heater, cooler는 각각 자신들이 가지고 있는 최소한의 무언가를 내보내야만 하는데 filter 에 의해 차단당한 것이다.

 

settingsSubscription: Subscription;

fanTemp$: BehaviorSubject<number> = new BehaviorSubject(null);
heaterTemp$: BehaviorSubject<number> = new BehaviorSubject(null);
coolerTemp$: BehaviorSubject<number> = new BehaviorSubject(null);
  

this.settingsSubscription = combineLatest({
    fan: this.fanTemp$.pipe(filter(data => data)),
    heater: this.heaterTemp$,
    cooler: this.coolerTemp$
}).subscribe();


this.fanTemp$.next(12);

// output: { fan: 12, heater: null, cooler: null }

 

위와 같이 fan 에만 filter 연산자를 붙혀주고, fan 에게만 값을 넣어줬을 때 combineLatest 는 값을 발행하게된다.

 

그렇다면 null 이 아닌 것만을 어떻게 제외시킬 수 있을까?

 

combineLatest 에서 pipe 로 처리할 수도 있고, 값이 발행된 뒤에 할 수도 있을 것이다.

 

두 번째 방법은 combineLatest 에게 pipe 를 붙혀주는 것이다. 만약 fan, heater, cooler 모두 값이 없다면 굳이 null 값들로 구현될 로직도 없기 때문에 데이터 가공과 관련된 로직은 스트림에서 처리하는 것이 맞을 것 같다고 생각한다. 

 

물론 구독(subscribe) 후 처리할 수도 있으나, RxJS의 개념을 충분히 살리고자 pipe 라는 데이터 가공 공간에서 하는게 좋을 것 같다고 생각했고, 데이터를 가공하는 곳과 비즈니스 로직이 실행되는 곳을 분리하는게 깔끔해 보였다.

 

this.settingsSubscription = combineLatest({
    fan: this.fanTemp$,
    heater: this.heaterTemp$,
    cooler: this.coolerTemp$
}).pipe(
    map((data) => {
        const filtered = Object.entries(data).filter(([, value]) => value !== null);
        const result = Object.fromEntries(filtered);
        return result;
    }),
    filter(data => Object.keys(data).length > 0)
).subscribe((result) => {
    // 여기서 비즈니스 로직코드 작성하기
});

댓글