심화 가이드
템플릿

템플릿 조각 만들기: <ng-template>

<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)은 템플릿 조각이 렌더링되는 곳이 아니라, 템플릿 조각이 선언된 컴포넌트의 컨텍스트와 연결됩니다.

템플릿 조각 참조하기

템플릿 조각을 참조하는 방법은 세 가지입니다:

위 세 방법 모두 템플릿 조각은 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) 는:

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이 어떻게 사용되는지 알아보려면, 이런 내용을 확인해 보세요: