본문 바로가기
RxJS(Reactive X)

RxJS 에서 피해야 할 코드

by 찬찬2 2024. 6. 21.

1. 수동으로 옵저버블 구독하기 → 직접 프로퍼티에 바인딩하고 async 파이프로 자동으로 구독과 취소하기

 

@Component({
   template: `<h1>{{name}}</h1>`
})
export class MyComponent {
   name = '';
   
   ngOnInit(){
      this.http.get('name').subscribe(result => {
         this.name = result;
      }) 
   }
}

 

위와 같이 ngOnInit 에서 HTTP 요청을 하고 응답 받은 값을 MyComponent 의 name 프로퍼티에 할당하는 방식으로 그동안 작성해왔다. 아래 코드를 보면...

 

@Component({
   template: `<h1>{{name$ | async}}</h1>`
})
export class MyComponent {
   name$ = this.http.get('user');
}

 

ngOnInit 에서 구독하는 메서드를 빼고 바로 프로퍼티에 할당했다. 그리고 컴포넌트 템플릿에서는 async 파이프를 썼다.

 

여기서 중요한 점은 async 파이프가 템플릿이 렌더될때 자동으로 구독을 해주고 컴포넌트의 생명주기가 끝났을때 스스로 구독을 취소도 해주기 때문에 메모리 누수가 발생하지 않는다는 점.

 

위와 같이 단순한 데이터만 받아와서 템플릿에 보여줘야하는 경우, 위와 같이 하는 방법이 좋은 것 같다. 물론 데이터베이스에서 데이터를 받고 비즈니스 로직과 같이 다소 복잡한 코드가 필요한 경우는 직접 구독을 해주는 방식으로 해야겠쥬

 

2. 한 개의 스트림을 여러 곳에서 복수 구독할때

 

const { of, Subject, timer } = rxjs;
const { delay, tap } = rxjs.operators;

const fetchData$ = of('Fetched Data').pipe(
  delay(2000),
  tap(() => console.log('HTTP request made'))
);

fetchData$.subscribe(data => console.log('Subscriber 1:', data));

setTimeout(() => {
  fetchData$.subscribe(data => console.log('Subscriber 2:', data));
}, 1000);

setTimeout(() => {
  fetchData$.subscribe(data => console.log('Subscriber 3:', data));
}, 3000);


// 결과
// "HTTP request made"
// "Subscriber 1:", "Fetched Data"
// "HTTP request made"
// "Subscriber 2:", "Fetched Data"
// "HTTP request made"
// "Subscriber 3:", "Fetched Data"

 

위와 같이 하나의 스트림에서 발행하는 값을 복수의 구독자가 구독중이라면, 구독자의 수 만큼 스트림이 재실행되는 것을 볼 수 있다.

 

보통 이러한 상황들은, API 로 데이터를 받아와서 복수의 장소에서 그 값을 사용할때, form 에서 valueChanges 메서드를 사용할때, 특히 Angular 에서 service.ts 와 같이 복수의 컴포넌트가 하나의 데이터를 바라보는 형태일때 발생한다.

 

const { of, Subject, timer } = rxjs;
const { delay, shareReplay, tap } = rxjs.operators;

const fetchData$ = of('Fetched Data').pipe(
  delay(2000),
  tap(() => console.log('HTTP request made')),
  shareReplay(1)
);

fetchData$.subscribe(data => console.log('Subscriber 1:', data));

setTimeout(() => {
  fetchData$.subscribe(data => console.log('Subscriber 2:', data));
}, 1000);

setTimeout(() => {
  fetchData$.subscribe(data => console.log('Subscriber 3:', data));
}, 3000);


// 결과
// "HTTP request made"
// "Subscriber 1:", "Fetched Data"
// "Subscriber 2:", "Fetched Data"
// "Subscriber 3:", "Fetched Data"

 

shareReplay 연산자는 스트림에서 발행한 "마지막" 값을 구독자에게 전달하는 역할을 한다. Subject 중 ReplaySubject 와 같다고 볼 수 있다.

 

shareReplay 연산자를 사용해 변수에 바로 바인딩하는 위와 같은 방식이 아닌, 내가 발행하고 싶은 순간 발행하고, 마지막 값이 필요한 상황에서 BehaviorSubject, ReplaySubject, AsyncSubject를 사용하면 된다. 또는 아래와 같이 Subject + asObservable + shareReplay 를 써도 된다.

 

const { of, Subject } = rxjs;
const { delay, tap, shareReplay } = rxjs.operators;
const subject$ = new Subject();
const subject = subject$.asObservable().pipe(
   shareReplay(1)
);


// http request
setTimeout(() => {
  subject$.next('Fetched Data 1');
},1000);

subject$.next('Fetched Data 2');

//  컴포넌트 A
subject.subscribe(data => console.log('Subscriber 1:', data));

//  컴포넌트 B
setTimeout(() => {
  subject.subscribe(data => console.log('Subscriber 2:', data));
}, 1000);

//  컴포넌트 C
setTimeout(() => {
  subject.subscribe(data => console.log('Subscriber 3:', data));
}, 3000);


// 결과
// "Subscriber 1:", "Fetched Data"
// "Subscriber 2:", "Fetched Data"
// "Subscriber 3:", "Fetched Data"

 

BehaviorSubejct 의 경우 디폴트값이 필수인데 만약 내가 어느 시점에 구독을 시작했을때 디폴트값으로 한 번 발행되고, 뒤 이어 API에서 받아온 데이터를 출력해 총 두 번 값이 발행된다. null 을 디폴트값으로 사용하고 filter 연산자로 해결할 수 있으나 이렇게 추가적인 코드가 꼭 필요하기 때문에 다른 방법을 더 찾아볼 필요가 있다.

ReplaySubject 가 BehaviorSubject 보다 내가 원하는 목적에 더 적합하기는 하다. 그리고 ReplaySubject(1) 보다 AsyncSubject 가 더 적합하다. Anuglar 의 HttpClient 는 데이터를 받아온 뒤 그 Observable 이 complete 되기 때문에 마지막 값만 담을 수 있는 AsyncSubject 가 적합하다.

 

const { AsyncSubject } = rxjs;
const subject$ = new AsyncSubject();

setTimeout(() => {
	subject$.next('Fetched Data 2');
	subject$.complete();
}, 1000);

subject$.next('Fetched Data 1');

//  컴포넌트 A
subject$.subscribe(data => console.log('Subscriber 1:', data));

//  컴포넌트 B
setTimeout(() => {
	subject$.subscribe(data => console.log('Subscriber 2:', data));
}, 2000);

//  컴포넌트 C
setTimeout(() => {
   subject$.subscribe(data => console.log('Subscriber 3:', data))
}, 3000);


// 결과
// "Subscriber 1:", "Fetched Data 2"
// "Subscriber 2:", "Fetched Data 2"
// "Subscriber 3:", "Fetched Data 2"

 

요약해보자면,

 

조건 : 동일한 값을 모든 구독자에게 내가 원하는 시기에 맞추어 발행해야 할때

 

1. Subject + asObservable + shareReplay

- 단순히 Subject 만 사용하는 경우, 구독 시점에 영향을 받기 때문에  값이 발행된 이후 구독을 시작할 경우 구독 전 발행된 값을 읽지 못한다.

2. BehaviorSubject

- 옵저버블을 구독하지 않은 상태에서도 getValue 로 값에 접근할 수 있음.

- 디폴트 값이 필수

- 구독 시 디폴트 값으로 첫 발행을 시작

3. ReplaySubject

- 마지막으로 발행된 데이터로 부터 n 갯수 만큼 이전에 발행된 데이터를 조회할 수 있다.

4. AsyncSubject

- 스트림이 complete 된 상태일때, 마지막에 발행된 값을 조회할 수 있다.

댓글