7. 컨텐츠 프로젝션
이 문서는 컨텐츠 프로젝션을 활용해서 컴포넌트를 유연하게 만들고 재사용할 수 있게 만드는 방법을 설명한다.
이 문서에서 다루는 예제 애플리케이션은 라이브 예제 링크 / 다운로드 링크에서 직접 실행하거나 다운받아 실행할 수 있다.
컨텐츠 프로젝션은 어떤 내용을 다른 컴포넌트 안으로 넣는(project) 개발 패턴이다. 이 패턴을 활용하면 어떤 컴포넌트가 전달하는 컨텐츠를 Card
컴포넌트가 받아서 표시할 수 있다.
Angular가 제공하는 다양한 컨텐츠 프로젝션 구현 방법을 알아보자:
- 단일 슬롯 컨텐츠 프로젝션: 컴포넌트 외부에서 컨텐츠를 하나만 받는다.
- 다중 슬롯 컨텐츠 프로젝션: 컴포넌트 외부에서 컨텐츠를 여러 개 받는다.
- 조건별 컨텐츠 프로젝션: 특정 조건에 맞는 컨텐츠만 프로젝션해서 렌더링한다.
1. 단일 슬롯 컨텐츠 프로젝션
가장 간단한 방법은 단일 슬롯 컨텐츠 프로젝션이다. 단일 슬롯 컨텐츠 프로젝션은 컴포넌트 외부에서 컨텐츠를 하나만 받아 렌더링하는 방식이다.
이렇게 구현한다:
컴포넌트를 생성한다.
컴포넌트 템플릿에 <ng-content>
엘리먼트를 추가한다. 외부에서 받아온 컨텐츠는 이 엘리먼트 안에 렌더링된다.
<ng-content>
엘리먼트 안에 컴포넌트 밖에서 받아온 메시지를 표시한다면 이렇게 구현하면 된다.
import { Component } from "@angular/core";
@Component({
selector: "app-zippy-basic",
template: `
<h2>Single-slot content projection</h2>
<ng-content></ng-content>
`,
})
export class ZippyBasicComponent {}
<ng-content>
엘리먼트를 추가하고 나면 이 컴포넌트를 사용하면서 컴포넌트 안으로 전달할 메시지를 지정하면 된다. 이렇게 구현한다:
<app-zippy-basic>
<p>Is content projection cool?</p>
</app-zippy-basic>
Note
<ng-content>
엘리먼트는 컨텐츠가 표시될 위치만 지정하는 엘리먼트이며, DOM 트리에 실제로 생성되는 DOM 엘리먼트는 아니다.<ng-content>
에 사용된 커스텀 어트리뷰트는 무시된다.
2. 다중 슬롯 컨텐츠 프로젝션
컴포넌트에는 슬롯을 여러 개 둘 수도 있다. 개별 슬롯에 CSS 셀렉터를 지정하면 이 셀렉터를 사용해서 컨텐츠가 들어갈 슬롯을 결정할 수 있다. 이런 방식을 다중 슬롯 컨텐츠 프로젝션이라고 한다. 이 방식을 사용하려면 <ng-content>
에 select
어트리뷰트를 지정해서 컨텐츠가 들어갈 위치를 정확하게 지정해야 한다.
이렇게 구현한다:
컴포넌트를 생성한다.
컴포넌트 템플릿에 ng-content
엘리먼트를 추가한다. 외부에서 받아온 컨텐츠는 이 엘리먼트 안에 렌더링된다.
ng-content
엘리먼트에 select
어트리뷰트를 추가한다. Angular는 태그 이름, 어트리뷰트, CSS 클래스, :not
과 같은 가상 클래스, 그리고 이들의 조합을 모두 셀렉터로 지원한다.
아래 컴포넌트에는 ng-content 엘리먼트가 2개 존재한다.
import { Component } from "@angular/core";
@Component({
selector: "app-zippy-multislot",
template: `
<h2>Multi-slot content projection</h2>
Default:
<ng-content></ng-content>
Question:
<ng-content select="[question]"></ng-content>
`,
})
export class ZippyMultislotComponent {}
그러면 question
어트리뷰트가 지정된 컨텐츠가 렌더링되는 ng-content
엘리먼트는 select=[question]
어트리뷰트가 지정된 엘리먼트가 된다.
<app-zippy-multislot>
<p question>Is content projection cool?</p>
<p>Let's learn about content projection!</p>
</app-zippy-multislot>
Note
- 컴포넌트에
select
가 지정되지 않은ng-content
엘리먼트가 존재하면, 이 엘리먼트에는 렌더링될ng-content
엘리먼트가 지정되지 않은 모든 컨텐츠가 프로젝션된다. - 이 예제에서는 두 번째
ng-content
엘리먼트에만select
어트리뷰트가 선언되었기 때문에, 첫 번째ng-content
엘리먼트에는 렌더링될 위치가 지정되지 않은 컨텐츠가 모두 프로젝션된다.
3. 조건별 컨텐츠 프로젝션
컴포넌트가 조건에 따라 컨텐츠를 렌더링해야 하거나 한 컨텐츠를 여러 번 렌더링해야 한다면 컴포넌트에 <ng-template>
엘리먼트를 사용해서 렌더링 조건을 지정할 수 있다.
컨텐츠 프로젝션을 이런 용도로 사용하는 경우에는 <ng-content>
를 권장하지 않는다. 왜냐하면 컨텐츠를 받아서 표시하는 컴포넌트 입장에서는 이 컨텐츠가 반드시 초기화가 끝난 상태여야 하는데, 이런 제약은 <ng-content>
에 *ngIf
가 적용되었거나 *ngIf
구문 안쪽으로 <ng-content>
엘리먼트가 있는 경우에도 해당되기 때문이다.
<ng-template>
엘리먼트를 사용하면 자유로운 조건으로 렌더링될 컨텐츠를 지정할 수 있으며, 한 컨텐츠를 여러 번 렌더링할 수도 있다. <ng-template>
안에 있는 컨텐츠는 실제로 렌더링되는 시점에 Angular가 초기화한다.
이렇게 구현한다:
컴포넌트를 생성한다.
컴포넌트 템플릿에 <ng-container>
엘리먼트를 추가한다. 이 엘리먼트에는 <ng-template>
엘리먼트의 내용이 표시된다:
<ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>
이 예제에서는 조건에 맞는 <ng-template>
엘리먼트를 선택하기 위해 ngTemplateOutlet
디렉티브를 사용했다. ngTemplateOutlet
디렉티브는 모든 엘리먼트에서 사용할 수 있다. 이 예제에서는 실제 DOM 엘리먼트에 렌더링되지 않는 ng-container
엘리먼트에 사용했다.
<ng-container>
엘리먼트를 div
와 같은 엘리먼트로 감싸고, 이 엘리먼트에 조건을 지정한다.
<div *ngIf="expanded" [id]="contentId">
<ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>
</div>
이제 프로젝션될 컨텐츠는 <ng-template>
안에 이렇게 구성하면 된다:
<ng-template>
엘리먼트 안에는 컴포넌트가 받아서 렌더링할 컨텐츠를 추가했다. 컴포넌트는 TemplateRef
나 @ContentChild
, @ContentChildren
데코레이터를 사용해서 이 템플릿 컨텐츠를 참조할 수 있다. 이 예제에서는 템플릿을 참조하기 위해 appExampleZippyContent
라는 커스텀 디렉티브를 사용했다. 이렇게 <ng-template>
을 컴포넌트 클래스에서 TemplateRef
타입으로 참조하면 ngTemplateOutlet
디렉티브나 ViewContainerRef.createEmbeddedView
를 사용해서 컨텐츠를 렌더링할 수 있다.
템플릿에 사용할 어트리뷰트 디렉티브를 생성한다. 이 디렉티브에는 TemplateRef
인스턴스를 의존성으로 주입한다.
@Directive({
selector: "[appExampleZippyContent]",
})
export class ZippyContentDirective {
constructor(public templateRef: TemplateRef<unknown>) {}
}
이전 단계에서 <ng-template>
엘리먼트에 커스텀 디렉티브 appExampleZippyDirective
를 지정했다. 이 디렉티브 코드는 Angular가 커스텀 어트리뷰트로 템플릿을 참조하기 위해 정의한 것이다.
컨텐츠가 렌더링될 컴포넌트에서 @ContentChild
를 사용해서 프로젝션될 컨텐츠 템플릿을 참조한다.
@ContentChild(ZippyContentDirective) content!: ZippyContentDirective;
여기까지 진행하고 나면 컴포넌트는 특정 조건을 만족하는 템플릿만 인스턴스를 생성한다. 그리고 대상이 되는 템플릿은 커스텀 디렉티브를 지정해 두고 @ComponentChild
데코레이터를 사용해서 컴포넌트 클래스에 할당했다.
Note
- 다중 슬롯 컨텐츠 프로젝션을 활용하는 경우에
@ContentChildren
는 프로젝션된 엘리먼트를 모은 QueryList를 반환한다.
4. 더 복잡한 조건에서 컨텐츠 프로젝션하기
다중 슬롯 컨텐츠 프로젝션에서 언급했던 것처럼 컨텐츠가 프로젝션 될 곳을 지정할 때는 어트리뷰트나 엘리먼트, CSS 클래스, 그리고 이것들을 조합한 방법을 사용할 수 있다. 아래 예제에서 커스텀 어트리뷰트 question
가 지정된 <p>
태그가 app-zippy-multislot
컴포넌트 안으로 프로젝션된다고 하자.
<app-zippy-multislot>
<p question>Is content projection cool?</p>
<p>Let's learn about content projection!</p>
</app-zippy-multislot>
그런데 이 컨텐츠를 엘리먼트 안에 자식 엘리먼트로 렌더링해야 하는 경우가 있다. 이런 경우에는 ngProjectAs
어트리뷰트를 사용하면 된다.
HTML이 이렇게 구성되었다고 하자:
<ng-container ngProjectAs="[question]">
<p>Is content projection cool?</p>
</ng-container>
이 예제에서는 DOM에 실제로 렌더링되지 않는 <ng-container>
어트리뷰트를 사용했다.
Note
<ng-container>
엘리먼트는 DOM 엘리먼트를 그룹으로 묶을 때 사용하는 논리 엘리먼트이다.- DOM 트리에는 실제로 렌더링되지 않는다.
이렇게 구성하면 <ng-container>
안에 있는 컨텐츠가 컴포넌트 안에 있는 엘리먼트 안으로 프로젝션된다. ngProjectAs
어트리뷰트는 이 동작을 하기 위해 지정되었다. ngProjectAs
어트리뷰트를 지정하면 <ng-container>
엘리먼트의 전체 내용이 [question]
셀렉터를 지정한 위치로 프로젝션된다.