ngComponentOutlet으로 동적 컴포넌트 만들기

Adam Kim·2025년 10월 10일
0

angular

목록 보기
49/88

@if를 활용하여 조건에 맞는 컴포넌트만 노출하기

일반적으로 템플릿에서 조건에 따라 표시할 컴포넌트를 동적으로 교체하는 것은 까다로울 수 있습니다. 전통적으로는 여러 컴포넌트를 템플릿에 나열하고, @if나 @switch 같은 조건부 렌더링 구문을 사용해 원하는 컴포넌트만 노출하는 방법을 사용했습니다.

@switch (type()) {
  @case (TYPEA) { <component-a /> }
  @case (TYPEB) { <component-b /> }
  @case (TYPEC) { <component-c /> }
  @case (TYPED) { <component-d /> }
}

이러한 방식은 템플릿에서 직관적으로 렌더링될 컴포넌트를 파악할 수 있는 장점이 있지만, 조건이 많아질수록 템플릿 코드가 복잡해지고 지저분해진다는 단점이 있습니다.

ngComponentOutlet으로 동적으로 컴포넌트 렌더링하기

ngComponentOutlet은 컴포넌트 클래스(TS 코드) 내에서 템플릿에 렌더링할 컴포넌트를 동적으로 선택하고 교체할 수 있게 해주는 강력한 디렉티브입니다.

사용법은 간단합니다. 템플릿에서 컴포넌트가 표시될 위치에 *ngComponentOutlet을 선언하고, 렌더링할 컴포넌트 클래스를 바인딩하면 됩니다.

// app.component.ts
import { Component, signal, Type } from '@angular/core';
import { CommonModule } from '@angular/common'; // NgComponentOutlet을 위함

@Component({ selector: 'a-component', standalone: true, template: `<p>Hello World from A</p>` })
export class AComponent {}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, AComponent],
  template: `
    <ng-container *ngComponentOutlet="component()"></ng-container>
  `
})
export class AppComponent {
  component = signal<Type<any>>(AComponent);
}

위 코드에서는 AppComponent의 component Signal이 AComponent를 가리키고 있으므로, 위치에 AComponent가 렌더링됩니다.

여러 컴포넌트 중 선택하기

ngComponentOutlet을 사용하면 버튼 클릭과 같은 사용자 상호작용에 따라 렌더링될 컴포넌트를 쉽게 교체할 수 있습니다.

// app.component.ts
import { Component, signal, Type } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({ selector: 'a-component', standalone: true, template: `<p>Hello World</p>` })
export class AComponent {}
@Component({ selector: 'b-component', standalone: true, template: `<p>Good bye</p>` })
export class BComponent {}

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, AComponent, BComponent],
  template: `
    <ng-container *ngComponentOutlet="currentComponent()"></ng-container>
    <button (click)="changeComponent()">Change Component</button>
  `
})
export class AppComponent {
  isA = signal(true);
  currentComponent = signal<Type<any>>(AComponent);

  changeComponent() {
    this.isA.update(val => !val);
    this.currentComponent.set(this.isA() ? AComponent : BComponent);
  }
}

값을 주입하는 방법

ngComponentOutlet의 강력한 기능 중 하나는 동적으로 렌더링되는 컴포넌트의 종류와 상관없이 동일한 방식으로 데이터를 주입할 수 있다는 점입니다. 데이터는 Injector를 통해, DOM 노드는 content를 통해 주입할 수 있습니다.

Injector 활용하기

Injector를 사용하면 @Input 없이도 동적으로 생성되는 자식 컴포넌트에 값을 주입할 수 있습니다. InjectionToken을 사용하는 것이 일반적입니다.

// title.token.ts
import { InjectionToken } from '@angular/core';
export const TITLE = new InjectionToken<string>('app.title');

// child.component.ts
import { Component, inject } from '@angular/core';
import { TITLE } from './title.token';

@Component({
  standalone: true,
  template: `Complete: {{ titleInjected }}`
})
export class ChildComponent {
  titleInjected = inject(TITLE);
}

// app.component.ts
import { Component, Injector, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ChildComponent } from './child.component';
import { TITLE } from './title.token';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule],
  template: `
    <ng-container *ngComponentOutlet="ChildComponent; injector: myInjector"></ng-container>
  `
})
export class AppComponent {
  ChildComponent = ChildComponent; // 템플릿에서 사용하기 위해
  myInjector: Injector;

  constructor() {
    // 부모 인젝터를 상속받는 새로운 인젝터 생성
    this.myInjector = Injector.create({
      providers: [{ provide: TITLE, useValue: 'hello world from injector' }],
      parent: inject(Injector) // 현재 인젝터를 부모로 설정
    });
  }
}

이 방식은 useValue뿐만 아니라 useClass, useFactory 등 다양한 Provider를 동적으로 주입할 수 있어 매우 유연합니다.

Content 활용하기

content 옵션은 동적 컴포넌트 내의 슬롯에 DOM 노드를 주입(Project)할 수 있게 해줍니다. document.createElement나 document.createTextNode를 사용하여 생성한 노드를 전달할 수 있습니다.
주의: content에 전달되는 값은 반드시 2차원 배열 (Node[][]) 형태여야 합니다.

// child.component.ts (ng-content 사용)
@Component({
  standalone: true,
  template: `Complete: <ng-content></ng-content>`
})
export class ChildComponent {}

// app.component.ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule],
  template: `
    <ng-container *ngComponentOutlet="ChildComponent; content: myContent"></ng-container>
  `
})
export class AppComponent {
  ChildComponent = ChildComponent;
  myContent: Node[][];

  constructor() {
    const button = document.createElement('button');
    button.textContent = 'Click me from parent';
    button.onclick = () => alert('Button clicked!');
    
    // 2차원 배열 형태로 노드를 전달
    this.myContent = [[button]];
  }
}

이처럼 부모 컴포넌트에서 생성하고 이벤트를 제어하는 DOM 요소를 자식에게 동적으로 전달할 수 있습니다.

연결된 컴포넌트 제거하기

ngComponentOutlet에 바인딩된 컴포넌트 변수에 null을 할당하면 렌더링된 컴포넌트를 제거할 수 있습니다.

// app.component.ts
// ... imports
export class AppComponent {
  // Type<any> | null 유니온 타입을 사용
  currentComponent = signal<Type<any> | null>(AComponent);

  removeComponent() {
    this.currentComponent.set(null);
  }
}
profile
Angular2+ Developer

0개의 댓글