createComponent는 ngComponentOutlet보다 더 저수준에서 동적 컴포넌트를 제어할 수 있어, 툴팁뿐만 아니라 복잡한 상호작용이 필요한 모달(Modal) 시스템을 구축하는 데 매우 유용합니다.
툴팁과 달리 모달은 더 많은 기능을 고려해야 합니다.
이 예제의 핵심입니다. 모달의 생성, 소멸, 결과 처리를 모두 담당하는 서비스입니다.
// src/app/modal.service.ts
import { Injectable, ViewContainerRef, ComponentRef, Type, inject } from '@angular/core';
import { Subject, Observable } from 'rxjs';
import { first, tap } from 'rxjs/operators';
// 모달 컴포넌트가 구현해야 할 인터페이스
export interface ModalComponent<R> {
result$: Subject<R>;
}
@Injectable({ providedIn: 'root' })
export class ModalService {
private hostVcr?: ViewContainerRef;
private componentRef?: ComponentRef<ModalComponent<any>>;
// 모달이 렌더링될 ViewContainerRef를 등록
registerHost(vcr: ViewContainerRef): void {
this.hostVcr = vcr;
}
// 제네릭을 사용하여 어떤 컴포넌트든 열고, 어떤 타입의 결과든 반환
open<T extends ModalComponent<R>, R>(component: Type<T>): Observable<R> {
if (!this.hostVcr) {
throw new Error('Modal host container is not registered!');
}
// 이전 모달이 있다면 제거 (단일 모달 정책)
this.hostVcr.clear();
const result$ = new Subject<R>();
// 컴포넌트를 동적으로 생성
this.componentRef = this.hostVcr.createComponent(component);
// 생성된 컴포넌트의 인스턴스에 result$ Subject를 주입
this.componentRef.instance.result$ = result$;
// Observable을 반환하고, 첫 번째 결과가 오면 모달을 닫음
return result$.asObservable().pipe(
first(),
tap(() => this.close())
);
}
close(): void {
if (this.componentRef) {
this.componentRef.destroy();
this.componentRef = undefined;
}
}
}
애플리케이션의 최상단에서 모달이 렌더링될 위치를 ModalService에 알려줍니다.
// src/app/app.component.ts
import { Component, ViewChild, ViewContainerRef, AfterViewInit, inject } from '@angular/core';
import { ContainerComponent } from './container.component'; // 예제 호출 컴포넌트
import { ModalService } from './modal.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [ContainerComponent],
template: `
<app-container></app-container>
<!-- 모든 모달은 이 위치에 렌더링됩니다 -->
<ng-container #modalHost></ng-container>
`,
})
export class AppComponent implements AfterViewInit {
@ViewChild('modalHost', { read: ViewContainerRef })
modalHostVcr!: ViewContainerRef;
private modalService = inject(ModalService);
ngAfterViewInit(): void {
// 서비스에 호스트 ViewContainerRef를 등록
this.modalService.registerHost(this.modalHostVcr);
}
}
ModalComponent 인터페이스를 구현하여 result$ 프로퍼티를 가집니다.
// src/app/a-modal.component.ts
import { Component, Input } from '@angular/core';
import { Subject } from 'rxjs';
import { ModalComponent } from './modal.service';
@Component({
standalone: true,
template: `
<div class="modal">
<p>Modal A: Are you sure?</p>
<button (click)="onConfirm()">확인</button>
<button (click)="onCancel()">취소</button>
</div>
`,
styles: `.modal { border: 1px solid blue; padding: 20px; background: white; }`
})
export class AModalComponent implements ModalComponent<boolean> {
result$!: Subject<boolean>;
onConfirm(): void {
this.result$.next(true);
this.result$.complete();
}
onCancel(): void {
this.result$.next(false);
this.result$.complete();
}
}
(BModalComponent도 유사하게 작성합니다.)
이제 모달을 열고 싶은 컴포넌트에서는 ModalService만 주입받으면 됩니다.
// src/app/container.component.ts
import { Component, inject, signal } from '@angular/core';
import { ModalService } from './modal.service';
import { AModalComponent } from './a-modal.component';
import { BModalComponent } from './b-modal.component';
@Component({
selector: 'app-container',
standalone: true,
template: `
<button (click)="openModal('A')">Show Modal A</button>
<button (click)="openModal('B')">Show Modal B</button>
<p>Last Modal Result: {{ modalResult() }}</p>
`
})
export class ContainerComponent {
private modalService = inject(ModalService);
modalResult = signal<any>('N/A');
openModal(type: 'A' | 'B'): void {
const modalToOpen = type === 'A' ? AModalComponent : BModalComponent;
this.modalService.open(modalToOpen).subscribe(result => {
// 모달이 닫히고 반환된 결과를 Signal에 저장
this.modalResult.set(result);
});
}
}
이 아키텍처는 Angular의 DI 시스템을 활용하여 매우 깔끔하고 재사용 가능한 모달 시스템의 기반을 제공합니다.