Angular로 애플리케이션을 제작하다보면 동적으로 컴포넌트를 추가해야할 때가 있습니다. 레이어 팝업을 띄운다던지, 페이지 내에서 사용자의 인터랙션에 따라 컨텐츠를 추가할 때, 우리는 특정 element를 생성해서 DOM에 추가하여야 합니다.

보통 Angular에서 컴포넌트의 추가는 route를 통해서만 이루어 집니다. 이때는 route에서 컴포넌트를 생성/제거를 해주기 때문에 우리는 크게 신경써야 할 부분이 없습니다.
하지만 동적으로 컴포넌트를 추가할때는 몇가지 신경써야 하는 부분들이 있습니다.

template에 동적 컴포넌트를 추가할 영역 정하기.

1
2
3
4
5
6
7
import {Component, ViewChild} from "@angular/core";
@Component({
selector: 'home',
template: `
<div #container></div>
`
})

위는 컴포넌트 템플릿의 #container은 컴포넌트를 추가할 영역입니다. 그럼 이제 class에서 해당 영역을 선언해봅시다.

1
2
3
export class HomeComponent{
@ViewChild('container') container;
}

container를 콘솔로 직어보면 ElementRef라는 객체가 보입니다. 이는 native 엘리먼트에 접근하기 위한 방법입니다.
하지만 우리는 ViewContainerRef가 필요합니다. 우리는 이를 다음과 같이 써서 얻을 수 있습니다.

1
2
3
export class HomeComponent{
@ViewChild('container', {read:ViewContainerRef}) container;
}

이렇게 하면, containerElementRef로 읽는 대신, ViewContainerRef로 읽어오게 됩니다.
다시 콘솔을 찍어보면 ViewContainerRef 객체가 보입니다. 이제 우리는 this.container.createComponent같은 메소드를 사용할 수 있습니다. 실제로 컴포넌트를 불러와 봅시다.

동적으로 컴포넌트 생성하기

1
2
3
4
5
6
7
8
export class HomeComponent{
@ViewChild('container', {read:ViewContainerRef}) container;
constructor(private resolver:ComponentFactoryResolver){}
ngAfterContentInit(){
this.container.createComponent(this.resolver.resolveComponentFactory(WidgetThree));
}
}

CreateComponentComponentFactory를 사용합니다. 컴포넌트를 생성하려면 CreateComponentComponentFactory를 전달해야합니다.
ComponentFactory Resolver(ComponentFactoryResolver) 서비스를 삽입함으로써 컴포넌트 팩토리를 얻습니다. 이 Resolver를 사용하여 this.resolver.resolveComponentFactory를 호출 한 다음 유형별로 조회 할 수 있습니다.
그리고 WidgetThree라는 컴포넌트를 인자로 넘깁니다.

이를 좀 더 가독성 있게 바꿔보겠습니다.

1
2
3
4
ngAfterContentInit(){
const widgetFactory = this.resolver.resolveComponentFactory(WidgetThree);
this.container.createComponent(widgetFactory);
}

widgetFactory 변수에 resolveComponentFactory(WidgetThree)를 담았습니다. 그리고 이를 createComponent함수를 이용해서 container안에 컴포넌트를 추가하려고 합니다.

하지만 이 함수를 실행해보면 다음과 같은 에러가 납니다.

ORIGINAL EXCEPTION: No component factory found for WidgetThree

기본적으로 Angular와 Angular 컴파일러는 template들의 selector에 참조되지 않는 컴포넌트들은 컴파일 하지 않습니다. 즉, selector기반으로 찾고, 번들링 합니다. 그리고 나머지 컴포넌트들은 무시합니다.
따라서 우리가 만일 WidgetThree컴포넌트를 module에서 declaration 했다고 하더라도, selector에 지정되어 있지 않으면 번들링 되지 않는 것입니다.
따라서 이와같은 컴포넌트를 컴파일 대상에 강제로 배정하기 위해서는 NgModule의 옵션 entryComponents에 해당 컴포넌트를 추가해주어야 합니다. 이는 이 컴포넌트를 사용할 것이라고 정의하는 것입니다.

아래는 해당내용에 대한 코드입니다.

1
2
3
4
5
6
@NgModule({
imports:[CommonModule],
declarations:[WidgetOne, WidgetTwo, WidgetThree],
entryComponents:[WidgetThree],
exports:[WidgetOne, WidgetTwo, WidgetThree, CommonModule]
})

자 이렇게 해서 실행하면 동적으로 컴포넌트를 추가 할 수 있습니다.

동적으로 생성한 컴포넌트에 데이터 전달하기

생성한 컴포넌트의 instance객체를 이용하여 동적으로 생성한 컴포넌트에 데이터를 전달할 수 있습니다.

1
2
3
4
5
ngAfterViewInit(){
const widgetFactory = this.resolver.resolveComponentFactory(WidgetThree);
const widgetRef = this.container.createComponent(widgetFactory);
widgetRef.instance.message = "I'm last!";
}

이제 데이터를 전달할 준비가 되었습니다. 받는쪽은 자식 컴포넌트가 부모 컴포넌트의 데이터를 받는 방식과 동일합니다. WidgetThree의 클래스에서 @Input 데코레이터를 사용하여 message 프로퍼티를 받습니다.

1
2
3
4
5
6
7
8
9
@Component({
selector: 'widget-three',
template: `
<input #input type="text" [value]="message">
`
})
export class WidgetThree{
@Input() message;
}