팁: 이 가이드 문서는 핵심 가이드 이후 내용을 다룹니다. 아직 Angular에 익숙하지 않다면 해당 문서를 먼저 읽어보세요.
템플릿에 컴포넌트를 직접 사용하는 방법 외에, 컴포넌트는 프로그래밍 방식으로 동적 렌더링할 수도 있습니다. 이 방식은 컴포넌트를 처음에 알 수 없는 경우나, 조건에 따라 달라지는 경우에 유용합니다.
컴포넌트를 프로그래밍 방식으로 렌더링하는 방법은 두 가지입니다: 템플릿에서 NgComponentOutlet을 사용하거나 TypeScript 코드에서 ViewContainerRef를 사용하면 됩니다.
참고: 무거운 컴포넌트의 로딩을 늦추는 지연 로딩을 활용하려면 @defer 기능을 활용하세요.
@defer 기능을 활용하면 @defer 블록 안에 있는 모든 컴포넌트, 디렉티브, 파이프 코드를 별도 JavaScript 빌드 파일로 추출해서 필요한 경우에만 로드합니다.
NgComponentOutlet 사용하기
NgComponentOutlet 은 템플릿에서 컴포넌트를 동적으로 렌더링하는 구조 디렉티브입니다.
import {NgComponentOutlet} from '@angular/common';
import {Component, input, computed} from '@angular/core';
@Component({/*...*/})
export class AdminBio {
/* ... */
}
@Component({/*...*/})
export class StandardBio {
/* ... */
}
@Component({
imports: [NgComponentOutlet],
template: `
<p>Profile for {{ user().name }}</p>
<ng-container *ngComponentOutlet="bioComponent()" />
`,
})
export class CustomDialog {
user = input.required<User>();
bioComponent = computed(() => {
return this.user().isAdmin ? AdminBio : StandardBio;
});
}
이 디렉티브에 대해 자세하게 알아보려면 NgComponentOutlet API 문서를 참고하세요.
렌더링되는 컴포넌트에 입력값 동적으로 전달하기
ngComponentOutletInputs 프로퍼티를 활용하면 렌더링되는 컴포넌트에 입력값을 동적으로 전달할 수 있습니다.
이 프로퍼티는 객체를 인자로 받는데, 객체의 키는 입력값의 이름이고, 객체의 값은 입력값의 실제 값입니다.
@Component({
selector: 'user-greeting',
template: `
<div>
<p>User: {{ username() }}</p>
<p>Role: {{ role() }}</p>
</div>
`,
})
export class UserGreeting {
username = input.required<string>();
role = input('guest');
}
@Component({
selector: 'profile-view',
imports: [NgComponentOutlet],
template: `<ng-container *ngComponentOutlet="greetingComponent; inputs: greetingInputs()" />`,
})
export class ProfileView {
greetingComponent = UserGreeting;
greetingInputs = signal({username: 'ngAwesome', role: 'admin'});
}
입력값은 greetingInputs 시그널의 값이 변경될 때마다 자동으로 변경되면서 부모의 상태와 동일하게 유지됩니다.
컨텐츠 프로젝션 제공하기
ngComponentOutletContent를 활용하면 렌더링되는 컴포넌트의 컨텐츠 프로젝션을 동적으로 전달할 수 있습니다.
이 방식은 <ng-content>로 동적 컴포넌트를 구성할 때 유용합니다.
@Component({
selector: 'card-wrapper',
template: `
<div class="card">
<ng-content />
</div>
`,
})
export class CardWrapper {}
@Component({
imports: [NgComponentOutlet],
template: `
<ng-container *ngComponentOutlet="cardComponent; content: cardContent()" />
<ng-template #contentTemplate>
<h3>Dynamic Content</h3>
<p>This content is projected into the card.</p>
</ng-template>
`,
})
export class DynamicCard {
private vcr = inject(ViewContainerRef);
cardComponent = CardWrapper;
private contentTemplate = viewChild<TemplateRef<unknown>>('contentTemplate');
cardContent = computed(() => {
const template = this.contentTemplate();
if (!template) return [];
// 프로젝션 영역을 배열로 반환합니다. 개별 엘리먼트는 <ng-content> 영역에 매칭됩니다.
// CardWrapper에는 <ng-content>가 하나 있기 때문에, 배열에는 엘리먼트를 하나만 반환합니다..
return [this.vcr.createEmbeddedView(template).rootNodes];
});
}
참고: 하이드레이션은 네이티브 DOM API로 생성한 DOM 노드를 프로젝션하는 방식을 지원하지 않습니다.
이렇게 사용하려고 하면 NG0503 오류가 발생합니다.
컨텐츠를 프로젝션하려면 Angular API를 사용하거나 컴포넌트에 ngSkipHydration을 사용하세요.
인젝터 등록하기
ngComponentOutletInjector를 활용하면 동적으로 생성하는 컴포넌트에 커스텀 인젝터를 등록할 수 있습니다.
이 방식은 컴포넌트별 서비스나 환경설정이 필요한 경우에 유용합니다.
export const THEME_DATA = new InjectionToken<string>('THEME_DATA', {
factory: () => 'light',
});
@Component({
selector: 'themed-panel',
template: `<div [class]="theme">...</div>`,
})
export class ThemedPanel {
theme = inject(THEME_DATA);
}
@Component({
selector: 'dynamic-panel',
imports: [NgComponentOutlet],
template: `<ng-container *ngComponentOutlet="panelComponent; injector: customInjector" />`,
})
export class DynamicPanel {
panelComponent = ThemedPanel;
customInjector = Injector.create({
providers: [{provide: THEME_DATA, useValue: 'dark'}],
});
}
컴포넌트 인스턴스에 접근하기
동적으로 생성된 컴포넌트 인스턴스는 exportAs로 접근할 수 있습니다:
@Component({
selector: 'counter',
template: `<p>Count: {{ count() }}</p>`,
})
export class Counter {
count = signal(0);
increment() {
this.count.update((c) => c + 1);
}
}
@Component({
imports: [NgComponentOutlet],
template: `
<ng-container [ngComponentOutlet]="counterComponent" #outlet="ngComponentOutlet" />
<button (click)="outlet.componentInstance?.increment()">Increment</button>
`,
})
export class CounterHost {
counterComponent = Counter;
}
참고: 컴포넌트가 렌더링되기 전이라면 componentInstance 프로퍼티의 값은 null 입니다.
자세한 내용은 NgComponentOutlet API를 참고하세요.
ViewContainerRef 사용하기
뷰 컨테이너 는 컴포넌트 트리에서 내용물을 담는 노드를 의미합니다.
그리고 컴포넌트나 디렉티브는 ViewContainerRef를 의존성 객체로 주입받아서 DOM에 존재하는 뷰 컨테이너를 참조할 수 있습니다.
ViewContainerRef 객체의 createComponent 메서드를 사용하면 컴포넌트를 동적으로 생성하고 렌더링할 수 있습니다.
ViewContainerRef를 사용해서 컴포넌트를 생성하면 Angular는 ViewContainerRef로 참조한 컴포넌트나 디렉티브 바로 옆에 DOM을 추가합니다.
@Component({
selector: 'leaf-content',
template: `This is the leaf content`,
})
export class LeafContent {}
@Component({
selector: 'outer-container',
template: `
<p>This is the start of the outer container</p>
<inner-item />
<p>This is the end of the outer container</p>
`,
})
export class OuterContainer {}
@Component({
selector: 'inner-item',
template: `<button (click)="loadContent()">Load content</button>`,
})
export class InnerItem {
private viewContainer = inject(ViewContainerRef);
loadContent() {
this.viewContainer.createComponent(LeafContent);
}
}
위 예제코드에서 사용자가 "Load content" 버튼을 클릭하면 아래 DOM 구조가 화면에 렌더링됩니다.
<outer-container>
<p>This is the start of the outer container</p>
<inner-item>
<button>Load content</button>
</inner-item>
<leaf-content>This is the leaf content</leaf-content>
<p>This is the end of the outer container</p>
</outer-container>
컴포넌트 지연 로딩
참고: 컴포넌트를 지연로딩한다면 @defer feature 사용을 고려해 보세요.
@defer가 제공하는 기능보다 다른 기능이 필요하다면 NgComponentOutlet이나 ViewContainerRef를 사용해서 표준 JavaScript 동적 import를 활용하면 됩니다.
@Component({
...,
template: `
<section>
<h2>Basic settings</h2>
<basic-settings />
</section>
<section>
<h2>Advanced settings</h2>
@if(!advancedSettings) {
<button (click)="loadAdvanced()">
Load advanced settings
</button>
}
<ng-container *ngComponentOutlet="advancedSettings" />
</section>
`
})
export class AdminSettings {
advancedSettings: {new(): AdvancedSettings} | undefined;
async loadAdvanced() {
const { AdvancedSettings } = await import('path/to/advanced_settings.js');
this.advancedSettings = AdvancedSettings;
}
}
이렇게 구현하면 사용자가 버튼을 클릭했을 때 AdvancedSettings 컴포넌트가 동적으로 로딩되어 화면에 렌더링됩니다.
컴포넌트 생성 시점에 입출력값 바인딩, 호스트 디렉티브 설정하면
컴포넌트를 동적으로 생성할 때 입력값을 수동으로 설정하고 출력값을 구독하면 오류가 발생할 가능성이 높습니다. 컴포넌트 인스턴스를 생성한 뒤 바인딩을 연결하려면 추가 코드를 작성해야 하는 경우가 많습니다.
간단하게 이야기하면, createComponent나 ViewContainerRef.createComponent 함수는 inputBinding(), outputBinding(), twoWayBinding()과 같이 입출력값을 바인딩하는 기능을 제공합니다.
호스트 디렉티브에는 directives 배열을 지정할 수도 있습니다.
이 방식을 활용하면 컴포넌트를 코드로 생성하면서 템플릿 바인딩을 한 번에 처리할 수 있습니다.
ViewContainerRef.createComponent를 활용하는 호스트 뷰
ViewContainerRef.createComponent는 컴포넌트를 생성하면서 호스트 뷰와 호스트 엘리먼트를 컨테이너의 뷰 계층 내 지정된 위치에 자동으로 삽입합니다.
이 메서드는 동적 컴포넌트가 목록 항목이나 인라인 UI와 같이 컨테이너의 논리적, 시각적 구조의 일부가 되어야 할 때 활용합니다.
반면, 함수로 제공되는 createComponent API는 새 컴포넌트를 생성하면서 이 컴포넌트를 뷰나 DOM에 삽입하지 않고, ComponentRef를 반환하기 때문에, 이 참조를 활용해서 컴포넌트의 호스트 엘리먼트의 원하는 위치에 넣을 수 있습니다.
import {Component, input, model, output} from '@angular/core';
@Component({
selector: 'app-warning',
template: `
@if (isExpanded()) {
<section>
<p>Warning: Action needed!</p>
<button (click)="close.emit(true)">Close</button>
</section>
}
`,
})
export class AppWarning {
readonly canClose = input.required<boolean>();
readonly isExpanded = model<boolean>();
readonly close = output<boolean>();
}
import {
Component,
ViewContainerRef,
signal,
inputBinding,
outputBinding,
twoWayBinding,
inject,
} from '@angular/core';
import {FocusTrap} from '@angular/cdk/a11y';
import {ThemeDirective} from '../theme.directive';
@Component({
template: `<ng-container #container />`,
})
export class Host {
private vcr = inject(ViewContainerRef);
readonly canClose = signal(true);
readonly isExpanded = signal(true);
showWarning() {
const compRef = this.vcr.createComponent(AppWarning, {
bindings: [
inputBinding('canClose', this.canClose),
twoWayBinding('isExpanded', this.isExpanded),
outputBinding<boolean>('close', (confirmed) => {
console.log('Closed with result:', confirmed);
}),
],
directives: [
FocusTrap,
{type: ThemeDirective, bindings: [inputBinding('theme', () => 'warning')]},
],
});
}
}
위 예제에서 동적으로 생성되는 AppWarning은 canClose 입력값이 반응형 시그널과 바인딩되며, isExpanded 상태는 양방향으로 바인딩되고, close 출력값의 리스너도 연결합니다.
그리고 FocusTrap과 ThemeDirective는 directives에 지정되어 호스트 엘리먼트에 등록되었습니다.
createComponent + hostElement로 document.body에 팝업 붙이기
오버레이와 같이 현재 뷰 계층 외부에 렌더링할 때 사용하는 방식입니다.
hostElement로 지정한 엘리먼트는 컴포넌트의 호스트 엘리먼트가 되며, Angular는 새 엘리먼트를 생성하지 않습니다.
원하는 대로 바인딩을 직접 구성할 수 있습니다.
import {
ApplicationRef,
createComponent,
EnvironmentInjector,
inject,
Injectable,
inputBinding,
outputBinding,
Service,
} from '@angular/core';
import {Popup} from './popup';
@Service()
export class PopupService {
private readonly injector = inject(EnvironmentInjector);
private readonly appRef = inject(ApplicationRef);
show(message: string) {
// 팝업 UI의 호스트 엘리먼트를 생성합니다.
const host = document.createElement('popup-host');
// 함수 호출 한 번으로 컴포넌트를 생성하고 바인딩합니다.
const ref = createComponent(Popup, {
environmentInjector: this.injector,
hostElement: host,
bindings: [
inputBinding('message', () => message),
outputBinding('closed', () => {
document.body.removeChild(host);
this.appRef.detachView(ref.hostView);
ref.destroy();
}),
],
});
// 변화 감지 싸이클에 컴포넌트 뷰를 연결합니다.
this.appRef.attachView(ref.hostView);
// 일반적인 Angular 뷰 계층 밖의 DOM에 호스트 엘리먼트를 넣습니다.
// 그러면 팝업이 화면에 표시되는데, 오버레이나 모달을 구현할 때 일반적으로 사용하는 방식입니다.
document.body.appendChild(host);
}
}
렌더링 오류 처리하기
IMPORTANT: onError 옵션과 이 옵션의 ErrorDetails 매개변수는 개발자 미리보기 단계입니다.
ViewContainerRef.createComponent나 독립형 createComponent 함수로 컴포넌트를 동적으로 생성할 때, 옵션 객체에 onError 콜백을 지정하면 렌더링이나 변경 감지 단계에서 발생하는 오류를 처리할 수 있습니다. 템플릿의 @error 블록과 같은 기능을 프로그래밍 방식으로 사용하는 것입니다.
viewContainerRef.createComponent(DynamicComponent, {
onError: (err: Error, details: ErrorDetails) => {
console.error('Component rendering failed:', err);
// 대체 UI를 렌더링하거나 측정값을 기록합니다.
},
});
참고: onError 콜백은 렌더링이나 변경 감지 단계에서 발생하는 오류만 처리합니다. 생성자처럼 컴포넌트 인스턴스를 생성하는 동안 발생한 오류는 처리하지 않습니다. 생성 과정의 오류는 API를 호출할 때 Angular가 동기적으로 발생시킵니다.