팁: 이 가이드 문서는 핵심 가이드 이후 내용을 다룹니다. 아직 Angular에 익숙하지 않다면 해당 문서를 먼저 읽어보세요.
때로는 내용물을 다양하게 다루는 컨테이너 용도로 컴포넌트를 생성하는 경우가 있습니다. 커스텀 카드 컴포넌트 같은 경우가 그렇습니다:
@Component({
selector: 'custom-card',
template: '<div class="card-shadow"> <!-- 카드 내용은 여기에 들어갑니다. --> </div>',
})
export class CustomCard {
/* ... */
}
이때 내용물이 들어갈 위치는 <ng-content> 엘리먼트로 지정합니다:
@Component({
selector: 'custom-card',
template: '<div class="card-shadow"> <ng-content/> </div>',
})
export class CustomCard {
/* ... */
}
팁: <ng-content>는 네이티브 <slot> 엘리먼트와 비슷하게 동작하지만, Angular용 기능이 일부 추가되었습니다.
컴포넌트에 <ng-content>를 사용하면, 이 컴포넌트의 자식 컴포넌트나 프로젝션(projected) 되는 내용물은 모두 <ng-content> 엘리먼트 위치에 렌더링됩니다:
// 컴포넌트 소스
@Component({
selector: 'custom-card',
template: `
<div class="card-shadow">
<ng-content />
</div>
`,
})
export class CustomCard {
/* ... */
}
<!-- 컴포넌트를 사용하는 경우 -->
<custom-card>
<p>This is the projected content</p>
</custom-card>
<!-- 렌더링 된 DOM -->
<custom-card>
<div class="card-shadow">
<p>This is the projected content</p>
</div>
</custom-card>
컴포넌트 밖에서 컴포넌트 안으로 전달되는 모든 내용을 컴포넌트의 내용물(content) 라고 합니다. 컴포넌트 템플릿에 존재하는 엘리먼트를 의미하는 뷰(view) 와는 다릅니다.
중요: <ng-content> 엘리먼트는 컴포넌트도 아니고 DOM 엘리먼트도 아닙니다.
이 엘리먼트는 Angular에게 프로젝션되는 내용물의 위치를 지정하는 용도로만 사용되는 특별한 플레이스홀더(placeholder)입니다.
Angular 컴파일러는 <ng-content> 엘리먼트를 빌드 시점에 처리합니다.
<ng-content>는 실행 시점에 추가하거나, 제거하거나, 수정할 수 없습니다.
그리고 <ng-content>에 디렉티브를 적용하거나, 스타일, 어트리뷰트를 추가할 수도 없습니다.
<ng-content>는 @if나 @for, @switch로 제어할 수 없습니다.
Angular는 언제나 <ng-content> 플레이스홀더가 위치한 곳에 내용물을 렌더링하거나 DOM 노드를 생성하는데, <ng-content> 플레이스홀더가 화면에 표시되지 않는 경우도 그렇습니다.
컴포넌트 내용물을 조건부로 렌더링하려면 템플릿 조각(Template fragments)을 참고하세요.
플레이스홀더 여러 개 사용하기
<ng-content> 플레이스홀더에 CSS 셀렉터를 지정하면 내용물을 여러 개 전달할 수 있습니다.
위에서 다룬 카드 예제를 다시 보자면, select 어트리뷰트로 플레이스홀더를 구분하면 됩니다:
@Component({
selector: 'card-title',
template: `<ng-content>card-title</ng-content>`,
})
export class CardTitle {}
@Component({
selector: 'card-body',
template: `<ng-content>card-body</ng-content>`,
})
export class CardBody {}
<!-- 컴포넌트 템플릿 -->
@Component({
selector: 'custom-card',
template: `
<div class="card-shadow">
<ng-content select="card-title" />
<div class="card-divider"></div>
<ng-content select="card-body" />
</div>
`,
})
export class CustomCard {}
<!-- 컴포넌트 사용 코드 -->
@Component({
selector: 'app-root',
imports: [CustomCard, CardTitle, CardBody],
template: `
<custom-card>
<card-title>Hello</card-title>
<card-body>Welcome to the example</card-body>
</custom-card>
`,
})
export class App {}
<!-- 렌더링 된 DOM -->
<custom-card>
<div class="card-shadow">
<card-title>Hello</card-title>
<div class="card-divider"></div>
<card-body>Welcome to the example</card-body>
</div>
</custom-card>
<ng-content> 플레이스홀더에 사용하는 CSS 셀렉터는 컴포넌트 셀렉터로 지정하는 방식과 같습니다.
중요: 어트리뷰트 셀렉터나 클래스 셀렉터를 사용하려면 class="within"처럼 어트리뷰트나 클래스를 정적으로 설정해야 합니다.
[class.within]="isWithin()"처럼 동적으로 바인딩한 값은 select 셀렉터가 인식하지 못합니다.
<ng-content> 를 하나 이상 사용할 때 select 어트리뷰트를 지정한 <ng-content> 외에 select 어트리뷰트를 사용하지 않는 <ng-content>를 사용할 수 있는데, 이 경우는 select 어트리뷰트가 매칭되지 않는 모든 엘리먼트와 매칭됩니다:
<!-- 컴포넌트 템플릿 -->
<div class="card-shadow">
<ng-content select="card-title" />
<div class="card-divider"></div>
<!-- "card-title"이 아닌 모든 엘리먼트가 표시됩니다. -->
<ng-content />
</div>
<!-- 컴포넌트 사용 코드 -->
<custom-card>
<card-title>Hello</card-title>
<img src="..." />
<p>Welcome to the example</p>
</custom-card>
<!-- 렌더링 된 DOM -->
<custom-card>
<div class="card-shadow">
<card-title>Hello</card-title>
<div class="card-divider"></div>
<img src="..." />
<p>Welcome to the example</p>
</div>
</custom-card>
컴포넌트에 select 어트리뷰트가 지정되지 않은 <ng-content> 플레이스홀더가 없다면, 플레이스홀더가 매칭되지 않는 엘리먼트는 DOM에 렌더링되지 않습니다.
폴백 컨텐츠
컴포넌트에 매칭되는 자식 컨텐츠가 없다면, 컴포넌트 <ng-content>에 표시되는 폴백 컨텐츠(fallback content) 를 표시할 수 있습니다.
<ng-content> 엘리먼트 안에 내용을 구성하면 됩니다.
<!-- 컴포넌트 템플릿 -->
<div class="card-shadow">
<ng-content select="card-title">Default Title</ng-content>
<div class="card-divider"></div>
<ng-content select="card-body">Default Body</ng-content>
</div>
<!-- 컴포넌트 사용 코드 -->
<custom-card>
<card-title>Hello</card-title>
<!-- card-body 가 없는 경우 -->
</custom-card>
<!-- 렌더링 된 DOM -->
<custom-card>
<div class="card-shadow">
<card-title>Hello</card-title>
<div class="card-divider"></div>
Default Body
</div>
</custom-card>
별칭으로 프로젝션하기
Angular는 모든 엘리먼트에 CSS 셀렉터를 지정할 수 있도록 ngProjectAs 라는 특별한 어트리뷰트를 지원합니다.
ngProjectAs가 지정된 엘리먼트를 <ng-content> 플레이스홀더와 매칭할 때, Angular는 엘리먼트 자체의 셀렉터 대신 ngProjectAs 값을 비교합니다:
<!-- 컴포넌트 템플릿 -->
<div class="card-shadow">
<ng-content select="card-title" />
<div class="card-divider"></div>
<ng-content />
</div>
<!-- 컴포넌트 사용 코드 -->
<custom-card>
<h3 ngProjectAs="card-title">Hello</h3>
<p>Welcome to the example</p>
</custom-card>
<!-- 렌더링 된 DOM -->
<custom-card>
<div class="card-shadow">
<h3>Hello</h3>
<div class="card-divider"></div>
<p>Welcome to the example</p>
</div>
</custom-card>
ngProjectAs 는 정적 값만 지원하며, 동적 표현식에 바인딩할 수는 없습니다.
주의사항
프로젝션된 컨텐츠는 부모 뷰에 존재합니다.
어떤 컴포넌트가 컨텐츠를 받아서 프로젝션 안에 렌더링하더라도, 프로젝션된 컨텐츠는 여전히 이 컨텐츠를 정의한 컴포넌트가 주인입니다. Angular는 부모 뷰를 계속 추적하는데, 이때 알아야 할 부수 효과가 몇 가지 있습니다.
변경 감지: 프로젝션된 컨텐츠는 부모 컴포넌트의 변경 감지가 동작할 때 함께 검사됩니다.
그래서 컨텐츠를 받는 컴포넌트가 OnPush를 사용하면 Angular는 이 컴포넌트의 템플릿은 변경 감지를 건너뛰지만, 프로젝션된 컨텐츠는 부모 컴포넌트에 속하기 때문에 검사가 진행됩니다.
<!-- 부모 템플릿 (기본 변화 감지) -->
<onpush-wrapper>
<!-- 모든 부모 싸이클마다 확인합니다. OnPush는 적용되지 않습니다. -->
<expensive-component />
</onpush-wrapper>
의존성 주입: 프로젝션된 컨텐츠는 부모 인젝터로 의존성 객체를 받습니다.
컨텐츠를 받는 컴포넌트의 viewProviders를 참조하지 않습니다.
자세한 내용은 프로바이더와 viewProviders를 참고하세요.
일부 라이브러리 컴포넌트는 프로젝션된 자식 엘리먼트를 지원하지 않습니다.
메뉴, 탭, 리스트와 같은 컴포넌트는 ContentChildren을 활용해서 자식 컴포넌트를 찾고 키보드 네비게이션이나 포커스 관리, ARIA 어트리뷰트를 연결합니다.
그런데 이런 컴포넌트는 자식 요소를 직접 소유한다고 가정하고 구현되기 때문에, 외부에서 컨텐츠를 프로젝션하면 문제가 생길 수 있습니다.
예를 들면, <mat-menu-item> 엘리먼트를 추가 레이어로 감싸고 <mat-menu>에 프로젝션하면, 키보드 네비게이션이나 스크린 리더 지원이 동작하지 않습니다.
쿼리는 여전히 자식 항목을 찾겠지만, 프로젝션되는 항목이 다른 뷰 컨텍스트에서 오면 상호작용이 제대로 동작하지 않을 수 있습니다.
라이브러리 컴포넌트가 자식 컴포넌트의 동작을 관리한다면, 컨텐츠 프로젝션을 지원하는지 문서를 먼저 확인해 보세요.