8. 동적 컴포넌트 로더
컴포넌트의 템플릿이 항상 애플리케이션 실행 전에 로드되어야만 하는 것은 아니다. 컴포넌트 템플릿은 애플리케이션이 실행되는 중에도 불러올 수 있다.
이 문서는 ComponentFactoryResolver
예제를 사용해서 컴포넌트를 동적으로 생성하는 방법을 알아본다.
이 문서에서 다루는 예제는 라이브 예제 링크 / 다운로드 링크에서 실행하거나 다운받을 수 있다.
1. 동적 컴포넌트 로딩
광고 배너를 동적으로 만드는 예제를 보면서 자세하게 알아보자.
히어로 주식회사는 광고 캠페인을 몇 가지 싸이클로 표시하려고 한다. 그런데 이 광고의 내용은 여러 팀이 각자 추가하기 때문에 정적인 컴포넌트 구조로는 이 요구사항을 만족할 수 없다고 하자.
그러면 컴포넌트의 템플릿을 고정된 HTML 문서로 작성하지 않고 어딘가에서 불러오는 방법을 사용해야 한다.
이 로직은 컴포넌트를 동적으로 로드하는 Angular API를 활용해서 구현할 수 있다.
2. 앵커 디렉티브
컴포넌트를 정의하기 전에, 이 컴포넌트가 어디에 위치할지 지정하는 앵커를 지정해 보자.
광고가 표시될 위치를 지정하도록 AdDirective
디렉티브를 다음과 같이 정의한다.
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
를 적용할 때 대괄호([
, ]
)를 사용하지 않은 것에 주의한다. 이 문법은 어트리뷰트 셀렉터를 사용하는 것이 아니라 컴포넌트를 동적으로 로드하는 문법이다.
template: `
<div class="ad-banner-example">
<h3>Advertisements</h3>
<ng-template adHost></ng-template>
</div>
`;
4. 동적 컴포넌트 구성하기
ad-banner.component.ts
에 정의된 메서드들을 좀 더 자세하게 보자.
AdBannerComponent
는 AdItem
객체의 배열을 입력 프로퍼티로 받는데, 이 배열은 AdService
에서 받아올 것이다. AdItem
객체는 컴포넌트를 구성하기 위해 필요한 정보를 담는 용도로 사용하며, 요구사항을 만족시키기 위해 이 객체의 구체적인 값은 컴포넌트 외부인 AdService
에서 받아온다.
결국 AdBannerComponent
에는 템플릿이 정적으로 지정되지 않은 컴포넌트 데이터가 배열 형태로 전달될 것이다.
AdBannerComponent
는 getAds()
메서드를 사용해서 AdItems
배열을 각각 순회하는데, 3초마다 loadComponent()
메서드를 실행해서 컴포넌트를 하나씩 뷰에 표시한다.
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="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;
}
6. 최종 결과
우리가 구현한 광고 배너의 최종 결과물은 다음과 같다:
예제를 직접 실행하거나 다운로드 받으려면 라이브 예제 링크 / 다운로드 링크를 확인해 보자.