본문 바로가기

Web + APP/Angular

Async Pipe 뜯어먹기

반응형
SMALL

안녕하세요. 꼬동입니다.

 

오늘은 Angular에서 Observable Data를 template에서 사용할 때, 함께 쓰는 Async Pipe를 알아볼 예정입니다.

 

사실 이 그림이 다 설명해줍니다.


우선 Observable Data를 사용하기 위해서는 Observable Stream의 subscribe() 메소드를 이용해야 합니다.

 

허나 subscribe()의 경우엔 저희가 unsubscribe()도 해줘야 하고, 코드도 복잡해지고 어휴 지지입니다.

 

Async Pipe는 해당 로직을 이미 Angular에서 제공을 해줘, subscribe() 메소드를 사용하는 것보다, 위의 그림처럼 3가지의 이점을 가질 수 있습니다.

  1. 구독할 필요가 없습니다. Async Pipe가 Observable Stream을 Subscription으로 만들어줘 다뤄주기 때문입니다.
  2. 구독 해지할 필요가 없습니다. 그렇기 때문에 Subscription을 개발자가 관리할 필요가 없어집니다. 메모리 릭이 발생하지도 않겠죠 !?
  3. Change Detection Performance를 향상시켜줄 수 있습니다. 요 경우엔 몇 가지 토핑이 더 추가해야하는데, 아래에서 다뤄보겠습니다.

3번 내용을 좀 더 자세히 알기 위해서 Change Detection Strategies를 좀 알아보도록 합시다.

 

우선 Change Detection이라 함은, 앵귤러는 애플리케이션 데이터 구조의 변화를 추적하기 위해서 Change Detection을 사용합니다. 그렇기 때문에 데이터가 변화하면 UI가 업데이트를 시켜주죠.

 

Change Detection에는 두 가지 Strategies가 있습니다.

 

Default와 OnPush 전략입니다. Change Detection의 옵션이라고 생각하시면 됩니당.

 

아래처럼 등록하면 됩니다.

@Component({
	templateUrl: './product-list.component.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})

Default의 경우엔 정말 Default로 어떠한 변화가 생기더라도 감지를 하려합니다. 뭐 빠져나갈 공간은 없다만은, 효율적이지 않겠죠.

 

OnPush의 경우엔 특정 상황에 대해서 변화를 감지하는데요.

 

@Input properties가 변했을 때, Event가 감지 됐을 때, Observable이 데이터를 뱉을 때 등에 Change Detection을 수행합니다.

 

즉, Change Detection의 performance를 향상 시킬 수 있겠죠 ! Detection Cycle을 그만큼 줄이니까요.

 

그렇기 때문에 Async Pipe를 하고, OnPush 전략을 등록하면 Change Detection의 성능을 올릴 수 있다는 뜻입니다.


사실 여기까지 하고 글을 마무리 하려고 했는데, 너무 이모티콘 가득하고 영양가 없는 네이버 블로그 느낌이 나서, 한 번 Angular 코드를 뜯어봤습니다. 아래와 같더군요.

 

https://github.com/angular/angular/blob/62b5a6cb079e489d91982abe88d644d73feb73f3/packages/common/src/pipes/async_pipe.ts#L140

 

angular/angular

The modern web developer’s platform. Contribute to angular/angular development by creating an account on GitHub.

github.com

 

자 아는데까지 한 번 뜯어봅시다. 코드를 올리긴 했지만 코드를 먼저 다 볼 필요 없이 제가 쓴 글을 위주로 따라가면 더 쉽게 읽으실 수 있으실거에요 !

 

우선 제일 중요한 부분이 요 부분인거 같아요.

@Pipe({name: 'async', pure: false})
export class AsyncPipe implements OnDestroy, PipeTransform {
  private _latestValue: any = null;

  private _subscription: Unsubscribable|Promise<any>|null = null;
  private _obj: Subscribable<any>|Promise<any>|EventEmitter<any>|null = null;
  private _strategy: SubscriptionStrategy = null!;

  constructor(private _ref: ChangeDetectorRef) {}

  ngOnDestroy(): void {
    if (this._subscription) {
      this._dispose();
    }
  }

  // NOTE(@benlesh): Because Observable has deprecated a few call patterns for `subscribe`,
  // TypeScript has a hard time matching Observable to Subscribable, for more information
  // see https://github.com/microsoft/TypeScript/issues/43643

  transform<T>(obj: Observable<T>|Subscribable<T>|Promise<T>): T|null;
  transform<T>(obj: null|undefined): null;
  transform<T>(obj: Observable<T>|Subscribable<T>|Promise<T>|null|undefined): T|null;
  transform<T>(obj: Observable<T>|Subscribable<T>|Promise<T>|null|undefined): T|null {
    if (!this._obj) {
      if (obj) {
        this._subscribe(obj);
      }
      return this._latestValue;
    }

    if (obj !== this._obj) {
      this._dispose();
      return this.transform(obj);
    }

    return this._latestValue;
  }

  private _subscribe(obj: Subscribable<any>|Promise<any>|EventEmitter<any>): void {
    this._obj = obj;
    this._strategy = this._selectStrategy(obj);
    this._subscription = this._strategy.createSubscription(
        obj, (value: Object) => this._updateLatestValue(obj, value));
  }

  private _selectStrategy(obj: Subscribable<any>|Promise<any>|EventEmitter<any>): any {
    if (ɵisPromise(obj)) {
      return _promiseStrategy;
    }

    if (ɵisSubscribable(obj)) {
      return _subscribableStrategy;
    }

    throw invalidPipeArgumentError(AsyncPipe, obj);
  }

  private _dispose(): void {
    this._strategy.dispose(this._subscription!);
    this._latestValue = null;
    this._subscription = null;
    this._obj = null;
  }

  private _updateLatestValue(async: any, value: Object): void {
    if (async === this._obj) {
      this._latestValue = value;
      this._ref.markForCheck();
    }
  }
}

Async Pipe 클래스에 보시다시피 파이프 데코레이터를 쓰고, async로 이름을 지었죠.

@Pipe({name: 'async', pure: false})
export class AsyncPipe implements OnDestroy, PipeTransform {
  ...

Angular 공홈에선 이렇게 말합니다.

어디 보자

pure?: boolean

요 녀석이 true면, pipe는 pure한데요. 그 의미가 뭐냐면 transform() 메소드가 오직 inputs arguments가 변할 때만 실행이 됩니다. pipe는 디폴트로 pure해요.

만약 pipe가 내부적으로 상태(arguments보다 상태에 의존하는 결과)를 가졌다면, pure를 false로 하세요. 이 경우에 pipe는 각 change detection 사이클에서 pipe를 실행합니다. arguments가 바뀌지 않더라도요.

 

와 영어 실력 극혐입니다. 공부 좀 해야할 듯.

 

어쨋든, pipe는 Observable 상태에 따라서 값이 변화해야하니, pure를 true로 하지 않았을까 싶습니다. 아시는 분은 댓글 달아주세요 ~!

 

그 다음으로 주의 깊게 봐야할 곳은 어딜까요.

 

class SubscribableStrategy implements SubscriptionStrategy {
  createSubscription(async: Subscribable<any>, updateLatestValue: any): Unsubscribable {
    return async.subscribe({
      next: updateLatestValue,
      error: (e: any) => {
        throw e;
      }
    });
  }

  dispose(subscription: Unsubscribable): void {
    subscription.unsubscribe();
  }

  onDestroy(subscription: Unsubscribable): void {
    subscription.unsubscribe();
  }
}

class PromiseStrategy implements SubscriptionStrategy {
  createSubscription(async: Promise<any>, updateLatestValue: (v: any) => any): Promise<any> {
    return async.then(updateLatestValue, e => {
      throw e;
    });
  }

  dispose(subscription: Promise<any>): void {}

  onDestroy(subscription: Promise<any>): void {}
}


private _subscribe(obj: Subscribable<any>|Promise<any>|EventEmitter<any>): void {
  this._obj = obj;
  this._strategy = this._selectStrategy(obj);
  this._subscription = this._strategy.createSubscription(
  obj, (value: Object) => this._updateLatestValue(obj, value));
}

private _selectStrategy(obj: Subscribable<any>|Promise<any>|EventEmitter<any>): any {
  if (ɵisPromise(obj)) {
  	return _promiseStrategy;
  }

  if (ɵisSubscribable(obj)) {
  	return _subscribableStrategy;
  }

  throw invalidPipeArgumentError(AsyncPipe, obj);
}

요 부분 아닐까 싶은데요. 우선 위에서 보듯이 pipe는 Observable 뿐만 아니라 Promise도 대응 가능한 녀석이었습니다 !!

 

충격

사실 당연한 수순이 아니었을까 하는게, 비동기 처리를 도와주는 pipe이기에, Promise도 처리를 할 수 있어야 하지 않을까 싶네요.

 

그래서 코드를 보면 Promise의 경우엔 PromiseStrategy class를 사용하고, Observable의 경우엔 SubscriptionStrategy class를 사용하네요.

 

그러고 createSubscription 메소드를 실행하군요 !!! Promise의 경우엔 .then / Observable의 경우엔 .subscribe를 통해서 데이터를 가져옵니다 !!

 

그러고 끝이 아닌 _updateLatestValue를 통해서 값을 업데이트 시키는데요.

private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    this._ref.markForCheck();
  }
}

보시면, changeDetection.OnPush를 했다면, markForCheck()를 통해서 Change Detection이 일어나면 해당 참조 값을 변화 시키는거 같습니다.

 

여기서 이해가 잘 안되긴 하는뎅... 음... markForCheck()는 호출 당시엔 Change Detection을 일으키지 않고, 다음 Change Detection을 캐치해서 해당 부분에 변화를 주는걸로 알고 있는데요.

 

그렇다면 업데이트 된 걸 어떻게 알고, 변화를 주는 걸까요 ? 아는 분 계시면 댓글을 달아주세요 ㅠㅠㅠ

export class AsyncPipe ... {

  ngOnDestroy(): void {
    if (this._subscription) {
      this._dispose();
    }
  }

  private _dispose(): void {
    this._strategy.dispose(this._subscription!);
    this._latestValue = null;
    this._subscription = null;
    this._obj = null;
  }
  
}

그리고 Pipe를 선언한 Component가 없어지는 순간 ngOnDestory() 메소드를 통해서 해당 unsubscribe()를 하는 모습도 볼 수 있습니다. (unsubscribe() 메소드는 SubscribableStrategy class의 dispose() 메소드에서 찾을 수 있습니다)

 

전부 null 값을 부여하면서 Garbage Collector가 휩쓸고 갈 수 있게도 해주군요.

 

위의 코드 때문에 개발자가 굳이 unsubscribe를 할 필요가 없죠. 메모리를 알아서 정리해주니까요 !

얍얍


자 여기까지 Pipe 코드를 뜯어먹어보면서, 왜 subscribe를 할 필요가 없는지, unsubscribe를 할 필요가 없는지와 Change Detection 전략을 어떻게 다루는지를 알 수 있었습니다. (사실 Change Detection은 밍숭맹숭하게 알아봤지만요)

 

재밌네용 !

 

다음엔 다른 코드를 한 번 뜯어 먹어봐야겠습니다.


이상 Async Pipe 뜯어먹기 였습니다. ^_^


20.06.05

다행히 댓글에 많은 분들이 의견을 내어주어서, 종합하고자 글을 추가합니다.

 

종합한 제 생각은 이러합니다.

 

결국 이벤트가 발생하면, 몽키패치된 CD 전략으로 인해 CD가 발생되고, async pipe에서 markForCheck()가 되어있으니, 해당 CD를 캐치하고 해당 컴포넌트가 리렌더링 된다. 까지 이해를 했습니다.

 

이를 설명하듯 _updateLatestValue()에는 markForCheck()가 있죠, 그 안에서 값을 변화했고요. markForCheck() 호출하면서 markViewDirty()를 호출하고 root까지 마크해둔 컴포넌트를 검색하게 됩니다.

 

그 다음 몽키패치된 이벤트로 인한 (꼭 이벤트는 아닐 수 있습니다) CD가 발생되면서 mark된 해당 컴포넌트가 리렌더링이 된다.

 

라고 !! 이해를 했습니다.

 

혹시나 또 틀린 점 다르게 생각한 점이 있다면, 댓글 마구마구 달아주세요 ~!

 

roomy님 / 제리님 감사합니다 ~

반응형
LIST