<ng-template> 엘리먼트는 표준 <template> 엘리먼트와 비슷하게 템플릿 조각(template fragment) 를 선언하는 엘리먼트입니다.
이렇게 정의한 템플릿 조각은 동적으로 렌더링하거나 코드로 렌더링할 수 있습니다.
템플릿 조각 만들기
템플릿 조각은 컴포넌트 템플릿에서 <ng-template> 엘리먼트를 사용하면 생성할 수 있습니다:
<p>This is a normal element</p>
<ng-template>
<p>This is a template fragment</p>
</ng-template>
<ng-template> 엘리먼트는 실제로 렌더링되지 않습니다.
이 엘리먼트는 이후에 동적으로 렌더링할 템플릿 조각에 대한 참조로 사용됩니다.
템플릿 조각의 컨텍스트
템플릿 조각에는 바인딩 표현식이 존재할 수 있습니다:
@Component({
/* ... */,
template: `<ng-template>You've selected {{count}} items.</ng-template>`,
})
export class ItemCounter {
count: number = 0;
}
템플릿 조각에 존재하는 표현식(expressions)이나 실행문(statements)은 템플릿 조각이 렌더링되는 곳이 아니라, 템플릿 조각이 선언된 컴포넌트의 컨텍스트와 연결됩니다.
템플릿 조각 참조하기
템플릿 조각을 참조하는 방법은 세 가지입니다:
<ng-template>엘리먼트에 템플릿 참조 변수를 선언하는 방법- 컴포넌트/디렉티브 쿼리로 쿼리하는 방법
<ng-template>엘리먼트를 의존성 관계로 참조하는 방법
위 세 방법 모두 템플릿 조각은 TemplateRef 객체 타입으로 참조합니다.
템플릿 참조 변수로 참조하기
<ng-template> 엘리먼트에 템플릿 참조 변수를 지정하면 템플릿의 다른 영역에서 템플릿 조각을 참조할 수 있습니다:
<p>This is a normal element</p>
<ng-template #myFragment>
<p>This is a template fragment</p>
</ng-template>
이제 이 컴포넌트의 템플릿에서는 myFragment 변수로 템플릿 조각을 참조할 수 있습니다.
쿼리로 참조하기
컴포넌트/디렉티브 쿼리 API를 사용하면 템플릿 조각을 참조할 수 있습니다.
viewChild 쿼리를 사용하면 TemplateRef 객체를 직접 쿼리할 수 있습니다:
@Component({
/* ... */,
template: `
<p>This is a normal element</p>
<ng-template>
<p>This is a template fragment</p>
</ng-template>
`,
})
export class ComponentWithFragment {
templateRef = viewChild<TemplateRef<unknown>>(TemplateRef);
}
그러면 이제 다른 클래스 멤버처럼 템플릿 조각을 참조할 수 있습니다.
템플릿 조각이 여러 개라면, 각 <ng-template> 엘리먼트에 템플릿 참조 변수를 추가해서 이름을 지정하고 그 이름으로 쿼리하면 됩니다:
@Component({
/* ... */,
template: `
<p>This is a normal element</p>
<ng-template #fragmentOne>
<p>This is one template fragment</p>
</ng-template>
<ng-template #fragmentTwo>
<p>This is another template fragment</p>
</ng-template>
`,
})
export class ComponentWithFragment {
fragmentOne = viewChild<TemplateRef<unknown>>('fragmentOne');
fragmentTwo = viewChild<TemplateRef<unknown>>('fragmentTwo');
}
이 경우에도 이전과 마찬가지로, 쿼리로 찾은 템플릿 조각은 다른 클래스 멤버처럼 참조할 수 있습니다.
의존성 관계로 참조하기
<ng-template> 엘리먼트에 디렉티브가 사용되었다면, 이 디렉티브를 활용해서 TemplateRef 객체를 참조할 수 있습니다:
@Directive({
selector: '[myDirective]',
})
export class MyDirective {
private fragment = inject(TemplateRef);
}
<ng-template myDirective>
<p>This is one template fragment</p>
</ng-template>
이렇게 참조한 템플릿 조각은 클래스 멤버처럼 참조할 수 있습니다.
템플릿 조각 렌더링하기
템플릿 조각을 TemplateRef 객체 타입으로 참조하고 나면, 이 템플릿 조각은 템플릿에서 NgTemplateOutlet 디렉티브로 렌더링하거나, TypeScript 코드에서 ViewContainerRef로 렌더링할 수 있습니다.
NgTemplateOutlet 로 렌더링하기
@angular/common 패키지로 제공되는 NgTemplateOutlet 디렉티브는 TemplateRef를 인자로 받아서 디렉티브가 지정된 엘리먼트의 이웃으로 템플릿 조각을 렌더링합니다.
보통은 <ng-container> 엘리먼트에 NgTemplateOutlet 디렉티브를 사용합니다.
먼저, NgTemplateOutlet 심볼을 로드합니다:
import {NgTemplateOutlet} from '@angular/common';
그리고 아래 예제 코드처럼 템플릿 조각을 선언한 후 <ng-container>에 NgTemplateOutlet을 지정해서 렌더링하면 됩니다:
<p>This is a normal element</p>
<ng-template #myFragment>
<p>This is a fragment</p>
</ng-template>
<ng-container *ngTemplateOutlet="myFragment"></ng-container>
최종 결과는 이렇게 렌더링됩니다:
<p>This is a normal element</p>
<p>This is a fragment</p>
ViewContainerRef 로 렌더링하기
Angular 컴포넌트 트리에서 내용물이 존재할 수 있는 노드를 뷰 컨테이너(view container) 라고 합니다.
그리고 컴포넌트나 디렉티브는 ViewContainerRef 를 의존성으로 주입 받아서 해당 컴포넌트나 디렉티브에 해당하는 뷰 컨테이너를 참조할 수 있습니다.
이렇게 참조한 ViewContainerRef 객체의 createEmbeddedView 메서드를 사용하면 템플릿 조각을 동적으로 렌더링할 수 있으며, 렌더링하는 템플릿 조각은 ViewContainerRef 객체의 이웃으로 DOM에 추가됩니다.
아래 코드는 사용자가 버튼을 클릭하면 템플릿 조각을 찾아서 DOM에 렌더링하는 예제 코드입니다.
@Component({
/* ... */,
selector: 'component-with-fragment',
template: `
<h2>Component with a fragment</h2>
<ng-template #myFragment>
<p>This is the fragment</p>
</ng-template>
<my-outlet [fragment]="myFragment" />
`,
})
export class ComponentWithFragment { }
@Component({
/* ... */,
selector: 'my-outlet',
template: `<button (click)="showFragment()">Show</button>`,
})
export class MyOutlet {
private viewContainer = inject(ViewContainerRef);
fragment = input.required<TemplateRef<unknown>>();
showFragment() {
this.viewContainer.createEmbeddedView(this.fragment());
}
}
이렇게 구현하고 사용자가 "Show" 버튼을 클릭하면 DOM은 이렇게 렌더링됩니다:
<component-with-fragment>
<h2>Component with a fragment</h2>
<my-outlet>
<button>Show</button>
</my-outlet>
<p>This is the fragment</p>
</component-with-fragment>
템플릿 조각을 렌더링하면서 인자 전달하기
<ng-template>으로 템플릿 조각을 선언할 때, 변수를 함께 선언할 수 있습니다.
그리고 템플릿 조각을 렌더링하면서 이 변수에 해당하는 context 객체를 전달할 수 있습니다.
그러면 컴포넌트 데이터를 컨텍스트 객체로 전달해서 바인딩 표현식이나 실행문에서 활용할 수 있습니다.
컨텍스트 객체에 선언하는 프로퍼티는 어트리뷰트 이름 앞에 let- 접두사를 붙여 선언합니다:
<ng-template let-pizzaTopping="topping">
<p>You selected: {{ pizzaTopping }}</p>
</ng-template>
NgTemplateOutlet 활용하기
컨텍스트 객체는 ngTemplateOutletContext로 바인딩할 수 있습니다:
<ng-template #myFragment let-pizzaTopping="topping">
<p>You selected: {{ pizzaTopping }}</p>
</ng-template>
<ng-container [ngTemplateOutlet]="myFragment" [ngTemplateOutletContext]="{topping: 'onion'}" />
ViewContainerRef 활용하기
createEmbeddedView의 두 번째 인자로 컨텍스트 객체를 전달할 수 있습니다:
this.viewContainer.createEmbeddedView(this.myFragment, {topping: 'onion'});
템플릿 조각에서 인젝터 활용하기
템플릿 조각을 렌더링하면, 이 템플릿 조각이 렌더링된 위치가 아니라, 템플릿을 선언한 위치 에 있는 인젝터 컨텍스트가 사용됩니다. 커스텀 인젝터를 등록하면 이 인젝터를 오버라이딩할 수 있습니다.
NgTemplateOutlet 활용하기
ngTemplateOutletInjector 입력 프로퍼티로 커스텀 Injector를 전달할 수 있습니다:
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: 'root',
imports: [NgTemplateOutlet, ThemedPanel],
template: `
<ng-template #myFragment>
<themed-panel />
</ng-template>
<ng-container *ngTemplateOutlet="myFragment; injector: customInjector" />
`,
})
export class Root {
customInjector = Injector.create({
providers: [{provide: THEME_DATA, useValue: 'dark'}],
});
}
인젝터 상속하기
ngTemplateOutletInjector에 문자열로 'outlet'을 지정하면 템플릿이 선언된 위치의 인젝터 대신 내장 뷰에 인젝터를 상속할 수 있습니다.
<ng-template #node let-items>
<item-component>
@for (child of items; track $index) {
<ng-container
*ngTemplateOutlet="node; context: {$implicit: child.children}; injector: 'outlet'"
/>
}
</item-component>
</ng-template>
<ng-container *ngTemplateOutlet="node; context: {$implicit: topLevelItems}" />
개별 node 템플릿의 재귀적 렌더링은 이 템플릿을 감싸는 <item-component>의 인젝터를 상속받으므로, 개별 중첩 계층에서는 부모 컴포넌트의 프로바이더에 접근할 수 있습니다.
참고: 이 방식은 재귀적인 구조를 구성하거나 컴포넌트 트리에서 프로바이더에 접근해야 하는 상황에 유용합니다.
ViewContainerRef 활용하기
createEmbeddedView 메서드의 옵션 객체에 커스텀 인젝터를 전달할 수 있습니다:
this.viewContainer.createEmbeddedView(this.myFragment, context, {
injector: myCustomInjector,
});
구조 디렉티브
구조 디렉티브(structural directive) 는:
TemplateRef를 의존성으로 주입받고,ViewContainerRef를 의존성으로 주입받아, 앞서 주입받은TemplateRef를 코드로 렌더링하는 디렉티브입니다.
Angular는 구조 디렉티브를 간편하게 사용할 수 있는 특별한 방법을 제공합니다.
디렉티브를 엘리먼트에 사용할 때 이 디렉티브 셀렉터 앞에 아스테리스크(*) 문자를 추가하면, Angular는 이 엘리먼트 전체를 템플릿 조각으로 다룹니다:
<section *myDirective>
<p>This is a fragment</p>
</section>
이 코드는 아래 코드와 같습니다:
<ng-template myDirective>
<section>
<p>This is a fragment</p>
</section>
</ng-template>
구조 디렉티브는 일반적으로 템플릿 조각을 조건에 따라 렌더링하거나, 여러 번 렌더링하는 방식으로 활용됩니다.
더 자세한 내용은 구조 디렉티브 문서를 참고하세요.
추가 자료
다른 라이브러리에서 ng-template이 어떻게 사용되는지 알아보려면, 이런 내용을 확인해 보세요:
- Angular Material의 탭 - 탭이 활성화되어야 DOM이 렌더링됩니다.
- Angular Material의 표 - 데이터를 다양한 방식으로 렌더링할 수 있습니다.