createComponent으로 동적 컴포넌트 만들기 (Dynamic Component with createComponent)
보다 범용적인 버전에서 활용할 수 있는 createComponent
를 사용하여 dynamic component를 구현합니다.
기본 원리
componentFactory
를 활용하여 호출할 component의 정보를 가져옵니다.- component를 담을 container component의
viewContainerRef
를 가져옵니다. viewContainerRef
의createComponent
함수를 통해 호출할 component를 container component에 랜더링 합니다.viewContainerRef
의clear
함수를 활용하여 호출한 component를 제거 합니다.
component 생성하기
container component
dynamic component를 호출하는 container에 해당하는 component를 작성해 봅시다.
버튼 2개를 클릭했을 때 서로 다른 component가 화면에 출력되는 예제 입니다.
@Component({
selector: 'app-container',
template: `
<button (click)="callAComponent()">Show A-Component</button>
<button (click)="callBComponent()">Show B-Component</button>
`
})
export class ContainerComponent {
constructor(
private viewContainerRef: ViewContainerRef,
private resolver: ComponentFactoryResolver
) {}
callAComponent() {
const resolve = this.resolver.resolveComponentFactory(AComponent);
this.viewContainerRef.crateComponent(resolve);
}
callBComponent() {
const resolve = this.resolver.resolveComponentFactory(BComponent);
this.viewContainerRef.crateComponent(resolve);
}
}
dynamic components
호출할 두개의 component를 작성해봅시다. css를 작성하면 보다 확실히 확인할 수 있으나 여기에서는 생략합니다.
@Component({
selector: 'a-component',
template: `
<p>Hello World</p>`
})
export class AComponent {
}
@Component({
selector: 'b-component',
template: `
<p>Good bye</p>`
})
export class BComponent {
}
예제를 실행하면 버튼을 클릭하여 AComponent
, BComponent
를 스위치하여 화면에 표시합니다.
값을 주입하는 방법
값을 주입하는 방법은 매우 간단합니다. createComponent
의 리턴 값을 받아 instance에 값을 주입하면 됩니다.
여기에서 instance란 쉽게 말해 호출할 component의 public 변수 / 함수 라고 생각하면 됩니다.
또한, @Input()
으로 선언된 변수에도 주입이 가능하지만 @Input('') set ...
에는 적용되지 않습니다.
그리고, onchange
사이클에도 잡히지 않으므로 주의하여야 합니다.
위의 component들을 다시 작성해보겠습니다.
container component
dynamic component를 호출하는 container에 해당하는 component를 작성해 봅시다.
버튼 2개를 클릭했을 때 서로 다른 component가 화면에 출력되는 예제 입니다.
@Component({
selector: 'app-container',
template: `
<button (click)="callAComponent()">Show A-Component</button>
<button (click)="callBComponent()">Show B-Component</button>
`
})
export class ContainerComponent {
constructor(
private viewContainerRef: ViewContainerRef,
private resolver: ComponentFactoryResolver
) {}
callAComponent() {
const resolve = this.resolver.resolveComponentFactory(AComponent);
const componentRef = this.viewContainerRef.crateComponent(resolve);
componentRef.instance.data = 'hello world';
}
callBComponent() {
const resolve = this.resolver.resolveComponentFactory(BComponent);
const componentRef = this.viewContainerRef.crateComponent(resolve);
componentRef.instance.data = 'good bye';
}
}
dynamic components
@Component({
selector: 'a-component',
template: `
<p><% raw %><% endraw %></p>`
})
export class AComponent {
data!: string;
}
@Component({
selector: 'b-component',
template: `
<p><% raw %><% endraw %></p>`
})
export class BComponent {
@Input() data!: string;
}
실행해보면 data 변수를 통해 주입된 값이 표시됨을 확인할 수 있습니다.
BComponent
의 @Input()
은 이런 형태도 잘 주입 된다는 것을 테스트 해보기 위해 작성한 것이며, 의미는 없습니다.
값 주입이 안되는 경우
만일 값을 2회 이상 주입한다면 instance 통해 주입하더라도 값이 변경되지 않습니다.
afterViewInit
에서 확인해보면 값이 들어오고 있음을 확인할 수 있는데 이 때 changeDetection
을 통해 랜더링 시켜주어야 비로소 화면에 적용됩니다.
container component
...
export class ContainerComponent implements AfterViewInit {
constructor(
private viewContainerRef: ViewContainerRef,
private resolver: ComponentFactoryResolver,
private cd: ChangeDetectionRef
) {}
ngAfterViewInit() {
this.cd.markForCheck();
}
Subject를 활용한 값 주입
changeDetection
사용이 꺼려진다면 Subject를 활용하는 것도 좋은 방안이 될 수 있습니다.
초기값을 가질 수 있는 BahaviorSubject
를 활용한다면 보다 쉽게 값을 주입할 수 있습니다.
container component
@Component({
selector: 'app-container',
template: `
<button (click)="callAComponent()">Show A-Component</button>
<button (click)="callBComponent()">Show B-Component</button>
`
})
export class ContainerComponent {
constructor(
private viewContainerRef: ViewContainerRef,
private resolver: ComponentFactoryResolver
) {}
callAComponent() {
const resolve = this.resolver.resolveComponentFactory(AComponent);
const componentRef = this.viewContainerRef.crateComponent(resolve);
componentRef.instance.data$.next('hello world');
}
callBComponent() {
const resolve = this.resolver.resolveComponentFactory(BComponent);
const componentRef = this.viewContainerRef.crateComponent(resolve);
componentRef.instance.data.next('good bye');
}
}
dynamic components
@Component({
selector: 'a-component',
template: `
<p><% raw %><% endraw %></p>`
})
export class AComponent {
data$ = new BehaviorSubject<any>('');
}
@Component({
selector: 'b-component',
template: `
<p><% raw %><% endraw %></p>`
})
export class BComponent {
data$ = new BehaviorSubject<any>('');
}
Component 제거하기
제거는 매우 간단하게 container component의 viewContainerRef
의 clear
함수를 호출하면 됩니다.
container component
@Component({
selector: 'app-container',
template: `
<button (click)="callAComponent()">Show A-Component</button>
<button (click)="callBComponent()">Show B-Component</button>
<button (click)="removeAll()">RemoveAll</button>
`
})
export class ContainerComponent {
constructor(
private viewContainerRef: ViewContainerRef,
private resolver: ComponentFactoryResolver
) {}
callAComponent() {
const resolve = this.resolver.resolveComponentFactory(AComponent);
const componentRef = this.viewContainerRef.crateComponent(resolve);
componentRef.instance.data$.next('hello world');
}
callBComponent() {
const resolve = this.resolver.resolveComponentFactory(BComponent);
const componentRef = this.viewContainerRef.crateComponent(resolve);
componentRef.instance.data.next('good bye');
}
removeAll() {
this.viewContainerRef.clear();
}
}
기타
v13 이후 개선사항
v13부터는 기존의 createComponent
함수가 deprecated 되고, 새로 작성된 createComponent
함수를 사용해야 합니다.
기존에는 componentFactory
를 통해 component에 접근해야 했는데 새로운 버전에서는 component에 직접 접근할 수 있게 되어 더욱 간결한 코딩이 가능해졌습니다.
위의 container component를 v13 버전으로 작성하면 다음과 같습니다.
container component
@Component({
selector: 'app-container',
template: `
<button (click)="callAComponent()">Show A-Component</button>
<button (click)="callBComponent()">Show B-Component</button>
`
})
export class ContainerComponent {
constructor(
private viewContainerRef: ViewContainerRef
) {}
callAComponent() {
const componentRef = this.viewContainerRef.crateComponent(AComponent);
componentRef.instance.data$.next('hello world');
}
callBComponent() {
const componentRef = this.viewContainerRef.crateComponent(BComponent);
componentRef.instance.data.next('good bye');
}
}
댓글남기기