Skip to content

7. 컨텐츠 프로젝션


이 문서는 컨텐츠 프로젝션을 활용해서 컴포넌트를 유연하게 만들고 재사용할 수 있게 만드는 방법을 설명한다.


이 문서에서 다루는 예제 애플리케이션은 라이브 예제 링크 / 다운로드 링크에서 직접 실행하거나 다운받아 실행할 수 있다.


컨텐츠 프로젝션은 어떤 내용을 다른 컴포넌트 안으로 넣는(project) 개발 패턴이다. 이 패턴을 활용하면 어떤 컴포넌트가 전달하는 컨텐츠를 Card 컴포넌트가 받아서 표시할 수 있다.


Angular가 제공하는 다양한 컨텐츠 프로젝션 구현 방법을 알아보자:



1. 단일 슬롯 컨텐츠 프로젝션

가장 간단한 방법은 단일 슬롯 컨텐츠 프로젝션이다. 단일 슬롯 컨텐츠 프로젝션은 컴포넌트 외부에서 컨텐츠를 하나만 받아 렌더링하는 방식이다.


이렇게 구현한다:


컴포넌트를 생성한다.


컴포넌트 템플릿에 <ng-content> 엘리먼트를 추가한다. 외부에서 받아온 컨텐츠는 이 엘리먼트 안에 렌더링된다.


<ng-content> 엘리먼트 안에 컴포넌트 밖에서 받아온 메시지를 표시한다면 이렇게 구현하면 된다.


content-projection/src/app/zippy-basic/zippy-basic.component.ts
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> 엘리먼트를 추가하고 나면 이 컴포넌트를 사용하면서 컴포넌트 안으로 전달할 메시지를 지정하면 된다. 이렇게 구현한다:


content-projection/src/app/app.component.html
<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개 존재한다.


content-projection/src/app/zippy-multislot/zippy-multislot.component.ts
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] 어트리뷰트가 지정된 엘리먼트가 된다.


content-projection/src/app/app.component.html
<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> 엘리먼트의 내용이 표시된다:


content-projection/src/app/example-zippy.template.html
<ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>


이 예제에서는 조건에 맞는 <ng-template> 엘리먼트를 선택하기 위해 ngTemplateOutlet 디렉티브를 사용했다. ngTemplateOutlet 디렉티브는 모든 엘리먼트에서 사용할 수 있다. 이 예제에서는 실제 DOM 엘리먼트에 렌더링되지 않는 ng-container 엘리먼트에 사용했다.


<ng-container> 엘리먼트를 div와 같은 엘리먼트로 감싸고, 이 엘리먼트에 조건을 지정한다.


content-projection/src/app/example-zippy.template.html
<div *ngIf="expanded" [id]="contentId">
  <ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>
</div>


이제 프로젝션될 컨텐츠는 <ng-template> 안에 이렇게 구성하면 된다:


<ng-template appExampleZippyContent>
  It depends on what you do with it.
</ng-template>


<ng-template> 엘리먼트 안에는 컴포넌트가 받아서 렌더링할 컨텐츠를 추가했다. 컴포넌트는 TemplateRef@ContentChild, @ContentChildren 데코레이터를 사용해서 이 템플릿 컨텐츠를 참조할 수 있다. 이 예제에서는 템플릿을 참조하기 위해 appExampleZippyContent라는 커스텀 디렉티브를 사용했다. 이렇게 <ng-template>을 컴포넌트 클래스에서 TemplateRef 타입으로 참조하면 ngTemplateOutlet 디렉티브나 ViewContainerRef.createEmbeddedView를 사용해서 컨텐츠를 렌더링할 수 있다.


템플릿에 사용할 어트리뷰트 디렉티브를 생성한다. 이 디렉티브에는 TemplateRef 인스턴스를 의존성으로 주입한다.


content-projection/src/app/app.component.ts
@Directive({
  selector: "[appExampleZippyContent]",
})
export class ZippyContentDirective {
  constructor(public templateRef: TemplateRef<unknown>) {}
}


이전 단계에서 <ng-template> 엘리먼트에 커스텀 디렉티브 appExampleZippyDirective를 지정했다. 이 디렉티브 코드는 Angular가 커스텀 어트리뷰트로 템플릿을 참조하기 위해 정의한 것이다.


컨텐츠가 렌더링될 컴포넌트에서 @ContentChild를 사용해서 프로젝션될 컨텐츠 템플릿을 참조한다.


content-projection/src/app/app.component.ts
@ContentChild(ZippyContentDirective) content!: ZippyContentDirective;


여기까지 진행하고 나면 컴포넌트는 특정 조건을 만족하는 템플릿만 인스턴스를 생성한다. 그리고 대상이 되는 템플릿은 커스텀 디렉티브를 지정해 두고 @ComponentChild 데코레이터를 사용해서 컴포넌트 클래스에 할당했다.


Note

  • 다중 슬롯 컨텐츠 프로젝션을 활용하는 경우에 @ContentChildren는 프로젝션된 엘리먼트를 모은 QueryList를 반환한다.


4. 더 복잡한 조건에서 컨텐츠 프로젝션하기

다중 슬롯 컨텐츠 프로젝션에서 언급했던 것처럼 컨텐츠가 프로젝션 될 곳을 지정할 때는 어트리뷰트나 엘리먼트, CSS 클래스, 그리고 이것들을 조합한 방법을 사용할 수 있다. 아래 예제에서 커스텀 어트리뷰트 question가 지정된 <p> 태그가 app-zippy-multislot 컴포넌트 안으로 프로젝션된다고 하자.


content-projection/src/app/app.component.html
<app-zippy-multislot>
  <p question>Is content projection cool?</p>
  <p>Let's learn about content projection!</p>
</app-zippy-multislot>


그런데 이 컨텐츠를 엘리먼트 안에 자식 엘리먼트로 렌더링해야 하는 경우가 있다. 이런 경우에는 ngProjectAs 어트리뷰트를 사용하면 된다.


HTML이 이렇게 구성되었다고 하자:


content-projection/src/app/app.component.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] 셀렉터를 지정한 위치로 프로젝션된다.


References