Angular로 컴포넌트를 설계하다 보면 종종 부모와 자식 간의 책임 분리 문제에 부딪히게 됩니다. 특히, "데이터와 로직은 자식 컴포넌트에 있지만, 그 결과물은 부모 컴포넌트의 특정 위치에 렌더링되어야 할 때"가 그렇습니다.
이번 글에서는 exportAs라는 다소 생소할 수 있는 기능을 활용하여, 자식 컴포넌트가 정의한 템플릿을 부모 컴포넌트에서 아주 깔끔하고 선언적으로 렌더링하는 방법을 알아보겠습니다.
두 가지 흔한 시나리오를 생각해 봅시다.
<app-window> 컴포넌트가 있고, 그 안에는 다양한 내용의 자식 컴포넌트가 들어갈 수 있습니다. 이때 윈도우 상단의 메뉴 아이템은 자식 컴포넌트의 종류나 상태에 따라 결정되어야 합니다.<app-list> 컴포넌트는 여러 <app-list-item> 을 렌더링합니다. 각 아이템은 자신의 상태(예: '편집 중', '승인 대기')에 따라 '저장', '삭제', '승인' 등 각기 다른 액션 버튼을 가져야 하고, 이 버튼들은 목록의 특정 영역(예: 헤더나 푸터)에 표시되어야 합니다.이 두 경우 모두, 렌더링할 내용을 가장 잘 아는 것은 자식 컴포넌트이지만, 렌더링이 일어날 위치는 부모 컴포넌트의 DOM 구조에 속해있습니다. 어떻게 이 둘을 자연스럽게 연결할 수 있을까요?
해결의 열쇠는 @Component 데코레이터의 exportAs 프로퍼티에 있습니다. 이 프로퍼티는 템플릿 내에서 해당 컴포넌트의 인스턴스를 참조할 수 있는 '별명'을 부여하는 역할을 합니다.
먼저, 자식 컴포넌트는 부모에게 전달할 템플릿을 으로 정의하고, @ViewChild를 통해 클래스의 공개(public) 프로퍼티로 만듭니다.
// child.component.ts
import { Component, ViewChild, TemplateRef } from '@angular/core';
@Component({
selector: 'app-child',
standalone: true,
exportAs: 'smartChild', // 👈 템플릿에서 'smartChild'라는 별명으로 인스턴스를 참조할 수 있게 함
template: `
<div>자식 컴포넌트의 고유 콘텐츠</div>
<!-- 부모에게 전달하고 싶은 템플릿 -->
<ng-template #menuItems>
<button>아이템 1</button>
<button>아이템 2</button>
</ng-template>
`,
})
export class ChildComponent {
// 템플릿(#menuItems)을 클래스의 공개 프로퍼티(menuItems)로 가져옴
@ViewChild('menuItems', { static: true })
menuItems!: TemplateRef<any>;
}
핵심 포인트:
@ViewChild('menuItems') menuItems: 템플릿 내의 #menuItems를 menuItems라는 클래스 프로퍼티에 할당합니다. 이 프로퍼티는 public이므로 외부(부모)에서 접근 가능합니다.이제 부모 컴포넌트는 자식의 인스턴스를 변수로 받은 뒤, 해당 인스턴스의 공개 프로퍼티에 접근하여 템플릿을 원하는 위치에 렌더링합니다.
// parent.component.ts
import { Component } from '@angular/core';
import { NgTemplateOutlet } from '@angular/common';
import { ChildComponent } from './child.component';
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent, NgTemplateOutlet],
template: `
<div class="parent-layout">
<header class="parent-header">
<h2>부모 컴포넌트 영역</h2>
<div class="menu-bar">
<!-- 💡 자식의 템플릿이 렌더링될 위치 -->
<ng-container [ngTemplateOutlet]="child.menuItems"></ng-container>
</div>
</header>
<main>
<!-- 자식 컴포넌트를 렌더링하고, 인스턴스를 #child 변수에 할당 -->
<app-child #child="smartChild"></app-child>
</main>
</div>
`,
})
export class ParentComponent {}
동작 원리 분석:
<app-child #child="smartChild"></app-child>: <app-child> 컴포넌트를 생성하고, exportAs에 정의된 별명(smartChild)을 사용하여 ChildComponent의 인스턴스를 child라는 템플릿 변수에 담습니다.<ng-container [ngTemplateOutlet]="child.menuItems"></ng-container>: 부모 템플릿의 다른 위치에서 ngTemplateOutlet 디렉티브를 사용합니다.[ngTemplateOutlet]="child.menuItems": ngTemplateOutlet에 렌더링할 템플릿으로 child 변수(자식 컴포넌트 인스턴스)의 menuItems 프로퍼티를 전달합니다. menuItems는 자식 컴포넌트에서 @ViewChild 로 가져온 바로 그 <ng-template> 입니다.exportAs는 부모와 자식 컴포넌트 간의 결합도를 높이지 않으면서도 템플릿 렌더링의 제어권을 유연하게 주고받을 수 있는 강력한 방법입니다.
@Output 으로 TemplateRef를 이벤트로 보내거나 복잡한 서비스를 사용하는 대신, 템플릿 내에서 모든 관계를 선언적으로 정의할 수 있어 코드가 훨씬 직관적이고 깔끔해집니다. 이 기법을 활용하면 더욱 유연하고 재사용 가능한 컴포넌트 구조를 설계할 수 있습니다.