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

Adam Kim·2025년 10월 10일
0

angular

목록 보기
51/88

보다 범용적인 버전에서 활용할 수 있는 createComponent를 사용하여 dynamic component를 구현합니다.

기본 원리

  1. componentFactory를 활용하여 호출할 component의 정보를 가져옵니다.
  2. component를 담을 container component의 viewContainerRef를 가져옵니다.
  3. viewContainerRefcreateComponent 함수를 통해 호출할 component를 container component에 랜더링 합니다.
  4. viewContainerRefclear 함수를 활용하여 호출한 component를 제거 합니다.

component 생성하기

container component

dynamic component를 호출하는 container에 해당하는 component를 작성해 봅시다.

버튼 2개를 클릭했을 때 서로 다른 component가 화면에 출력되는 예제 입니다.

@Component({
  selector: 'app-container',
  template: `
  <button (click)="callAComponent()">Show A-Component</button>
  <button (click)="callBComponent()">Show B-Component</button>
  `
})
export class ContainerComponent {
  constructor(
    private viewContainerRef: ViewContainerRef,
    private resolver: ComponentFactoryResolver
  ) {}

  callAComponent() {
    const resolve = this.resolver.resolveComponentFactory(AComponent);
    this.viewContainerRef.crateComponent(resolve);
  } 
  callBComponent() {
    const resolve = this.resolver.resolveComponentFactory(BComponent);
    this.viewContainerRef.crateComponent(resolve);
  }
}

dynamic components

호출할 두개의 component를 작성해봅시다. css를 작성하면 보다 확실히 확인할 수 있으나 여기에서는 생략합니다.

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

}

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

}

예제를 실행하면 버튼을 클릭하여 AComponent, BComponent를 스위치하여 화면에 표시합니다.

값을 주입하는 방법

값을 주입하는 방법은 매우 간단합니다. createComponent의 리턴 값을 받아 instance에 값을 주입하면 됩니다.

여기에서 instance란 쉽게 말해 호출할 component의 public 변수 / 함수 라고 생각하면 됩니다.



또한, @Input() 으로 선언된 변수에도 주입이 가능하지만 @Input('') set ... 에는 적용되지 않습니다.

그리고, onchange 사이클에도 잡히지 않으므로 주의하여야 합니다.



위의 component들을 다시 작성해보겠습니다.

container component

dynamic component를 호출하는 container에 해당하는 component를 작성해 봅시다.

버튼 2개를 클릭했을 때 서로 다른 component가 화면에 출력되는 예제 입니다.

@Component({
  selector: 'app-container',
  template: `
  <button (click)="callAComponent()">Show A-Component</button>
  <button (click)="callBComponent()">Show B-Component</button>
  `
})
export class ContainerComponent {
  constructor(
    private viewContainerRef: ViewContainerRef,
    private resolver: ComponentFactoryResolver
  ) {}

  callAComponent() {
    const resolve = this.resolver.resolveComponentFactory(AComponent);
    const componentRef = this.viewContainerRef.crateComponent(resolve);
    componentRef.instance.data = 'hello world';
  }
  callBComponent() {
    const resolve = this.resolver.resolveComponentFactory(BComponent);
    const componentRef = this.viewContainerRef.crateComponent(resolve);
    componentRef.instance.data = 'good bye';
  }
}

dynamic components

@Component({
  selector: 'a-component',
  template: `
    <p><% raw %>{{data}}<% endraw %></p>`
})
export class AComponent {
  data!: string;
}

@Component({
  selector: 'b-component',
  template: `
    <p><% raw %>{{data}}<% endraw %></p>`
})
export class BComponent {
  @Input() data!: string;
}

실행해보면 data 변수를 통해 주입된 값이 표시됨을 확인할 수 있습니다.

BComponent@Input()은 이런 형태도 잘 주입 된다는 것을 테스트 해보기 위해 작성한 것입니다.

값 주입이 안되는 경우

만일 값을 2회 이상 주입한다면 instance 통해 주입하더라도 값이 변경되지 않습니다.

afterViewInit에서 확인해보면 값이 들어오고 있음을 확인할 수 있는데 이 때 changeDetection을 통해 랜더링 시켜주어야 비로소 화면에 적용됩니다.

container component

...
export class ContainerComponent implements AfterViewInit {
  constructor(
    private viewContainerRef: ViewContainerRef,
    private resolver: ComponentFactoryResolver,
    private cd: ChangeDetectionRef
  ) {}
  ngAfterViewInit() {
    this.cd.markForCheck();
  }

Subject를 활용한 값 주입

changeDetection 사용이 꺼려진다면 Subject를 활용하는 것도 좋은 방안이 될 수 있습니다.

초기값을 가질 수 있는 BahaviorSubject를 활용한다면 보다 쉽게 값을 주입할 수 있습니다.

container component

@Component({
  selector: 'app-container',
  template: `
  <button (click)="callAComponent()">Show A-Component</button>
  <button (click)="callBComponent()">Show B-Component</button>
  `
})
export class ContainerComponent {
  constructor(
    private viewContainerRef: ViewContainerRef,
    private resolver: ComponentFactoryResolver
  ) {}

  callAComponent() {
    const resolve = this.resolver.resolveComponentFactory(AComponent);
    const componentRef = this.viewContainerRef.crateComponent(resolve);
    componentRef.instance.data$.next('hello world');
  }
  callBComponent() {
    const resolve = this.resolver.resolveComponentFactory(BComponent);
    const componentRef = this.viewContainerRef.crateComponent(resolve);
    componentRef.instance.data.next('good bye');
  }
}

dynamic components

@Component({
  selector: 'a-component',
  template: `
    <p><% raw %>{{data$ | async}}<% endraw %></p>`
})
export class AComponent {
  data$ = new BehaviorSubject<any>('');
}

@Component({
  selector: 'b-component',
  template: `
    <p><% raw %>{{data$ | async}}<% endraw %></p>`
})
export class BComponent {
  data$ = new BehaviorSubject<any>('');
}

Component 제거하기

제거는 매우 간단하게 container component의 viewContainerRefclear 함수를 호출하면 됩니다.

container component

@Component({
  selector: 'app-container',
  template: `
  <button (click)="callAComponent()">Show A-Component</button>
  <button (click)="callBComponent()">Show B-Component</button>
  <button (click)="removeAll()">RemoveAll</button>
  `
})
export class ContainerComponent {
  constructor(
    private viewContainerRef: ViewContainerRef,
    private resolver: ComponentFactoryResolver
  ) {}

  callAComponent() {
    const resolve = this.resolver.resolveComponentFactory(AComponent);
    const componentRef = this.viewContainerRef.crateComponent(resolve);
    componentRef.instance.data$.next('hello world');
  }
  callBComponent() {
    const resolve = this.resolver.resolveComponentFactory(BComponent);
    const componentRef = this.viewContainerRef.crateComponent(resolve);
    componentRef.instance.data.next('good bye');
  }
  removeAll() {
    this.viewContainerRef.clear();
  }
}

기타

v13 이후 개선사항

v13부터는 기존의 createComponent 함수가 deprecated 되고, 새로 작성된 createComponent 함수를 사용해야 합니다.

기존에는 componentFactory를 통해 component에 접근해야 했는데 새로운 버전에서는 component에 직접 접근할 수 있게 되어 더욱 간결한 코딩이 가능해졌습니다.



위의 container component를 v13 버전으로 작성하면 다음과 같습니다.

container component

@Component({
  selector: 'app-container',
  template: `
  <button (click)="callAComponent()">Show A-Component</button>
  <button (click)="callBComponent()">Show B-Component</button>
  `
})
export class ContainerComponent {
  constructor(
    private viewContainerRef: ViewContainerRef
  ) {}

  callAComponent() {
    const componentRef = this.viewContainerRef.crateComponent(AComponent);
    componentRef.instance.data$.next('hello world');
  }
  callBComponent() {
    const componentRef = this.viewContainerRef.crateComponent(BComponent);
    componentRef.instance.data.next('good bye');
  }
}

Angular (v17+) 버전으로 개선하기

v17 이후로는 Standalone Components와 Signals가 표준으로 자리 잡았습니다. 최신 Angular에서는 동적 컴포넌트 생성이 훨씬 더 간결하고 강력해졌습니다.

아래는 Standalone Components, Signals, 그리고 @ViewChild를 활용한 최신 버전의 예제입니다.

1. 동적으로 생성될 컴포넌트 (A, B Components)

  • standalone: true로 선언합니다.
  • 데이터를 받기 위해 @Input과 signal을 함께 사용합니다. signal은 외부에서 값을 변경할 수 있는 WritableSignal입니다.
// a.component.ts
import { Component, Input, signal, WritableSignal } from '@angular/core';

@Component({
  standalone: true,
  selector: 'a-component',
  template: `<p>A Component says: {{ data() }}</p>`
})
export class AComponent {
  @Input() data: WritableSignal<string> = signal('');
}
// b.component.ts
import { Component, Input, signal, WritableSignal } from '@angular/core';

@Component({
  standalone: true,
  selector: 'b-component',
  template: `<p>B Component says: {{ data() }}</p>`
})
export class BComponent {
  @Input() data: WritableSignal<string> = signal('');
}

2. 컨테이너 컴포넌트

  • 템플릿에 ng-container를 두고, 템플릿 참조 변수(#container)를 사용하여 위치를 지정합니다.
  • @ViewChild를 사용하여 ViewContainerRef에 접근합니다.
  • createComponent에 컴포넌트 클래스를 직접 전달하여 인스턴스를 생성합니다.
  • 생성된 컴포넌트의 instance.data (Signal)에 .set() 메서드로 값을 주입합니다.
// container.component.ts
import { Component, ViewChild, ViewContainerRef } from '@angular/core';
import { AComponent } from './a.component';
import { BComponent } from './b.component';

@Component({
  selector: 'app-container',
  standalone: true,
  imports: [AComponent, BComponent], // 동적 컴포넌트도 import 필요
  template: `
    <button (click)="callAComponent()">Show A-Component</button>
    <button (click)="callBComponent()">Show B-Component</button>
    <button (click)="removeAll()">Remove All</button>
    
    <!-- 동적 컴포넌트가 렌더링될 위치 -->
    <ng-container #container></ng-container>
  `
})
export class ContainerComponent {
  // #container를 ViewContainerRef로 읽어옵니다.
  @ViewChild('container', { read: ViewContainerRef, static: true })
  private viewContainerRef!: ViewContainerRef;

  callAComponent() {
    this.viewContainerRef.clear(); // 기존 컴포넌트 제거
    const componentRef = this.viewContainerRef.createComponent(AComponent);
    // Signal에 값을 설정합니다.
    componentRef.instance.data.set('Hello from Container!');
  }

  callBComponent() {
    this.viewContainerRef.clear(); // 기존 컴포넌트 제거
    const componentRef = this.viewContainerRef.createComponent(BComponent);
    // Signal에 값을 설정합니다.
    componentRef.instance.data.set('Goodbye from Container!');
  }

  removeAll() {
    this.viewContainerRef.clear();
  }
}

이처럼 최신 Angular에서는 ComponentFactoryResolver 없이도 타입 안정성을 유지하며, Signal을 통해 반응형 데이터를 동적 컴포넌트에 쉽게 주입할 수 있습니다.

참고 사이트

profile
Angular2+ Developer

0개의 댓글