일반적으로 템플릿에서 조건에 따라 표시할 컴포넌트를 동적으로 교체하는 것은 까다로울 수 있습니다. 전통적으로는 여러 컴포넌트를 템플릿에 나열하고, @if나 @switch 같은 조건부 렌더링 구문을 사용해 원하는 컴포넌트만 노출하는 방법을 사용했습니다.
@switch (type()) {
@case (TYPEA) { <component-a /> }
@case (TYPEB) { <component-b /> }
@case (TYPEC) { <component-c /> }
@case (TYPED) { <component-d /> }
}
이러한 방식은 템플릿에서 직관적으로 렌더링될 컴포넌트를 파악할 수 있는 장점이 있지만, 조건이 많아질수록 템플릿 코드가 복잡해지고 지저분해진다는 단점이 있습니다.
ngComponentOutlet은 컴포넌트 클래스(TS 코드) 내에서 템플릿에 렌더링할 컴포넌트를 동적으로 선택하고 교체할 수 있게 해주는 강력한 디렉티브입니다.
사용법은 간단합니다. 템플릿에서 컴포넌트가 표시될 위치에 *ngComponentOutlet을 선언하고, 렌더링할 컴포넌트 클래스를 바인딩하면 됩니다.
// app.component.ts
import { Component, signal, Type } from '@angular/core';
import { CommonModule } from '@angular/common'; // NgComponentOutlet을 위함
@Component({ selector: 'a-component', standalone: true, template: `<p>Hello World from A</p>` })
export class AComponent {}
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, AComponent],
template: `
<ng-container *ngComponentOutlet="component()"></ng-container>
`
})
export class AppComponent {
component = signal<Type<any>>(AComponent);
}
위 코드에서는 AppComponent의 component Signal이 AComponent를 가리키고 있으므로, 위치에 AComponent가 렌더링됩니다.
ngComponentOutlet을 사용하면 버튼 클릭과 같은 사용자 상호작용에 따라 렌더링될 컴포넌트를 쉽게 교체할 수 있습니다.
// app.component.ts
import { Component, signal, Type } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({ selector: 'a-component', standalone: true, template: `<p>Hello World</p>` })
export class AComponent {}
@Component({ selector: 'b-component', standalone: true, template: `<p>Good bye</p>` })
export class BComponent {}
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, AComponent, BComponent],
template: `
<ng-container *ngComponentOutlet="currentComponent()"></ng-container>
<button (click)="changeComponent()">Change Component</button>
`
})
export class AppComponent {
isA = signal(true);
currentComponent = signal<Type<any>>(AComponent);
changeComponent() {
this.isA.update(val => !val);
this.currentComponent.set(this.isA() ? AComponent : BComponent);
}
}
ngComponentOutlet의 강력한 기능 중 하나는 동적으로 렌더링되는 컴포넌트의 종류와 상관없이 동일한 방식으로 데이터를 주입할 수 있다는 점입니다. 데이터는 Injector를 통해, DOM 노드는 content를 통해 주입할 수 있습니다.
Injector를 사용하면 @Input 없이도 동적으로 생성되는 자식 컴포넌트에 값을 주입할 수 있습니다. InjectionToken을 사용하는 것이 일반적입니다.
// title.token.ts
import { InjectionToken } from '@angular/core';
export const TITLE = new InjectionToken<string>('app.title');
// child.component.ts
import { Component, inject } from '@angular/core';
import { TITLE } from './title.token';
@Component({
standalone: true,
template: `Complete: {{ titleInjected }}`
})
export class ChildComponent {
titleInjected = inject(TITLE);
}
// app.component.ts
import { Component, Injector, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ChildComponent } from './child.component';
import { TITLE } from './title.token';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
template: `
<ng-container *ngComponentOutlet="ChildComponent; injector: myInjector"></ng-container>
`
})
export class AppComponent {
ChildComponent = ChildComponent; // 템플릿에서 사용하기 위해
myInjector: Injector;
constructor() {
// 부모 인젝터를 상속받는 새로운 인젝터 생성
this.myInjector = Injector.create({
providers: [{ provide: TITLE, useValue: 'hello world from injector' }],
parent: inject(Injector) // 현재 인젝터를 부모로 설정
});
}
}
이 방식은 useValue뿐만 아니라 useClass, useFactory 등 다양한 Provider를 동적으로 주입할 수 있어 매우 유연합니다.
content 옵션은 동적 컴포넌트 내의 슬롯에 DOM 노드를 주입(Project)할 수 있게 해줍니다. document.createElement나 document.createTextNode를 사용하여 생성한 노드를 전달할 수 있습니다.
주의: content에 전달되는 값은 반드시 2차원 배열 (Node[][]) 형태여야 합니다.
// child.component.ts (ng-content 사용)
@Component({
standalone: true,
template: `Complete: <ng-content></ng-content>`
})
export class ChildComponent {}
// app.component.ts
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule],
template: `
<ng-container *ngComponentOutlet="ChildComponent; content: myContent"></ng-container>
`
})
export class AppComponent {
ChildComponent = ChildComponent;
myContent: Node[][];
constructor() {
const button = document.createElement('button');
button.textContent = 'Click me from parent';
button.onclick = () => alert('Button clicked!');
// 2차원 배열 형태로 노드를 전달
this.myContent = [[button]];
}
}
이처럼 부모 컴포넌트에서 생성하고 이벤트를 제어하는 DOM 요소를 자식에게 동적으로 전달할 수 있습니다.
ngComponentOutlet에 바인딩된 컴포넌트 변수에 null을 할당하면 렌더링된 컴포넌트를 제거할 수 있습니다.
// app.component.ts
// ... imports
export class AppComponent {
// Type<any> | null 유니온 타입을 사용
currentComponent = signal<Type<any> | null>(AComponent);
removeComponent() {
this.currentComponent.set(null);
}
}