Skip to content

2. 컴포넌트 라이프싸이클


1. 라이프싸이클 후킹 함수

컴포넌트 인스턴스는 Angular가 컴포넌트 클래스의 인스턴스를 생성한 시점부터 미리 정의된 라이프싸이클을 따라 동작하며 라이프싸이클 단계에 따라 화면에 렌더링되고 자식 컴포넌트를 화면에 추가한다. 그리고 컴포넌트가 동작하는 동안 프로퍼티로 바인딩된 데이터가 변경되었는지 감지하며, 값이 변경되면 화면과 컴포넌트 인스턴스에 있는 데이터를 갱신하기도 한다. 라이프싸이클은 Angular가 컴포넌트 인스턴스를 종료하고 DOM에서 템플릿을 제거할 때까지 이어진다. 그리고 디렉티브도 컴포넌트와 비슷하게 Angular가 인스턴스를 생성하고 갱신하며 종료하는 라이프싸이클을 따른다.


애플리케이션에서 라이플싸이클 후킹 메서드(lifecycle hook method)를 사용하면 컴포넌트나 디렉티브가 동작하는 라이프싸이클에 개입할 수 있다. 그래서 인스턴스가 생성되는 시점, 데이터 변화가 감지되는 시점, 데이터 변화가 감지된 이후 시점, 인스턴스가 종료되는 시점에 원하는 동작을 할 수 있다.


2. 사전지식

라이프싸이클에 대해 알아보기 전에 다음 내용을 먼저 이해하는 것이 좋다:



3. 라이프싸이클 이벤트에 반응하기

Angular core 라이브러리의 라이프싸이클 훅 인터페이스에 정의된 메서드를 컴포넌트나 디렉티브 클래스에 구현하면 해당 라이프싸이클에 반응할 수 있다. 그래서 Angular가 컴포넌트나 디렉티브 인스턴스를 초기화하고, 갱신하며, 종료하는 시점에 원하는 동작을 실행할 수 있다.


각 인터페이스에는 라이프싸이클 후킹 메서드가 하나씩 정의되어 있으며, 이 메서드의 이름은 인터페이스 이름에 ng 접두사를 붙인 형태이다. OnInit 인터페이스에는 ngOnInit() 메서드가 정의되어 있는 식이다. ngOnInit() 메서드를 컴포넌트나 디렉티브 클래스에 정의하면 Angular가 입력 프로퍼티를 검사한 직후에 실행되기 때문에 인스턴스 초기화 로직을 작성할 수 있다.


peek-a-boo.directive.ts (일부)
@Directive({ selector: "[appPeekABoo]" })
export class PeekABooDirective implements OnInit {
  constructor(private logger: LoggerService) {}

  // implement OnInit's `ngOnInit` method
  ngOnInit() {
    this.logIt(`OnInit`);
  }

  logIt(msg: string) {
    this.logger.log(`#${nextId++} ${msg}`);
  }
}


라이프싸이클 후킹 함수를 전부 구현할 필요는 없다. 필요한 것만 구현해서 사용하면 된다.


1) 라이프싸이클 이벤트 순서

애플리케이션이 컴포넌트나 디렉티브 클래스의 생성자를 실행하면서 인스턴스를 초기화하고 나면 정해진 시점에 라이프싸이클 메서드가 실행된다.


후킹 메서드 용도 실행 시점
ngOnChanges() 바인딩된 입력 프로퍼티 값이 처음 설정되거나 변경될 때 실행된다. 이 메서드는 프로퍼티의 이전 값과 현재 값을 표현하는 SimpleChanges 객체를 인자로 받는다. 이 메서드는 매우 자주 실행된다. 그래서 이메서드에 복잡한 로직을 작성하면 애플리케이션 성능이 크게 저하될 수 있다. (컴포넌트에 입력 프로퍼티가 바인딩된 후) ngOnInit()이 실행되기 전에 한 번 실행되며 입력 프로퍼티로 바인딩된 값이 변경될 때마다 실행된다. 컴포넌트에 입력 프로퍼티가 없거나, 선언하고 사용하지 않는다면 ngOnChanges()가 실행되지 않는다.
ngOnInit() 디렉티브나 컴포넌트에 바인딩된 입력 프로퍼티 값이 처음 할당된 후에 실행된다. ngOnChanges()가 처음 실행된 후에 한 번 실행된다. ngOnInit() 템플릿에 입력 프로퍼티가 연결되지 않아 ngOnChanges()가 실행되지 않아도 실행된다.
ngDoCheck() Angular가 검출하지 못한 변화에 반응하거나, Angular가 변화를 감지하지 못하게 할 때 사용한다. ngOnInit()이 실행된 직후에 한 번 실행되며, 변화 감지 싸이클이 실행되면서 ngOnChanges()가 실행된 이후에 매번 실행된다.
ngAfterContentInit() Angular가 외부 컨텐츠를 컴포넌트나 디렉티브 뷰에 프로젝션한 이후에 실행된다. ngDoCheck()가 처음 실행된 후 한 번 실행된다.
ngAfterContentChecked() Angular가 디렉티브나 컴포넌트에 프로젝션된 컨텐츠를 검사하고 난 후에 실행된다. ngAfterContentInit()이 실행된 후, ngDoCheck()가 실행된 이후마다 실행된다.
ngAfterViewInit() Angular가 컴포넌트가 디렉티브 화면과 자식 컴포넌트 화면을 초기화한 후에 실행된다. ngAfterContentChecked()가 처음 실행된 후에 한 번 실행된다.
ngAfterViewChecked() Angular가 컴포넌트나 디렉티브 화면과 자식 화면을 검사한 후에 실행된다. ngAfterViewInit()가 실행된 후, ngAfterContentChecked()가 실행된 이후마다 실행된다.
ngOnDestroy() Angular가 디렉티브나 컴포넌트 인스턴스를 종료하기 전에 실행된다. 이 메서드는 옵저버블을 구독 해지하거나 이벤트 핸들러를 제거하는 등 메모리 누수를 방지하는 로직을 작성하는 용도로 사용한다. Angular가 디렉티브나 컴포넌트 인스턴스를 종료하기 직전에 실행된다.


2) 라이프싸이클 활용 예제

라이브 예제 링크 / 다운로드 링크에서 최상위 컴포넌트 AppComponent 안에 있는 컴포넌트들을 보면 라이프싸이클 후킹 함수를 어떻게 활용하는지 확인할 수 있다. 이 예제 프로젝트에서 AppComponent는 모든 자식 컴포넌트의 테스트 베드로 동작하며 자식 컴포넌트는 개별 라이프싸이클 후킹 메서드를 다룬다.


예제 프로젝트에서 어떤 내용을 다루는지 간단하게 살펴보자. 개별 항목에 대해서는 이 문서를 진행하면서 계속 알아보자.


컴포넌트 설명
Peek-a-boo 전체 라이프싸이클 후킹 메서드가 어떻게 동작하는지 보여준다. 개별 후킹 메서드가 실행되는 것을 화면에서 확인할 수 있다.
Spy 커스텀 디렉티브로 라이프싸이클 후킹 메서드를 활용하는 방법에 대해 다룬다. SpyDirective에는 ngOnInit()ngOnDestroy() 후킹 메서드가 정의되어 있으며, 이 디렉티브를 사용해서 엘리먼트가 화면에 추가되고 제거되는 것을 확인할 수 있다.
OnChanges 컴포넌트의 입력 프로퍼티 값이 변경될 때 ngOnChanges()가 어떻게 실행되는지에 대해 다룬다. 후킹 메서드에 전달되는 changes 객체를 어떻게 활용할 수 있는지도 확인해 본다.
DoCheck noDoCheck() 메서드로 커스텀 변화감지 로직을 구현하는 방법에 대해 다룬다. ngDoCheck() 메서드가 얼마나 자주 실행되는지 화면에 표시되는 로그를 확인해 본다.
AfterView Angular에서 의미하는 화면(view)이 무엇인지에 대해 다룬다. ngAfterViewInit() 메서드와 ngAfterViewChecked() 메서드에 대해 다룬다.
AfterContent 외부 컨텐츠를 컴포넌트에 프로젝션하는 것에 대해 다룬다. 컴포넌트 자식 뷰와 프로젝션된 컨텐츠를 구분하는 방법도 설명하며, ngAfterContentInit() 메서드와 ngAfterContentChecked() 메서드에 대해 다룬다.
Counter 컴포넌트와 디렉티브를 함께 사용할 때 라이프싸이클 후킹 함수를 각각 어떻게 적용하는지 알아본다.


4. 컴포넌트, 디렉티브 초기화하기

ngOnInit() 메서드를 활용하면 다음과 같은 초기화 작업을 실행할 수 있다.


  • 일반적으로 컴포넌트는 가볍고 간단하게 생성할 수 있어야 한다. 그래서 초기화 로직이 복잡하다면 이 로직은 생성자에 작성하지 않는 것이 좋다. 외부에서 데이터를 받아와야 하는 로직도 마찬가지이다. 이런 로직이 생성자에 있으면 테스트 환경에서 컴포넌트를 생성하거나 화면에 컴포넌트가 표시되기 전에도 외부로 HTTP 요청이 발생할 수 있다. 데이터를 외부에서 받아오고 컴포넌트를 초기화하는 로직은 ngOnInit()에 작성하는 것이 좋다. 히어로들의 여행 튜토리얼에서도 이 내용을 확인할 수 있다.


Note


  • Angular가 입력 프로퍼티 값을 할당한 후에 컴포넌트 초기화 작업을 할 수 있다. 생성자에서는 지역 변수를 할당하는 것 이외의 로직은 작성하지 않는 것이 좋다. 디렉티브에 바인딩되는 입력 프로퍼티 값은 생성자가 실행된 후에 할당된다는 것을 명심한다. 이 프로퍼티 값에 따라 디렉티브를 초기화해야 한다면 생성자가 아니라 ngOnInit()에서 해야 한다.


Note

  • 입력 프로퍼티에 데이터가 전달되는 것을 가장 먼저 확인할 수 있는 메서드는 ngOnChanges() 메서드이다.
  • 하지만 ngOnChanges()는 ngOnInit() 이전뿐 아니라 그 이후에도 여러 번 실행된다.
  • ngOnInit()은 한번만 실행되기 때문에 초기화 로직은 이 메서드에 작성하는 것이 좋다.


5. 인스턴스 종료하기

Angular가 디렉티브나 컴포넌트를 종료하기 전에 실행해야 하는 로직이 있다면 이 로직은 ngOnDestroy()에 작성한다.


그래서 자동으로 메모리 정리되지 않는 항목이 있다면 이 메서드에서 정리하면 된다. 이런 용도로 활용할 수 있다.


  • 옵저버블이나 DOM 이벤트 구독 해지
  • 인터벌 타이머 중단
  • 디렉티브가 전역이나 애플리케이션 서비스에 등록한 콜백 정리


ngOnDestroy() 메서드는 컴포넌트나 디렉티브가 종료된다는 것을 애플리케이션 다른 영역으로 전달하는 용도로도 사용할 수 있다.


6. 활용 예제

라이프싸이클 이벤트가 얼마나 자주 발생하는지, 어떻게 활용할 수 있는지 예제를 보며 확인해 보자.


1) 라이프싸이클 이벤트 발생 순서, 빈도

Angular가 라이프싸이클 후킹 메서드를 어떤 순서로 실행하는지 확인하려면 PeekABooComponent를 확인하면 된다.


물론 실제 앱에서 이 컴포넌트처럼 모든 라이프싸이클 메서드를 정의할 일은 거의 없으며, 데모를 위해 구현한 것이다.


이 컴포넌트에서 Create... 버튼을 누른 후에 Destroy... 버튼을 누르면 다음과 같은 로그가 화면에 표시된다.


001


라이프싸이클 후킹 메서드가 실행되는 순서는 이렇다: OnChanges, OnInit, DoCheck (3번), AfterContentInit, AfterContentChecked (3번), AfterViewInit, AfterViewChecked (3번), OnDestroy.


Note

  • 입력 프로퍼티(예제에서는 name 프로퍼티)의 값은 생성자가 실행되는 시점에 할당되지 않았다는 것에 주의한다.
  • 그래서 입력 프로퍼티를 활용해서 컴포넌트를 초기화하는 로직은 onInit() 메서드 안에 작성해야 한다.


그리고 Update Hero 버튼을 누르면 OnChanges 로그와 함께 DoCheck, AfterContentChecked, AfterViewChecked 로그도 함께 출력된다. 이 인터페이스로 구현하는 라이프싸이클 후킹 메서드는 자주 실행된다. 이 메서드에서는 간단한 로직만 작성하는 것이 좋다.


2) DOM을 추적하는 디렉티브

Spy 예제를 보면 디렉티브에 라이프싸이클 메서드를 정의해서 컴포넌트처럼 사용하는 방법을 확인할 수 있다. SpyDirective에는 엘리먼트가 화면에 표시되는 시점을 확인하기 위해 ngOnInit(), ngOnDestroy() 메서드를 구현했다.


그리고 부모 컴포넌트 SpyComponent 템플릿의 ngFor 안에서 반복하는 <div>SpyDirective를 적용했다.


이번 예제에는 디렉티브를 초기화하거나 정리하는 로직이 없다. 이 디렉티브는 단순하게 엘리먼트가 화면에 나타나고 사라지는 것을 추적하는 용도로만 활용한다.


스파이 디렉티브는 이렇게 개발자가 직접 조작할 수 없는 DOM 객체를 추적하는 용도로 활용할 수 있다. 그래서 네이티브 <div> 엘리먼트의 구현 코드나 서드 파티 컴포넌트를 직접 수정하지 않아도 된다.


이 디렉티브는 ngOnInit(), ngOnDestroy() 후킹 메서드가 실행될 때마다 LoggerService를 사용해서 로그 메시지를 출력한다.


src/app/spy.directive.ts
let nextId = 1;

// 엘리먼트에 스파이 디렉티브를 자유롭게 적용합니다.
// 사용방법: <div appSpy>...</div>
@Directive({ selector: "[appSpy]" })
export class SpyDirective implements OnInit, OnDestroy {
  private id = nextId++;

  constructor(private logger: LoggerService) {}

  ngOnInit() {
    this.logger.log(`Spy #${this.id} onInit`);
  }

  ngOnDestroy() {
    this.logger.log(`Spy #${this.id} onDestroy`);
  }
}


이 스파이 디렉티브는 네이티브 엘리먼트나 컴포넌트 엘리먼트에도 자유롭게 적용할 수 있으며, 동시에 여러 엘리먼트에 적용할 수도 있다. 이렇게 사용하면 된다:


src/app/spy.component.html
<p *ngFor="let hero of heroes" appSpy>{{hero}}</p>


개별 스파이는 히어로 <div>가 화면에 표시되거나 화면에서 제거될 때마다 생성되고 종료되며, 이 내용은 로그로 기록된다. 히어로 <div>를 하나 추가해 보자. ngOnInit()가 실행된 것을 로그로 확인할 수 있다.


Reset 버튼을 눌러서 heroes 목록을 초기화 해보자. 그러면 Angular가 히어로와 관련된 <div> 엘리먼트를 모두 DOM에서 제거하면서 스파이 디렉티브도 종료된다. 이 때 ngOnDestroy() 메서드에 정의한 로그가 화면에 표시된다.


3) 컴포넌트와 디렉티브에서 동시에 후킹하기

이 예제에서 CounterComponentngOnChanges() 메서드를 사용해서 부모 컴포넌트에서 전달되는 counter 프로퍼티 값이 변경될 때마다 로그를 출력한다.


코드를 보면 SpyDirectiveCounterComponent에도 적용된 것을 확인할 수 있으며, 이 경우에도 SpyDirective가 출력하는 로그로 CounterComponent가 생성되고 종료되는 시점을 확인할 수 있다.


7. 변화 감지 후킹 함수 활용하기

컴포넌트나 디렉티브에 바인딩된 입력 프로퍼티 값이 변경된 것을 감지하면 Angular가 ngOnChanges() 메서드를 실행한다. ngOnChanges() 함수에서 값이 어떻게 변경되었는지 확인하려면 다음과 같이 작성하면 된다.


on-changes.component.ts (일부)
ngOnChanges(changes: SimpleChanges) {
  for (const propName in changes) {
    const chng = changes[propName];
    const cur  = JSON.stringify(chng.currentValue);
    const prev = JSON.stringify(chng.previousValue);
    this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
  }
}


ngOnChanges() 메서드는 SimpleChange 객체를 인자로 받는데, 이 객체에는 개별 입력 프로퍼티가 객체의 프로퍼티 이름으로 선언되어 이전값과 현재값을 전달한다. 그래서 객체 프로퍼티를 순회하면 어떤 값이 변경되었는지 확인할 수 있다.


예제로 다루는 OnChangesComponent에는 입력 프로퍼티가 2개 있다: hero, power.


src/app/on-changes.component.ts
@Input() hero!: Hero;
@Input() power = '';


그리고 이 입력 프로퍼티들은 OnChangesParentComponent에서 이렇게 바인딩된다.


src/app/on-changes-parent.component.html
<on-changes [hero]="hero" [power]="power"></on-changes>


사용자가 입력 프로퍼티 값을 변경할 때 어떻게 동작하는지 확인해 보자.


002


power 프로퍼티의 값이 변경될 때마다 로그가 출력된다. 하지만 hero.name 프로퍼티가 변경된 것은 감지하지 못하며 ngOnChanges() 메서드도 실행되지 않는 것에 주의한다. 왜냐하면 Angular는 기본 상태에서 입력 프로퍼티 객체 자체가 변경된 것만 감지하기 때문이다. 이 경우에 입력 프로퍼티는 hero이며 hero 값은 히어로 객체 참조이다. 그래소 hero 안에 있는 name 프로퍼티 값이 변경되는 것은 감지하지 못한다.


1) 화면 변경사항 감지하기

변화 감지 싸이클이 실행되는 동안 Angular가 뷰 계층을 순회하면서 자식 뷰에서 발생한 변화가 부모 뷰에 영향을 미치지 않아야 한다. 자식 뷰에서 부모 뷰에 영향을 주면 단방향 데이터 흐름을 어기게 되기 때문에 예상한대로 렌더링되지 않을 수 있다.


그래서 자식 뷰에서 부모 뷰로 전달되는 변화를 만들어 내려면 이 변화를 반영하는 변화 감지 싸이클을 새로 발생시켜야 한다. 이 과정을 어떻게 처리하는지 알아보자.


AfterView 예제는 컴포넌트 자식 뷰를 생성한 후에 Angular가 실행하는 AfterViewInit(), AfterViewChecked() 후킹 메서드에 대해 다룬다.


자식 뷰는 히어로의 이름을 <input> 엘리먼트에 표시한다:


ChildComponent
@Component({
  selector: "app-child-view",
  template: `
    <label for="hero-name">Hero name: </label>
    <input type="text" id="hero-name" [(ngModel)]="hero" />
  `,
})
export class ChildViewComponent {
  hero = "Magneta";
}


그리고 AfterViewComponent는 템플릿 안에 이 자식 뷰를 표시한다:


AfterViewComponent (템플릿)
template: `
  <div>child view begins</div>
    <app-child-view></app-child-view>
  <div>child view ends</div>
`;


아래 코드는 자식 뷰 안에서 발생한 변화를 감지했을 때 처리하는 로직을 구현한 것이다. 자식 뷰에 있는 프로퍼티에 접근하기 위해 @ViewChild 데코레이터를 사용했다.


AfterViewComponent (클래스 일부)
export class AfterViewComponent implements AfterViewChecked, AfterViewInit {
  private prevHero = "";

  // `ChildViewComponent` 타입의 뷰 자식 컴포넌트를 참조합니다.
  @ViewChild(ChildViewComponent) viewChild!: ChildViewComponent;

  ngAfterViewInit() {
    // viewChild는 뷰가 모두 초기화된 이후에 값이 할당됩니다.
    this.logIt("AfterViewInit");
    this.doSomething();
  }

  ngAfterViewChecked() {
    // 뷰에서 변화감지 로직이 동작하면 viewChild가 갱신됩니다.
    if (this.prevHero === this.viewChild.hero) {
      this.logIt("AfterViewChecked (no change)");
    } else {
      this.prevHero = this.viewChild.hero;
      this.logIt("AfterViewChecked");
      this.doSomething();
    }
  }
  // ...
}


2) 화면이 갱신될 때까지 기다리기

이 예제에서 doSomething() 메서드는 히어로의 이름이 10글자를 넘어갔을 때 화면에 관련 메시지를 표시하는데, comment 프로퍼티를 갱신하기 전에 한 싸이클(tick) 기다린다.


AfterViewComponent (doSomething())
// 동작을 확인하기 위해 `comment` 값을 변경해 봅니다.
private doSomething() {
  const c = this.viewChild.hero.length > 10 ? `That's a long name` : '';
  if (c !== this.comment) {
    // 컴포넌트의 뷰는 방금 검사를 마쳤기 때문에 한 싸이클 뒤에 실행합니다.
    this.logger.tick_then(() => this.comment = c);
  }
}


후킹 메서드 ngAfterViewInit()ngAfterViewChecked()는 모두 컴포넌트 뷰가 갱신된 후에 실행된다. 이 때 컴포넌트에 바인딩되는 comment 프로퍼티 값을 즉시 변경하면 Angular가 에러를 발생시킨다.


그래서 LoggerService.tick_then()를 사용해서 브라우저의 JavaScript 싸이클을 한 번 지연시킨 후 새로운 변화 감지 싸이클을 시작하는 방식으로 구현하는 것이 좋다.


AfterView 예제를 실행해보면 별다른 변화가 없어도 AfterViewChecked() 메서드가 자주 실행되는 것을 확인할 수 있다. 이렇게 자주 실행되는 라이프싸이클 후킹 메서드에는 복잡한 로직을 작성하지 않아야 한다. 그래야 앱 성능 저하를 피할 수 있다.


003


3) 외부 컨텐츠 변경사항 감지하기

컨텐츠 프로젝션(content projection)은 컴포넌트 밖에서 가져온 HTML 컨텐츠를 컴포넌트 템플릿 안에 표시하는 것을 의미한다. 템플릿에 사용된 컨텐츠 프로젝션은 이런 경우이다.


  • 컴포넌트 엘리먼트 태그 안에 들어있는 HTML
  • 컴포넌트 템플릿에서 <ng-content>가 사용된 부분


Note

  • AngularJS에서는 이 테크닉을 트랜스클루전(transclusion)이라고 한다.


AfterContent 예제에서 다루는 AfterContentInitAfterContentChecked 후킹 함수는 Angular가 외부 컨텐츠를 컴포넌트 안에 프로젝션한 후에 실행된다.


이전에 살펴본 AfterView 예제와 비교해 보자. 이번에는 템플릿에 자식 뷰를 포함하는 것이 아니라 부모 컴포넌트 AfterContentComponent에서 받아오는 방식으로 구현했다. 그래서 부모 템플릿은 이렇게 구성된다.


AfterContentParentComponent (템플릿 일부)
`<after-content>
  <app-child></app-child>
</after-content>`;


<app-child> 태그가 <after-content> 태그 안에 들어가 있는 것을 유심히 보자. 컴포넌트 안에 프로젝션하는 경우가 아니라면 컴포넌트 엘리먼트 태그 안에는 아무것도 넣어서는 안된다.


이제 컴포넌트 템플릿을 보자.


AfterContentComponent (템플릿)
template: `
  <div>projected content begins</div>
    <ng-content></ng-content>
  <div>projected content ends</div>
`;


<ng-content> 태그는 외부 컨텐츠가 들어갈 위치를 지정한다. 그래서 이 경우에는 부모 컴포넌트에 사용한 <app-child>가 컴포넌트 안으로 프로젝션 된다.


004


4) AfterContent 후킹 함수 활용하기

AfterContentAfterView와 비슷하게 동작한다. 자식 컴포넌트에서 일어난다는 점만 다르다.


  • AfterView는 ViewChildren과 관련이 있다. 컴포넌트 템플릿 안에 사용된 자식 컴포넌트 태그에 반응한다.
  • AfterContent는 ContentChildren과 관련이 있다. 컴포넌트에 프로젝션된 자식 컴포넌트에 반응한다.


아래 예제 코드에서 AfterContent 후킹 함수는 자식 컨텐츠가 변경된 것을 감지할 때 동작한다. 컴포넌트 클래스에서 자식 컨텐츠를 참조하려면 @ContentChild 데코레이터를 사용하면 된다.


AfterContentComponent (클래스 일부)
export class AfterContentComponent
  implements AfterContentChecked, AfterContentInit
{
  private prevHero = "";
  comment = "";

  // `ChildComponent` 타입의 자식 컴포넌트를 참조합니다.
  @ContentChild(ChildComponent) contentChild!: ChildComponent;

  ngAfterContentInit() {
    // contentChild는 컨텐츠가 모두 초기화된 이후에 값이 할당됩니다.
    this.logIt("AfterContentInit");
    this.doSomething();
  }

  ngAfterContentChecked() {
    // 컨텐츠에서 변화감지 로직이 동작하면 contentChild가 갱신됩니다.
    if (this.prevHero === this.contentChild.hero) {
      this.logIt("AfterContentChecked (no change)");
    } else {
      this.prevHero = this.contentChild.hero;
      this.logIt("AfterContentChecked");
      this.doSomething();
    }
  }
  // ...
}


Note

  • 컨텐츠가 갱신된 것은 기다릴 필요가 없다.
  • 컴포넌트에 정의된 doSomething() 메서드는 컴포넌트에 바인딩된 comment 프로퍼티 값을 즉시 갱신한다.
  • 그런데 이때는 렌더링이 제대로 되도록 한 싸이클 기다리는 동작을 할 필요가 없다.
  • Angular는 AfterView를 실행하기 전에 AfterContent 후킹 함수를 먼저 실행한다.
  • 그리고 컨텐츠 프로젝션이 마무리 되는 시점은 Angular가 컴포넌트 뷰 화면을 마무리하기 전이다.
  • 따라서 AfterContent...와 AfterView... 후킹 함수가 실행되는 타이밍 사이에 약간의 틈이 있다.
  • 이 시점에 호스트 뷰에서 무언가 변경해도 정상적으로 렌더링 된다.


8. 커스텀 변화감지 로직 정의하기

입력 프로퍼티 값이 변경되었지만 ngOnChanges()에서 감지하지 못했다면, DoCheck 예제에서 다룬 것처럼 직접 변화를 감지하는 로직을 작성해도 된다. 아래 예제를 확인해 보자.


DoCheck 예제는 OnChanges 앱을 확장하며 ngDoCheck() 메서드를 추가한 것이다:


DoCheckComponent (ngDoCheck())
ngDoCheck() {

  if (this.hero.name !== this.oldHeroName) {
    this.changeDetected = true;
    this.changeLog.push(`DoCheck: Hero name changed to "${this.hero.name}" from "${this.oldHeroName}"`);
    this.oldHeroName = this.hero.name;
  }

  if (this.power !== this.oldPower) {
    this.changeDetected = true;
    this.changeLog.push(`DoCheck: Power changed to "${this.power}" from "${this.oldPower}"`);
    this.oldPower = this.power;
  }

  if (this.changeDetected) {
      this.noChangeCount = 0;
  } else {
      // 컴포넌트의 내용이 변경되지 않았으면 함수가 몇번 실행되었는지 로그를 출력합니다.
      const count = this.noChangeCount += 1;
      const noChangeMsg = `DoCheck called ${count}x when no change to hero or power`;
      if (count === 1) {
        // "no change" 메시지를 추가합니다.
        this.changeLog.push(noChangeMsg);
      } else {
        // 마지막에 추가한 "no change" 메시지를 수정합니다.
        this.changeLog[this.changeLog.length - 1] = noChangeMsg;
      }
  }

  this.changeDetected = false;
}


이 코드는 확인하고 싶은 값을 직접 가져와서 이전 값과 현재 값을 비교하고, hero 프로퍼티와 power 프로퍼티 값이 변경되지 않으면 변경된 내용이 없다는 메시지를 출력한다. 이 과정은 DoCheck()가 실행될 때마다 반복된다. 실행되는 모습을 확인해 보자.


005


이렇게 구현하면 ngDoCheck() 메서드에서 히어로의 name 프로퍼티가 변경된 것을 감지할 수 있지만, 이 방식은 아주 무거운 부하를 동반한다. ngDoCheck() 메서드는 꼭 필요하지 않은 변화 감지 싸이클에도 매번 반응하며 실행되기 때문이다. 실제로 사용자가 아무런 동작을 하지 않아도 이 메서드는 20번 이상 실행되는 것도 확인할 수 있다.


이 중 대부분은 Angular가 화면을 렌더링하는 동안 이 컴포넌트와는 상관없는 영역에서 일어난 변화때문에 실행된 것이다. 심지어 마우스 커서를 <input> 엘리먼트로 옮기기만 해도 후킹 함수가 실행된다. 실제로 변화를 감지하기 위해 필요한 함수 실행은 몇 번 되지 않는다. 그래서 이 후킹 메서드를 사용하면서 사용자에게 불편을 주지 않으려면 메서드 안에 들어가는 로직을 아주 간단하게 작성해야 한다.


References