티스토리 뷰

ElementRef, TemplateRef, ViewRef, ComponentRef and ViewContainerRef을 통해 dom을 조작하는 방법에 대해 알아보자

 

@ViewChild

component/directive class 안에서 추상화에 접근하기 위해선 Angular에서 제공하는 @ViewChild, @ViewChildren이라는 DOM쿼리라는 매커니즘을 사용한다. @ViewChild는 하나, @ViewChildren은 여러개의 쿼리를 반환한다.

// 기본 사용 형태
@ViewChild([reference from template], {read: [reference type]});

// example
@Component({
    selector: 'sample',
    template: `
        <span #tref>I am span</span>
    `
})
export class SampleComponent implements AfterViewInit {
    @ViewChild("tref", {read: ElementRef}) tref: ElementRef;

    ngAfterViewInit(): void {
        // outputs `I am span`
        console.log(this.tref.nativeElement.textContent);
    }
}

 

ElementRef

가장 기본적인 추상화 방식으로 기본 DOM요소를 접근하는데 쓰임. (사용법은 위의 코드 참고)

하지만 이렇게 직접적으로 액세스(textContent와 같은 특정 DOM API)를 허용하면 응용 프로그램이 XSS 공격에 더 취약해 질 수 있고 애플리케이션과 렌더링 계층을 긴밀하게 결합하여 여러 플랫폼에서 앱을 실행하기 어려우므로 사용에 주의해야 한다.

@ViewChild를 통해 모든 DOM요소에 대해 ElementRef를 반환할 수 있지만, 모든 컴포넌트는 custom DOM element내에서 호스팅 되고 모든 directive는 DOM element에 적용되므로 component/directive class는 DI를 통해 host element와 연결된 elementRef 인스턴스를 얻을 수 있다.

@Component({
    selector: 'sample',
    ...
export class SampleComponent{
    constructor(private hostElement: ElementRef) {
        //outputs <sample>...</sample>
        console.log(this.hostElement.nativeElement.outerHTML);
    }

따라서 컴포넌트는 DI를 통해 host element에 접근할 수 있지만 @ViewChild는 해당 뷰(template)의 DOM element에 대한 참조를 가져오는데 사용된다. 하지만 뷰가 없는 directive의 경우 접근할 수 없다.

 

TemplateRef

template은 애플리케이션 뷰에서 재사용되는 DOM element 그룹이다. HTML5이전엔 "text/template"타입의 script tag으로 쓰였지만 시맨틱이나 수동으로 DOM을 생성해야 하는 많은 단점이 있었고 content프로퍼티를 통해 접근해야 했다.

이 방식 대신 angular에서는 templateRef를 통해 접근할 수 있다. 

@Component({
    selector: 'sample',
    template: `
        <ng-template #tpl>
            <span>I am span in template</span>
        </ng-template>
    `
})
export class SampleComponent implements AfterViewInit {
    @ViewChild("tpl") tpl: TemplateRef<any>;

    ngAfterViewInit() {
        let elementRef = this.tpl.elementRef;
        // outputs `template bindings={}`
        console.log(elementRef.nativeElement.textContent);
    }
}


// DOM에서 template element를 제거하고 템플릿 주석이 삽입된다.
<sample>
    <!--template bindings={}-->
</sample>

TemplateRef클래스는 단순한 클래스이며 elementRef프로퍼티에서 host element에 대한 참조를 갖고 있고 createEmebeddedView라는 메서드를 갖고 있다. 뷰를 생성할 수 있고 ViewRef형태로 참조를 반환하기 때문에 매우 유용한 방법이다.

 

ViewRef

ViewRef는 Angular의 뷰를 나타낸다. 뷰는 애플리케이션UI의 기본 구성 요소이다. 뷰는 함께 create되고 destroy되는 element의 가장 작은 그룹이며 HTML태그들로 구성하는 것보단 뷰로 UI를 구성하는 것을 장려한다.

Angular는 두가지 뷰타입을 지원한다.

1. 템플릿에 연결된 Embedded View: 뷰가 템플릿으로 인스턴스화 될 수 있음

ngAfterViewInit() {
    let view = this.tpl.createEmbeddedView(null);
}

2.컴포넌트에 연결된 Host View: 컴포넌트가 동적으로 인스턴스화 될 때 생성됨

constructor(private injector: Injector, private r: ComponentFactoryResolver) {
    let factory = this.r.resolveComponentFactory(ColorComponent);
    let componentRef = factory.create(injector);
    let view = componentRef.hostView;
}

Angular에선 각 컴포넌트가 인젝터의 인스턴스에 바인딩되어 있으므로 컴포넌트를 생성할 때 현재 인젝터 인스턴스를 전달한다. 또한 동적으로 생성되는 컴포넌트는 모듈의 EntryComponents나 hosting component에 반드시 추가해야 한다.

 

 

ViewContainerRef

ViewContainerRef는 하나 이상의 뷰를 연결할 수 있는 컨테이너를 나타낸다. 가장 먼저 언급해야 할 것은 모든 DOM element는 View Container로 사용할 수 있다는 것이다. 흥미로운 점은 Angular가 엘리먼트 내부에 뷰를 삽입하는게 아니라 뷰 컨테이너에 바인딩 된 엘리먼트 뒤에 뷰를 추가한다는 것이다. (이 점은 router-outlet이 컴포넌트를 삽입하는  방식과 유사하다.)

일반적으로 뷰컨테이너가 생성되어야하는 곳을 표시하는 데 적합한 곳은 ng-container element이다. 이는 주석형태로 렌더되어 DOM에 HTML 엘리먼트를 추가하지 않는다.

@Component({
    selector: 'sample',
    template: `
        <span>I am first span</span>
        <ng-container #vc></ng-container>
        <span>I am last span</span>
    `
})
export class SampleComponent implements AfterViewInit {
    @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;

    ngAfterViewInit(): void {
        // outputs `template bindings={}`
        console.log(this.vc.element.nativeElement.textContent);
    }
}

다른 DOM 추상화와 다르게 뷰컨테이너는 element property를 통해 접근하는 특정 DOM엘리먼트에 바인딩 된다. 위의 코드에서는 주석으로 렌더링된 ng-container에 바인딩되어 있으므로 template bindings={}가 출력된다.

 

 

Manipulating views

ViewContainer는 뷰 조작을 위한 편리한 API를 제공한다. 

class ViewContainerRef {
    ...
    clear() : void
    insert(viewRef: ViewRef, index?: number) : ViewRef
    get(index: number) : ViewRef
    indexOf(viewRef: ViewRef) : number
    detach(index?: number) : ViewRef
    move(viewRef: ViewRef, currentIndex: number) : ViewRef
}

앞서  템플릿과 컴포넌트에서 두가지 타입의 뷰를 수동으로 생성하는 방법을 살펴보았다. 일단 뷰가 있을 때, insert를 사용하여 DOM에 삽입할 수 있었다. 다음은 템플릿에서 embedded view를 만든 후 ng-container element로 표시된 특정 위치에 삽입하는 예시이다.

@Component({
    selector: 'sample',
    template: `
        <span>I am first span</span>
        <ng-container #vc></ng-container>
        <span>I am last span</span>
        <ng-template #tpl>
            <span>I am span in template</span>
        </ng-template>
    `
})
export class SampleComponent implements AfterViewInit {
    @ViewChild("vc", {read: ViewContainerRef}) vc: ViewContainerRef;
    @ViewChild("tpl") tpl: TemplateRef<any>;

    ngAfterViewInit() {
        let view = this.tpl.createEmbeddedView(null);
        this.vc.insert(view);
    }
}
// html 결과물
<sample>
    <span>I am first span</span>
    <!--template bindings={}-->
    <span>I am span in template</span>

    <span>I am last span</span>
    <!--template bindings={}-->
</sample>

DOM에서 뷰를 삭제하기 위해 detach를 사용한다. 다른 메서드들은 인덱스로 뷰의 참조를 가져오거나 뷰를 다른 위치로 이동시키거나 컨테이너에서 모든 뷰를 제거하는데 사용할 수 있다.

 

Creating Views

ViewContainer는 자동으로 뷰를 생성하는 API도 제공한다.

class ViewContainerRef {
    element: ElementRef
    length: number

    createComponent(componentFactory...): ComponentRef<C>
    createEmbeddedView(templateRef...): EmbeddedViewRef<C>
    ...
}

해당 방법은 위에서 수동으로 했던 것을 간단하고 편리하게 해주는 wrapper이다. 템플릿이나 컴포넌트에서 뷰를 생성하고 지정된 위치에 삽입해준다.

 

 

ngTemplateOutlet and ngComponentOutlet

근본적인 매커니즘이 어떻게 동작하는지 아는 것은 항상 좋지만 지름길로 가는 것도 좋은 방법이 될 수 있다. 이 지름길은 ngTemplateOutlet, ngComponentOutlet의 두 디렉티브이다. 이 글이 쓰여진 시점에 두 디렉티브 모두 experimental이고 ngComponentOutlet은 버전4 기준이다.

ngTemplateOutlet

이 디렉티브는 DOM element를 ViewContainer로 표시하고 컴포넌트 클래스에서 명시적으로 이 작업을 수행할 필요 없이 템플릿에 의해 생성된 embedded view를 삽입한다. 즉 #vc DOM element에 뷰를 생성하고 삽입했던 위의 예제는 아래와 같이 쓸 수 있다.

@Component({
    selector: 'sample',
    template: `
        <span>I am first span</span>
        <ng-container [ngTemplateOutlet]="tpl"></ng-container>
        <span>I am last span</span>
        <ng-template #tpl>
            <span>I am span in template</span>
        </ng-template>
    `
})
export class SampleComponent {}

이렇게 사용하면 컴포넌트 클래스에 뷰를 인스턴스화하는 코드를 사용하지 않아 편리하다.

ngComponentOutlet

이 디렉티브는 embedded view가 아니라 컴포넌트를 인스턴스화하는 host view를 생성한다는 점에서 ngTemplateOutlet과 비슷하다.

<ng-container *ngComponentOutlet="ColorComponent"></ng-container>

 

 

정리

- 템플릿 변수 참조와 마찬가지로  ViewChild쿼리를 사용하여 Angular DOM 추상화 참조를 가져올 수 있다.

- DOM element에 사용할 수 있는 가장 간단한 wrapper는 ElementRef이다.

- 템플릿에서 embedded view를 생성할 수 있는 가장 간단한 방법은 TemplateRef이다.

- ComponentFactoryResolver를 사용해 생성한 ComponentRef에서 Host view에 접근할 수 있다.

- 뷰는 ViewContainerRef로  조작할 수 있다.

- ngTemplateOutlet(for embedded view), ngComponentOutlet(for host view)를 사용하면 자동으로 위에 것들을 사용할 수 있다.

 

 

 

출처

https://indepth.dev/posts/1052/exploring-angular-dom-manipulation-techniques-using-viewcontainerref

 

Exploring Angular DOM manipulation techniques using ViewContainerRef - Angular inDepth

The article explores common elements used for DOM manipulation in Angular with a particular focus on ViewContainerRef. Learn why they are needed, how they work and when you need to use which.

indepth.dev

 

댓글
최근에 올라온 글
«   2025/03   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31