Skip to content

8. 동적 컴포넌트 로더


컴포넌트의 템플릿이 항상 애플리케이션 실행 전에 로드되어야만 하는 것은 아니다. 컴포넌트 템플릿은 애플리케이션이 실행되는 중에도 불러올 수 있다.


이 문서는 ComponentFactoryResolver 예제를 사용해서 컴포넌트를 동적으로 생성하는 방법을 알아본다.


이 문서에서 다루는 예제는 라이브 예제 링크 / 다운로드 링크에서 실행하거나 다운받을 수 있다.


1. 동적 컴포넌트 로딩

광고 배너를 동적으로 만드는 예제를 보면서 자세하게 알아보자.


히어로 주식회사는 광고 캠페인을 몇 가지 싸이클로 표시하려고 한다. 그런데 이 광고의 내용은 여러 팀이 각자 추가하기 때문에 정적인 컴포넌트 구조로는 이 요구사항을 만족할 수 없다고 하자.


그러면 컴포넌트의 템플릿을 고정된 HTML 문서로 작성하지 않고 어딘가에서 불러오는 방법을 사용해야 한다.


이 로직은 컴포넌트를 동적으로 로드하는 Angular API를 활용해서 구현할 수 있다.


2. 앵커 디렉티브

컴포넌트를 정의하기 전에, 이 컴포넌트가 어디에 위치할지 지정하는 앵커를 지정해 보자.


광고가 표시될 위치를 지정하도록 AdDirective 디렉티브를 다음과 같이 정의한다.


src/app/ad.directive.ts
import { Directive, ViewContainerRef } from "@angular/core";

@Directive({
  selector: "[adHost]",
})
export class AdDirective {
  constructor(public viewContainerRef: ViewContainerRef) {}
}


AdDirective는 컴포넌트가 들어갈 뷰 컨테이너틀 참조할 수 있도록 ViewContainerRef를 의존성으로 주입받는다.


그리고 @Directive 데코레이터에는 셀렉터로 ad-host를 지정하는데, 우리가 만들 컴포넌트는 이 셀렉터에 해당하는 엘리먼트에 적용될 것이다.


3. 컴포넌트 불러오기

광고 배너의 코드는 ad-banner.component.ts에 대부분 작성되어 있다. 예제를 간단하게 하기 위해 이 컴포넌트의 템플릿은 @Component 데코레이터의 template 프로퍼티로 간단하게 정의했다.


이 코드에서 컴포넌트가 로드될 위치는 <ng-template> 엘리먼트 안이다. 그리고 AdDirective를 적용하려면 ad.directive.ts에 선언된 것처럼 ad-host 셀렉터를 사용하면 된다. <ng-template>AdDirective를 적용할 때 대괄호([, ])를 사용하지 않은 것에 주의한다. 이 문법은 어트리뷰트 셀렉터를 사용하는 것이 아니라 컴포넌트를 동적으로 로드하는 문법이다.


src/app/ad-banner.component.ts (템플릿)
template: `
  <div class="ad-banner-example">
    <h3>Advertisements</h3>
    <ng-template adHost></ng-template>
  </div>
`;


엘리먼트는 컴포넌트 외부에서 내용을 받아 컴포넌트를 구성하기 때문에 동적 컴포넌트를 구성하기에도 좋다.


4. 동적 컴포넌트 구성하기

ad-banner.component.ts에 정의된 메서드들을 좀 더 자세하게 보자.


AdBannerComponentAdItem 객체의 배열을 입력 프로퍼티로 받는데, 이 배열은 AdService에서 받아올 것이다. AdItem 객체는 컴포넌트를 구성하기 위해 필요한 정보를 담는 용도로 사용하며, 요구사항을 만족시키기 위해 이 객체의 구체적인 값은 컴포넌트 외부인 AdService에서 받아온다.


결국 AdBannerComponent에는 템플릿이 정적으로 지정되지 않은 컴포넌트 데이터가 배열 형태로 전달될 것이다.


AdBannerComponentgetAds() 메서드를 사용해서 AdItems 배열을 각각 순회하는데, 3초마다 loadComponent() 메서드를 실행해서 컴포넌트를 하나씩 뷰에 표시한다.


src/app/ad-banner.component.ts (일부)
export class AdBannerComponent implements OnInit, OnDestroy {
  @Input() ads: AdItem[] = [];

  currentAdIndex = -1;

  @ViewChild(AdDirective, { static: true }) adHost!: AdDirective;
  interval: number | undefined;

  ngOnInit() {
    this.loadComponent();
    this.getAds();
  }

  ngOnDestroy() {
    clearInterval(this.interval);
  }

  loadComponent() {
    this.currentAdIndex = (this.currentAdIndex + 1) % this.ads.length;
    const adItem = this.ads[this.currentAdIndex];

    const viewContainerRef = this.adHost.viewContainerRef;
    viewContainerRef.clear();

    const componentRef = viewContainerRef.createComponent<AdComponent>(
      adItem.component
    );
    componentRef.instance.data = adItem.data;
  }

  getAds() {
    this.interval = setInterval(() => {
      this.loadComponent();
    }, 3000);
  }
}


이 코드에서 loadComponent() 메서드의 로직은 조금 복잡하다. 하나씩 확인해 보자. 제일 먼저 어떤 광고를 표시할지 결정한다.


Note

  • loadComponent() 메서드는 뷰에 표시할 광고를 선택한다.
  • 이 함수는 현재 currentAdIndex 값에 1을 더한 값을 AdItem 배열의 길이로 나눈 나머지를 currentAdIndex 값으로 할당한다.
  • 그리고 이 값을 인덱스로 활용해서 adItem 배열을 참조한다.


그리고 나면 AdDirective 컴포넌트의 인스턴스에 있는 viewContainerRef를 참조한다. 이 객체는 adHost를 가리키는데, adHost는 이전에 언급했던 것처럼 Angular가 컴포넌트를 동적으로 로드할 위치를 지정한 디렉티브이다.


이전 설명에서 AdDirective에는 ViewContainerRef가 생성자를 통해 주입된다고 했다. 그래서 동적 컴포넌트를 구성하는 컴포넌트에서는 AdDirective의 인스턴스에 직접 접근할 수 있다.


그리고 컴포넌트의 템플릿을 구성하기 위해 ViewContainerRef에 있는 createComponent() 함수를 실행한다.


createComponent() 메서드는 이렇게 만들어진 컴포넌트의 인스턴스를 반환한다. 이 인스턴스의 프로퍼티를 직접 지정하면 컴포넌트의 내용을 바꿀 수 있다.


5. AdComponent 인터페이스

광고 배너 안에서는 AdService에서 받은 광고 데이터를 컴포넌트에 적용할 수 있도록 AdComponent 인터페이스를 사용한다.


그래서 아래 두 컴포넌트는 컴포넌트 클래스를 정의할 때 AdComponent 인터페이스를 활용한다:


import { Component, Input } from "@angular/core";

import { AdComponent } from "./ad.component";

@Component({
  template: `
    <div class="job-ad">
      <h4>{{ data.headline }}</h4>
      {{ data.body }}
    </div>
  `,
})
export class HeroJobAdComponent implements AdComponent {
  @Input() data: any;
}
import { Component, Input } from "@angular/core";

import { AdComponent } from "./ad.component";

@Component({
  template: `
    <div class="hero-profile">
      <h3>Featured Hero Profile</h3>
      <h4>{{ data.name }}</h4>

      <p>{{ data.bio }}</p>

      <strong>Hire this hero today!</strong>
    </div>
  `,
})
export class HeroProfileComponent implements AdComponent {
  @Input() data: any;
}
export interface AdComponent {
  data: any;
}


6. 최종 결과

우리가 구현한 광고 배너의 최종 결과물은 다음과 같다:


001


예제를 직접 실행하거나 다운로드 받으려면 라이브 예제 링크 / 다운로드 링크를 확인해 보자.


References