/ ANGULAR

Angular 강좌(11) - 데이터공유(@ViewChild)

Angular 강좌는 여러 절로 구성되어 있습니다.


부모 Component의 직접적인 자식 요소 제어

이번 포스트는 부모 Component에서 자식 요소에 직접 접근하는 방법에 대해서 알아보겠습니다. 이전 포스트에서 @Input decorator를 이용해 부모 Component에서 자식 Component로 데이터를 전달하는 방법에 대해서 알아보았는데 이번에는 약간 다릅니다.

부모 Component는 자식 Component 객체뿐만 아니라 자식으로 포함된 Directive에 직접 접근할 수 있고 또한 Component가 Rendering하는 View자체에 직접 접근할 수 있습니다.

하지만 이런 접근 방법이 항상 좋은건 아닙니다. 오히려 좋지 않은 현상이 발생하게 됩니다. 예를 들어 Component가 직접적으로 DOM에 접근해서 제어하는 코드를 작성한다고 가정해 보죠. 일단 간단하게 프로그램을 구현할 수 있으나 나중에 Component의 View가 변경되면 Component에서 처리하는 부분도 당연히 그에 맞게 바뀌어야 합니다. Component의 재사용성과 유지보수성에 문제가 생길 여지가 있습니다.

그렇기 때문에 이런 직접적인 접근방식은 꼭 필요한 경우가 아니면 지양하는 것이 좋습니다.

그럼 천천히 한번 알아보도록 하죠.


@ViewChild, @ViewChildren Decorator

부모 Component template안에 위치한 모든 자식 요소들을 ViewChild라고 합니다. 이 ViewChild안에는 자식 Component 객체뿐만 아니라 Component가 Rendering하는 View의 DOM 그리고 Directive가 포함됩니다.

자식 Component객체에 직접 접근하는 방법부터 살펴보도록 하겠습니다.

자식 Component 객체에 직접 접근하려면 @ViewChild decorator를 이용하시면 됩니다. 조건에 부합되는 객체 1개를 찾게되고 그에 대한 property를 지정해서 사용할 수 있습니다. 만약 @ViewChildren을 이용하면 조건에 부합되는 객체를 모두 찾게 되고 QueryList 형태로 객체들의 집합을 얻을 수 있습니다. QueryList는 실제 배열이 아니기 때문에 toArray() method를 이용해 배열을 얻어내 이용할 수 있습니다.

그럼 간단한 예를 가지고 알아보도록 하죠.

부모 Component에 초기화버튼을 하나 만들어서 해당 버튼을 누르면 Client가 선택한 도서 종류와 입력된 키워드를 초기화 시키는 작업을 해 보도록 하겠습니다.

먼저 초기화버튼을 만들어야 하니 book-search-main.component.html부터 수정해야 합니다.

<div class="bookSearch-outer">
  <div class="d-flex align-items-center p-3 my-3 text-white-50 bg-purple rounded box-shadow">
    <img class="mr-3" src="assets/images/search-icon.png" alt="" width="48" height="48">
    <div class="lh-100">
      <h5 class="mb-0 text-white lh-100">Search Result : </h5>
    </div>
  </div>

  <div class="example-container">
    <mat-form-field>
      <mat-select placeholder="도서종류"
                  #bookCategorySelect
                  [(ngModel)]="selectedValue"
                  (ngModelChange)="changeValue(bookCategorySelect.value)">
        <mat-option *ngFor="let category of bookCaterory"
                    [value]="category.value">
          
        </mat-option>
      </mat-select>
    </mat-form-field>
    <button mat-raised-button color="primary"
            (click)="clearCondition()">검색 초기화</button>
  </div>

  <div>
    <app-search-box [bookCategory]="displayCategoryName"
                    (searchEvent)="changeTitleBar($event)"></app-search-box>
  </div>
  <div>
    <app-detail-box></app-detail-box>
  </div>
  <div>
    <app-list-box></app-list-box>
  </div>
</div>

검색 초기화 버튼을 생성하고 해당 버튼을 클릭하면 clearCondition() method가 호출되도록 처리했습니다.

다음은 부모 Component인 book-search-main.component.ts 파일입니다. clearCondition() method를 작성해야하고 해당 method안에서 자신의 검색에 관련된 사항을 초기화하고 자식 Component를 찾아 자식 Component의 property를 초기화시키는 작업을 진행합니다.

import {Component, OnInit,
        ViewChild, ViewChildren, QueryList } from '@angular/core';
import { SearchBoxComponent } from "../search-box/search-box.component";

@Component({
  selector: 'app-book-search-main',
  templateUrl: './book-search-main.component.html',
  styleUrls: ['./book-search-main.component.css',
  './offcanvas.css']
})
export class BookSearchMainComponent implements OnInit {

  selectedValue = null;
  displayCategoryName = null;

  bookCaterory = [
    {value: 'all', viewValue: '국내외도서'},
    {value: 'country', viewValue: '국내도서'},
    {value: 'foreign', viewValue: '국외도서'}
  ];

  searchTitle = null;

  constructor() { }

  ngOnInit() {
  }

  changeValue(category: string): void {
    for(let element of this.bookCaterory ) {
      if(element.value == category) {
        this.displayCategoryName = element.viewValue;
      }
    }
  }

  changeTitleBar(searchInfo) : void {
    this.searchTitle = `${searchInfo.keyword} ( ${searchInfo.category} )`;
  }

  @ViewChild(SearchBoxComponent) searchComp: SearchBoxComponent;
  @ViewChildren(SearchBoxComponent) searchCompArr: QueryList<SearchBoxComponent>;

  clearCondition(): void {
    this.selectedValue = null;
    this.searchTitle = null;
/*
    @ViewChild를 사용할 경우
    this.searchComp._bookCategory = null;
    this.searchComp.keyword = null;
*/
    // @ViewChildren을 사용할 경우
    this.searchCompArr.toArray()[0]._bookCategory = null;
    this.searchCompArr.toArray()[0].keyword = null;
  }
}

부모 Component와 자식 Component가 데이터를 공유하는게 아니라 부모 Component가 직접 자식 Component 객체를 제어하는 방식입니다.


Component가 Rendering하는 View의 DOM에 직접 접근

@ViewChild와 @ViewChildren을 이용하면 자식 Component의 객체뿐 아니라 Component가 rendering하는 View의 DOM에 직접 접근할 수 있습니다. 이전에 나왔던 Template Reference Variable을 이용해서 Component가 DOM에 접근하는 것이죠.

우리 예제에 딱히 필요하진 않지만 이해를 돕기 위해 버튼 하나를 더 추가해 어떻게 사용하는지 살펴보겠습니다.

book-search-main.component.html을 수정해 버튼을 하나 더 추가합니다.

...
...
<h5 #resultStatus class="mb-0 text-white lh-100">Search Result : </h5>
...
...
...
    <button mat-raised-button color="primary"
            (click)="changeDOM()">DOM 직접 변경</button>
...
...            

일부만 표시했습니다. 결과를 표시하는 영역에 Template Reference Variable #resultStatus을 지정했습니다. 그리고 버튼을 하나 추가했구요. 해당 버튼을 클릭하면 changeDOM() method가 호출되겠네요.

다음은 book-search-main.component.ts 파일 내용입니다.

import {Component, OnInit,
        ViewChild, ViewChildren, QueryList,
        ElementRef } from '@angular/core';
import { SearchBoxComponent } from "../search-box/search-box.component";


@Component({
  selector: 'app-book-search-main',
  templateUrl: './book-search-main.component.html',
  styleUrls: ['./book-search-main.component.css',
  './offcanvas.css']
})
export class BookSearchMainComponent implements OnInit {

  selectedValue = null;
  displayCategoryName = null;

  bookCaterory = [
    {value: 'all', viewValue: '국내외도서'},
    {value: 'country', viewValue: '국내도서'},
    {value: 'foreign', viewValue: '국외도서'}
  ];

  searchTitle = null;

  constructor() { }

  ngOnInit() {
  }

  changeValue(category: string): void {
    for(let element of this.bookCaterory ) {
      if(element.value == category) {
        this.displayCategoryName = element.viewValue;
      }
    }
  }

  changeTitleBar(searchInfo) : void {
    this.searchTitle = `${searchInfo.keyword} ( ${searchInfo.category} )`;
  }

  @ViewChild(SearchBoxComponent) searchComp: SearchBoxComponent;
  @ViewChildren(SearchBoxComponent) searchCompArr: QueryList<SearchBoxComponent>;

  clearCondition(): void {
    this.selectedValue = null;
    this.searchTitle = null;
/*
    @ViewChild를 사용할 경우
    this.searchComp._bookCategory = null;
    this.searchComp.keyword = null;
*/
    // @ViewChildren을 사용할 경우
    this.searchCompArr.toArray()[0]._bookCategory = null;
    this.searchCompArr.toArray()[0].keyword = null;
  }

  @ViewChild('resultStatus') resultToolbar: ElementRef;

  changeDOM(): void {
    this.resultToolbar.nativeElement.onclick = function() {
      alert('DOM을 직접 제어할 수 있어요!!');
    };
    this.resultToolbar.nativeElement.innerHTML = "클릭해보세요!!";
  }

}

아래부분에 resultStatus Template Reference Variable을 이용해서 해당 Element의 Reference를 획득하는 부분을 잘 보시면 됩니다. 이렇게 ElementRef type의 객체를 획득하면 nativeElement 속성으로 직접 제어할 수 있습니다.

이번 포스트에서는 @ViewChild와 @ViewChildren을 이용해 자식 Component의 객체를 직접 제어하거나 rendering된 View의 DOM에 직접 접근해서 제어하는 방법에 대해서 살펴보았습니다. 다음 포스트는 Angular에서 Content Projection이라고 불리는 부분에 대해서 살펴보도록 하겠습니다.

End.


Angular 강좌는 아래의 책과 사이트를 참조했습니다. 조금 더 자세한 사항을 알고 싶으시면 해당 사이트를 방문하세요!!