728x90
반응형

 부모 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이라고 불리는 부분에 대해서 살펴보도록 하겠습니다.

728x90
반응형

'Web Programming > Angular - TypeScript' 카테고리의 다른 글

Angular Service  (0) 2018.08.28
Angular Content Projection 데이터공유  (0) 2018.08.28
Angular @Output 데이터공유  (0) 2018.08.28
Angular @Input 데이터공유  (0) 2018.08.28
Angular Material Table  (0) 2018.08.28
728x90
반응형

 @Output Decorator

이번 포스트는 자식 Component에서 부모 Component로 데이터를 전달하기 위한 @Outputdecorator에 대해서 알아보겠습니다.

자식 Component에서 부모 Component로 데이터를 전달하기 위해서는 EventEmitter를 이용한 이벤트 처리를 하셔야 합니다. 즉, 자식 Component에서 발생한 event를 부모 Component가 event bidning을 이용해 데이터를 받는 방식입니다.

코드레벨에서 알아보죠. 이전에 @Input decorator를 설명하면서 사용했던 예제를 좀 수정해서 사용하겠습니다.

아래는 search-box.component.ts 내용입니다.

import { Component, OnInit,
         Input, Output, EventEmitter } from '@angular/core';

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

  _bookCategory: string;
  //@Input() bookCategory:string;
  //@Input('bookCategory') mySelected:string;

  @Input()
  set bookCategory(value: string) {
    if( value != null ) {
      // 추가적인 작업이 들어올 수 있습니다.
      this._bookCategory = 'category: ' +value;
    } else {
      this._bookCategory = value;
    }

  }

  @Output() searchEvent = new EventEmitter();

  keyword = null;

  constructor() { }

  ngOnInit() {
  }

  setKeyword(keyword: string): void {
    this.keyword = keyword;
    this.searchEvent.emit({
      keyword : `${this.keyword}`,
      category: `${this._bookCategory.replace('category: ','')}`
    });
  }

  inputChange(): void {

  }
}

부모 Component에게 이벤트를 전달하기 위해 다음의 코드로 EventEmitter 객체를 생성하고 @Output decorator를 이용했습니다. 부모 Component는 searchEvent 이름으로 event binding 해야합니다.

@Output() searchEvent = new EventEmitter();

자식 Component에서 Search! 버튼을 눌렀을 때 setKeyword() method가 호출되는데 이 안에서 searchEvent에 대한 이벤트를 발생시킵니다. 그러면서 부모 Component에게 전달할 데이터를 인자로 넣어줍니다.

this.searchEvent.emit({
    keyword : `${this.keyword}`,
    category: `${this._bookCategory.replace('category: ','')}`
});

이제 부모 Component에서 어떻게 event binding을 이용해서 데이터를 받는지만 살펴보면 됩니다.

아래는 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 : {{searchTitle}}</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">
          {{ category.viewValue }}
        </mat-option>
      </mat-select>
    </mat-form-field>
  </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>

주의해서 보셔야 할 부분은

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

입니다. event binding을 이용해서 searchEvent 이벤트가 발생하면 changeTitleBar()method를 호출하고 인자를 받아서 처리하고 있네요. 인자 받는 방식도 유의해서 보셔야 합니다.


<h5 class="mb-0 text-white lh-100">Search Result : {{searchTitle}}</h5>

interpolation을 이용해 searchTitle 속성의 값을 View에 출력하고 있네요. 아마 changeTitleBar() method안에서 내용이 결정될 듯 합니다.

마지막으로 book-search-main.component.ts 파일입니다.

import { Component, OnInit } from '@angular/core';

@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} )`;
  }
}

기본적인 개념은 이미 설명했으니 코드레벨에서 한번 천천히 살펴보시면 충분히 이해할 수 있을 듯 보입니다.

여기까지 작업한 내용을 실행해 정상적으로 동작하는지 확인하시면 될 듯 합니다.

자식 Component에서 부모 Component로 Event를 이용해서 데이터를 어떻게 전달하는지에 대해서 간단하게 살펴보았습니다.

다음 포스트는 부모 Component에서 자식 요소에 접근할 때 또 다른 방법으로 어떤 방법이 있는지 알아보도록 하겠습니다.

728x90
반응형

'Web Programming > Angular - TypeScript' 카테고리의 다른 글

Angular Content Projection 데이터공유  (0) 2018.08.28
Angular @ViewChild 데이터공유  (0) 2018.08.28
Angular @Input 데이터공유  (0) 2018.08.28
Angular Material Table  (0) 2018.08.28
Angular 예제 따라하기  (0) 2018.08.28
728x90
반응형

Component Data Sharing

이번 포스트는 Component 간의 데이터 공유에 대해서 알아보고 그 내용을 기반으로 우리 mySearchProject의 도서검색 부분을 완성해 나가도록 하겠습니다.

최종적으로 완성된 프로그램은 여기를 클릭하시면 실행시켜 보실 수 있습니다.

실행시켜 보시면 이전에 비해 세가지 기능이 추가되었습니다.

  • 도서 검색 시 도서 종류(국내도서, 국외도서, 국내외도서)에 대한 Filtering이 가능합니다.
  • 키워드 입력 후 Search버튼을 클릭하면 해당 키워드에 대한 책만 list-box에 출력됩니다.
  • list-box에 출력된 책 중 하나를 선택하면 해당 책에 대한 세부내역을 detail-box에 출력합니다.

이 기능들을 구현하려면 Component간의 데이터 공유 방법을 아셔야 합니다. Component간 데이터를 공유하는 방법은 여러가지가 있는데 하나씩 살펴보면서 우리 코드에 적용해 보겠습니다.

그럼 천천히 한번 살펴보기로 하죠.


 @Input Decorator

이전에 View의 포함관계를 설명하면서 Component Tree에 대한 언급을 한 적이 있습니다. Component간의 부모-자식 관계가 성립되면 서로간의 데이터 연결통로가 생성됩니다. 이를 통해 부모 Component와 자식 Component간의 데이터 공유가 이루어질 수 있습니다.

먼저 부모 Component에서 자식 Component로 데이터를 전달하는 방법에 대해서 알아보죠.

만약 부모 Component가 사용자 입력양식을 가지고 있다면 Client에 의해서 사용자 입력양식의 상태값이 변경될 수 있고 그 상태값를 자식 Component와 공유할 필요가 있습니다. 우리 예제로 설명하자면 상위 Component인 book-search-main Component에서 Client가 설정한 도서 종류가 하위 Component인 search-box Component에 전해져야 제대로 된 검색을 수행할 수 있다는 말입니다.

이런 경우 부모 Component는 property binding을 이용해 자식 Component에게 데이터를 전달해 줄 수 있습니다. 이렇게 전달된 데이터는 @Input decorator에 의해서 자식 Component에서 사용될 수 있습니다.

우리 예제를 수정해서 Client가 Select Box에서 선택한 도서 종류 정보가 하위 Component인 search-box Component와 공유되는지 확인해 보겠습니다.

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">
          {{ category.viewValue }}
        </mat-option>
      </mat-select>
    </mat-form-field>
  </div>

  <div>
    <app-search-box [bookCategory]="displayCategoryName"></app-search-box>
  </div>
  <div>
    <app-detail-box></app-detail-box>
  </div>
  <div>
    <app-list-box></app-list-box>
  </div>
</div>

위의 코드에서 다음의 코드를 주의해서 보시면 됩니다.


<mat-select placeholder="도서종류"
            #bookCategorySelect
            [(ngModel)]="selectedValue"
            (ngModelChange)="changeValue(bookCategorySelect.value)">
    <mat-option *ngFor="let category of bookCaterory"
                [value]="category.value">
                {{ category.viewValue }}
    </mat-option>
</mat-select>

이전에 배웠던 Tempalte Reference Variable과 양방향 바인딩을 이용해 Client가 도서 종류를 변경하면 changeValue() method가 호출됩니다.

이 method는 book-search-main.component.ts안에 정의되어 있어야 하겠죠.

import { Component, OnInit } from '@angular/core';

@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: '국외도서'}
  ];

  constructor() { }

  ngOnInit() {
  }

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

method의 하는일을 보니 Client가 선택한 도서 종류를 가지고 displayCategoryName이라는 속성의 값을 변경하고 있습니다.

이 displayCategoryName 속성이 바로 자식 Component인 search-box Component에게 전달되는 데이터입니다.

다시 위쪽의 book-search-main.component.html의 내용을 보면 아래와 같은 코드가 있습니다.

<div>
    <app-search-box [bookCategory]="displayCategoryName"></app-search-box>
</div>

search-box Component에 property binding을 이용해 bookCategory라는 이름으로 displayCategoryName 속성을 바인딩해 놓은걸 확인하실 수 있습니다.

이제 search-box.component.ts의 내용을 보죠

import { Component, OnInit, Input } from '@angular/core';

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

  @Input() bookCategory:string;

  keyword = null;

  constructor() { }

  ngOnInit() {
  }

  setKeyword(keyword: string): void {
    this.keyword = keyword;
  }

  inputChange(): void {

  }
}

@Input decorator를 볼 수 있습니다. @Input decorator를 이용하기 위해서 import를 해 줘야 하는것도 잊지 마시구요.

@Input() bookCategory:string;

위의 코드처럼 bookCategory라는 이름으로 부모 Component가 property binding으로 전달해준 데이터를 받을 수 있습니다.

이 속성을 View에 interpolation을 이용해서 출력하면 될 듯 합니다.

다음은 View에 rendering되는 search-box.component.html 입니다.


<div class="example-container">
  <mat-toolbar class="search-toolbar-style">
    Search Keyword : {{keyword}}
    <ng-container *ngIf="bookCategory != null">
      ( {{bookCategory}} )
    </ng-container>
  </mat-toolbar>
  <mat-form-field>
    <input matInput #inputKeyword placeholder="Search Keyword"
           [(ngModel)]="keyword" (ngModelChange)="inputChange()">
  </mat-form-field>
  <button mat-raised-button color="warn"
          (click)="setKeyword(inputKeyword.value)">Search!</button>
</div>

Toolbar부분에 {{keyword}}와 함께 {{bookCategory}}를 이용해서 속성과 binding시킨 걸 확인할 수 있습니다. 만약 bookCategory값이 null이면 출력되지 않게끔 built-in directive를 이용해 처리했습니다.

위의 코드에서 보듯이 자식 Component인 select-box Component는 자신에게 데이터를 주는 부모 Component가 어떤 Component인지는 알 필요가 없습니다. 단지 전달된 데이터를 사용할 수 있도록 해주는 property의 이름과 data type만이 필요할 뿐이죠. Component간의 Loosely Coupling을 유지하면서 데이터를 공유할 수 있습니다.

기본적인 @Input decorator를 사용하는 방법에 대해 설명했는데 몇개의 응용이 있습니다.

우리는 부모 Component의 book-search-main.component.html에서 property binding을 이용해 bookCategory라는 이름의 property를 사용했습니다. 이를 사용하기 위해서 자식 Component인 search-box Component에서 역시 같은 이름으로 사용했구요. 만약 다른이름으로 사용하실려면 아래와 같이 처리하시면 됩니다.

@Input('bookCategory') mySelected:string;

bookCategory라는 이름의 property 대신 mySelected property를 사용할 수 있습니다. interpolation 역시 mySelected으로 사용해야겠죠.

지금까지의 예는 모두 부모 Component가 전달해 준 데이터를 그대로 가져다 사용하는 방식입니다. 만약 부모 Component가 전달해 준 데이터를 가공해서 자식 Component에서 사용하려면 어떻게 해야 할까요?

setter를 이용하면 이 작업을 할 수 있습니다. 우리의 코드가 이렇게 바뀌겠네요.

import { Component, OnInit, Input } from '@angular/core';

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

  //@Input() bookCategory:string;
  //@Input('bookCategory') mySelected:string;
  _bookCategory: string;  

  @Input()
  set bookCategory(value: string) {
    if( value != null ) {
      // 추가적인 작업이 들어올 수 있습니다.
      this._bookCategory = 'category: ' +value;
    } else {
      this._bookCategory = value;
    }

  }

  keyword = null;

  constructor() { }

  ngOnInit() {
  }

  setKeyword(keyword: string): void {
    this.keyword = keyword;
  }

  inputChange(): void {

  }
}

사용되는 setter의 이름과 부모 Component가 property binding하는 property의 이름이 같아야 합니다. interpolation은 _bookCategory으로 변경되어야 하겠네요.

한가지 추가적으로 기억하셔야 할 점은 이렇게 부모 Component가 자식 Component에게 데이터를 전달해 줄 때 이 방식이 call-by-value방식이 아닌 call-by-reference방식이라는 것입니다. 즉, 우리의 예제에서 부모 Component와 자식 Component가 둘 다 bookCategory를 reference하고 있는 형태입니다. 이렇게 연결된 상태에서 부모 Component가 해당 property의 값을 변경시키면 그 값을 자식 Component가 공유하고 있으므로 변경된 값을 그 즉시 사용할 수 있는 것이죠.

이렇게 생각하면 자식 Component가 공유되고 있는 property의 값을 변경하면 그 변경 내용이 부모 Component에게 영향을 미쳐야 합니다. 하지만 실제로 코드 작업을 해 보면 그렇지 않다는 것을 확인하실 수 있습니다. 왜 이런 현상이 발생할까요? 만약 자식 Component에서 변경된 값이 부모 Component에게 영향을 주게끔 처리하면 나중에 이 공유데이터의 변화를 예측하기 힘들어지게 됩니다. 데이터의 변경을 tracking하기 힘들어진다는 것이죠. 지금은 간단한 경우이니 별 문제가 안되지만 프로그램이 커지게 되면 이런 데이터의 공유 문제가 프로그램의 구현과 디버깅을 힘들게 하는 원인이 됩니다.

Angular는 Stateful Component와 Stateless Component의 개념이 있습니다. Stateful Component는 다른말로 Smart Component 라고도 하는데 이 Component는 데이터의 정보를 변경하거나 저장할 수 있습니다. 하지만 Dumb Component라고 불리는 Stateless Component는 단지 상태 정보를 참조만 해서 이용할 수 있습니다. 우리의 예제에서 상위 Component인 book-search-main Component는 Stateful Component입니다. 반면 자식 Component인 search-box Component는 Stateless Component이구요. 그렇기 때문에 자식 Component에서 공유된 변수에 대한 변경을 해 주어도 상위 Component에 영향을 미치지 않게 되는 것입니다. 조금 어려운 개념인데 이 Stateful과 Stateless에 대해 조금 더 알고싶으시면 여기를 참조하시면 됩니다.

쉽게 말하자면 @Input decorator를 이용하면 부모 Component에서 자식 Component에게 데이터를 전달할 수 있지만 그 반대는 허용되지 않는군요. 이 문제를 해결하기 위해 @Outputdecorator를 사용할 수 있습니다. 즉, 자식 Component에서 변경된 사항을 부모 Component에게 전달하는 방법이 따로 있다는 것이죠. 다음 포스트에서는 @Output decorator에 대해서 알아보도록 하겠습니다.

728x90
반응형

'Web Programming > Angular - TypeScript' 카테고리의 다른 글

Angular @ViewChild 데이터공유  (0) 2018.08.28
Angular @Output 데이터공유  (0) 2018.08.28
Angular Material Table  (0) 2018.08.28
Angular 예제 따라하기  (0) 2018.08.28
SPA형식의 Web Application  (0) 2018.08.28
728x90
반응형

 Material Table Component

이번 포스트에서는 이전에 만들었던 list-box Component가 표현하는 부분을 Material Table을 이용해서 작성해 보겠습니다. 기본적인 테이블 구성과 함께 Pagination 까지 추가해서 간단하게 Paging까지 구현해보겠습니다.

사실 설명할 부분이 많지는 않습니다. DataSource만 Table에 잘 연결하면 알아서 보여주기 때문이죠. 그게 또 Component 기반 개발의 장점이기도 하구요.

먼저 CSS부터 수정하도록 하겠습니다. 다음은 list-box.component.css 파일입니다.

.example-container {
  display: flex;
  flex-direction: column;
  min-width: 300px;
  margin-top: 30px;
}

.mat-table {
  overflow: auto;
  max-height: 500px;
}

.mat-header-cell.mat-sort-header-sorted {
  color: black;
}

.list-table-style {
  font-family: Georgia;
}

.list-header-style {
  background-color: beige;
}

그 다음은 list-box.component.html 파일입니다.


<div class="example-container mat-elevation-z8">
  <mat-table class="list-table-style" #table [dataSource]="dataSource">

    <ng-container matColumnDef="bisbn">
      <mat-header-cell *matHeaderCellDef> ISBN </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.bisbn}} </mat-cell>
    </ng-container>

    <ng-container matColumnDef="btitle">
      <mat-header-cell *matHeaderCellDef> Title </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.btitle}} </mat-cell>
    </ng-container>

    <ng-container matColumnDef="bauthor">
      <mat-header-cell *matHeaderCellDef> Author </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.bauthor}} </mat-cell>
    </ng-container>

    <ng-container matColumnDef="bprice">
      <mat-header-cell *matHeaderCellDef> Price </mat-header-cell>
      <mat-cell *matCellDef="let element"> {{element.bprice}} </mat-cell>
    </ng-container>

    <mat-header-row class="list-header-style" 
                    *matHeaderRowDef="displayedColumns">                  
    </mat-header-row>
    <mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
  </mat-table>

  <mat-paginator #paginator
                 [pageSize]="5"
                 [pageSizeOptions]="[5, 10, 20]"
                 showFirstLastButtons>
  </mat-paginator>
</div>

Table Component를 이용하기 때문에 book-search.module.ts에 관련된 Module을 import해 주어야 합니다.

import { MatTableModule } from '@angular/material/table';

그리고 Paging처리를 해야 하기 때문에 MatPaginatorModule 역시 import합니다.

import { MatPaginatorModule } from '@angular/material/paginator';

위의 HTML에서 가장 중요한 부분은 당연히 DataSource를 바인딩 하는 부분입니다.

<mat-table class="list-table-style" #table [dataSource]="dataSource">

Property binding을 이용하여 Component에 있는 dataSource라는 속성과 연결시켰습니다. 이 dataSource라는 속성은 도서정보에 대한 객체배열을 이용해서 만든 MatTableDataSource class의 객체입니다. JSON 데이터를 가져와서 만든객체입니다.

마지막으로 list-box.compoennt.ts파일의 내용입니다.

import { Component, OnInit } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { MatTableDataSource } from '@angular/material';
import { MatPaginator } from '@angular/material';
import { ViewChild } from '@angular/core';

interface IBook {
  bauthor: string;
  bdate: string;
  btranslator: string;
  bpublisher: string;
  btitle: string;
  bprice: number;
  bisbn: string;
  bimgurl: string;
}

@Component({
  selector: 'app-list-box',
  templateUrl: './list-box.component.html',
  styleUrls: ['./list-box.component.css']
})
export class ListBoxComponent {

  displayedColumns = ['bisbn', 'btitle', 'bauthor', 'bprice'];
  dataSource;
  books: IBook[];

  @ViewChild(MatPaginator) paginator: MatPaginator;

  constructor(private http: HttpClient) {
    this.http.get<IBook[]>('assets/data/book.json')
      .subscribe(res => {
        this.books = res;
        this.dataSource = new MatTableDataSource<IBook>(this.books);
        this.dataSource.paginator = this.paginator;
      });
  }
}

 Code Review

원래 Code Review란 표현은 Code Inspection에서 기인한 용어로 코드를 실제로 실행하지 않고 사람이 검토하는 과정을 통해 논리적인 잠재 오류를 찾아내고 이를 개선하는 작업을 지칭합니다.

그런데 여기서는 그냥 위의 코드를 살펴보자는 의미로 사용했습니다. ^^;;

코드를 좀 간단히 설명해보죠.

먼저 Table을 생성하는 구문은 다음과 같습니다.

<mat-table [dataSource]="dataArray">
  ...
</mat-table>

위의 코드에서 dataArray라고 되어있는 부분이 실제 Table에 rendering되는 데이터입니다. 배열형태로 되어 있고 배열안의 각각의 객체를 row로 가져와서 화면에 출력하게 됩니다.

다음은 Table의 컬럼을 표현하는 template입니다. 구문은 다음과 같습니다.


<ng-container matColumnDef="bisbn">
    <mat-header-cell *matHeaderCellDef> ISBN </mat-header-cell>
    <mat-cell *matCellDef="let element"> {{element.bisbn}} </mat-cell>
</ng-container>

matColumnDef 속성은 사용할 컬럼의 이름입니다. 이 부분은 list-box.component.ts파일안에 컬럼명에 대한 배열이 정의되는데 이 부분과 매칭되어야 합니다. 다음은 list-box.component.ts안에 정의된 컬럼명에 대한 배열입니다.

displayedColumns = ['bisbn', 'btitle', 'bauthor', 'bprice'];

그리고 아래의 구문에 의해 ISBN 컬럼의 제목과 내용이 출력됩니다. dataSource에 연결된 모든 row를 가져와서 element라는 변수에 반복적으로 할당 하면서 element.bisbn값을 테이블에 출력하라는 말입니다.


<mat-header-cell *matHeaderCellDef> ISBN </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element.bisbn}} </mat-cell>

이와 같은 형태로 하나의 컬럼에 대한 데이터를 화면에 출력할 수 있습니다. 우리는 총 4개의 컬럼을 화면에 출력하고 있는 것이죠.

list-box.compoennt.ts에서는 사용할 데이터를 HttpClient의 get() method로 가져온 후 이를 다음의 코드를 이용해서 객체화 시켰습니다.

this.dataSource = new MatTableDataSource<IBook>(this.books);

dataSource와 연결시키기 위해 위에 있는 코드처럼 객체를 생성해서 연결하셔야 합니다.

Paginator의 사용은 코드에 나온것처럼 사용하시면 됩니다. 내부적으로 처리되기 때문에 사용하는 방법만 아시면 충분합니다.

TypeScript를 사용하기 때문에 interface를 이용하여 data type을 명확히 지정했습니다. 이 부분역시 이전 HTML Table Element로 작업했을 때와는 다르게 처리하셔야 합니다.


 정리

이제 1차적인 작업은 모두 끝났습니다. 논리적인 설명보다는 실제 사용할 화면을 만들면서 필요한 개념들에 대해서 그때 그때 설명하는 방식을 취했습니다. 이제 Component간의 상태공유에 대한 문제만 해결되면 우리의 프로그램은 얼추 완성할 수 있을 듯 보입니다.

조금만 더 진행시켜 일단 프로그램을 완성한 후 세부적인 내용들에 대해서 다시 짚어가며 살펴보도록 하겠습니다.

728x90
반응형

'Web Programming > Angular - TypeScript' 카테고리의 다른 글

Angular @Output 데이터공유  (0) 2018.08.28
Angular @Input 데이터공유  (0) 2018.08.28
Angular 예제 따라하기  (0) 2018.08.28
SPA형식의 Web Application  (0) 2018.08.28
Angular 개발환경세팅 하기  (0) 2018.08.28
728x90
반응형

 list-box Component View

이번 포스트에서는 도서 정보를 리스트 형태로 출력하는 list-box Component를 구현해 보도록 하겠습니다.

먼저 HTML Table Element를 이용하여 구현해보겠습니다. 사실 우리는 최종적으로 Angular Material Table Component를 이용할 것이기 때문에 HTML Table Element에 대한 CSS처리는 하지 않았습니다.

여하간 만들어지는 list의 형태는 다음 그림과 같습니다.

listbox-html-table-view

더 많은 책이 하단에 쭉 나열됩니다. 나중에는 book-search-main Component에서 만들어 놓은 Angular Material Select를 이용해 선택한 조건으로 책들에 대한 리스트가 출력되겠지만 지금은 그냥 조건없이 모든 책이 나열됩니다.

기존에 작성했던 book-search-main.component.html 파일을 열어서 HTML Select Element를 이용한 부분을 Material Select로 변경합니다.


<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="도서종류"  [(ngModel)]="selectedValue">
        <mat-option *ngFor="let category of bookCaterory"
                    [value]="category.value">
          {{ category.viewValue }}
        </mat-option>
      </mat-select>
    </mat-form-field>
  </div>
  
  <div>
    <app-search-box></app-search-box>
  </div>
  <div>
    <app-detail-box></app-detail-box>
  </div>
  <div>
    <app-list-box></app-list-box>
  </div>
</div>

일단 Angular Material의 MatSelectModule을 사용하기 때문에 book-search.module.ts에 다음과 같이 import작업부터 해야 코드에러가 나지 않을 듯 보입니다.

import { MatSelectModule } from '@angular/material/select';

mat-select가 Select box에 대한 Angular Material Component 입니다. 양방향 바인딩으로 selectedValue란 이름의 Component의 속성에 바인딩 시켜놓은 상태입니다.

mat-option은 Select box안의 각각의 Option Component입니다. 여러개가 존재할 수 있기 때문에 ngFor directive를 이용하여 반복처리 했습니다.

Angular는 구조적 지시자(Structural Directive)라는걸 제공합니다. DOM 요소를 추가하거나 삭제 혹은 반복처리를 함으로 화면의 구조를 변경할 때 사용합니다. 대표적으로는 ngIf와 ngFor가 있습니다. 이름에서 의미하다시피 ngIf는 boolean값을 입력받아 true일 경우 ngIf 가 선언된 Element를 DOM에 추가합니다. 만약 false일 경우에는 ngIf 가 선언된 Element는 DOM에서 제거됩니다. ngFor 는 반복가능한 데이터를 입력받아 DOM에 반복해서 Element를 표현할 때 사용합니다.

Directive에 대해서는 나중에 다른 포스트에서 다시 설명하겠습니다. 여기서는 구조적 지시자로 ngIf와 ngFor를 사용해서 DOM을 제어하는 방식에 대해서만 알아두시면 됩니다.

코드를 보고 유추하건대 bookCaterory는 배열형태의 데이터이고 배열의 각 원소는 객체이겠네요. 데이터 바인딩에서 학습했던 내용과 연계해서 생각해 보시면 됩니다.

그럼 아마도 book-search-main.component.ts에 다음과 같은 내용이 포함되어야 할 것입니다.

import { Component, OnInit } from '@angular/core';

@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;
  bookCaterory = [
    {value: 'all', viewValue: '국내외도서'},
    {value: 'country', viewValue: '국내도서'},
    {value: 'foreign', viewValue: '국외도서'}
  ];
  
  constructor() { }

  ngOnInit() {
  }

}

그 다음은 실제 리스트가 출력되는 list-box.component.css 파일의 내용입니다. 위쪽 margin을 주기 위한 style을 정의해 놓았습니다.

.example-container {
  margin-top: 20px;
}

다음은 list-box.component.html 파일의 내용입니다.


<table class="example-container">
  <thead>
  <th>ISBN</th>
  <th>Title</th>
  <th>Author</th>
  <th>Price</th>
  </thead>
  <tbody>
    <tr *ngFor="let book of books">
      <td>{{book.bisbn}}</td>
      <td>{{book.btitle}}</td>
      <td>{{book.bauthor}}</td>
      <td>{{book.bprice}}</td>
    </tr>
  </tbody>
</table>

코드를 수행시키기 위해 먼저 book.json 파일을 하나 준비합니다. book.json 파일의 내용은 다음과 같이 작성하시면 됩니다.

단, btranslator는 번역자를 의미합니다. 이 값이 존재하면 외국서적이라는 말이겠죠. 만약 국내도서면 이 값이 ""로 표현됩니다. 다른 key값들에 대해서는 이름에서 그 의미를 충분히 유추할 수 있을 듯 합니다.

[  
   {  
      "bauthor": "카일 루든(Kyle Loudon)",
      "bdate":"2000년 04월",
      "btranslator":"허 욱",
      "bpublisher":"한빛미디어(주)",
      "btitle":"C로 구현한 알고리즘",
      "bprice":25000,
      "bisbn":"89-7914-063-0",
      "bimgurl":"http://image.hanbit.co.kr/cover/_m_1063m.gif"
   },
   {  
      "bauthor":"권기경, 박용우",
      "bdate":"2002년 09월",
      "btranslator":"",
      "bpublisher":"한빛미디어(주)",
      "btitle":"IT EXPERT, 모바일 자바 프로그래밍",
      "bprice":23000,
      "bisbn":"89-7914-206-4",
      "bimgurl":"http://image.hanbit.co.kr/cover/_m_1206m.gif"
   },
   ...
   ...
   ...
]   

작성한 데이터 파일을 src/assets/data 폴더 아래에 저장합니다. 이 JSON data를 불러오기 위해 HttpClientModule를 이용합니다. 더 쉽게 파일로 import해서 쓸 수 있지만 여기서는 HttpClientModule로 처리했습니다.

book-search.module.ts 파일안에 HttpClientModule에 대한 import 구문을 작성합니다.

import { HttpClientModule } from "@angular/common/http";

아래는 list-box.component.ts 파일의 내용입니다.

import { Component, OnInit } from '@angular/core';
import { HttpClient } from "@angular/common/http";

interface IBook {
  bauthor: string;
  bdate: string;
  btranslator: string;
  bpublisher: string;
  btitle: string;
  bprice: number;
  bisbn: string;
  bimgurl: string;
}

@Component({
  selector: 'app-list-box',
  templateUrl: './list-box.component.html',
  styleUrls: ['./list-box.component.css']
})
export class ListBoxComponent implements OnInit {

  books: IBook[];

  constructor(private http: HttpClient) {
    this.http.get<IBook[]>('assets/data/book.json')
      .subscribe(res => this.books = res);
  }

  ngOnInit() {
  }

}

약간 이상한 코드가 들어있는데 그 이유는 책의 정보를 가져오기 위해 HttpClient의 get() method를 호출하면서 Arrow Function을 이용해 코드를 작성했기 때문입니다. 이 부분은 나중에 RxJS를 설명할 때 더 자세히 봐야 할 듯 하고 지금은 book.json에 대한 HTTP연결로 JSON 데이터를 가져온다고 이해하시면 충분합니다.


 문제점

도서정보에 대한 JSON을 작성해서 실행해보시면 아시겠지만 출력은 잘 됩니다.

하지만 몇가지 문제가 있습니다.

  • 책이 100권이 있으면 밑으로 쭉 나열되게 됩니다. Paging 처리를 해야 하는데 이것또한 쉬운작업은 아닙니다.

  • Event 처리하기가 쉽지 않습니다. 각 행을 클릭하면 해당 책의 세부정보를 detail-box Component를 이용하여 View에 출력해야 합니다. 클릭이벤트를 처리하기가 쉽지 않네요.

  • book-search-main.component.html에서 만들어놓은 Select Box의 선택 정보를 알아와서 그에 맞추어 책들을 필터링 해야 하는데 어떤 도서를 선택했는지 현재로서는 알 방법이 없습니다.

이 외에도 Table Header를 클릭해서 리스트를 Sorting하는 것과 같은 일반적인 테이블이 가지는 기능을 우리가 추가로 구현해야되는 문제가 있습니다. 제대로 사용할려면 부가적인 작업이 훨씬 더 많이 들어가야 합니다.

이와 같은 문제를 Material의 Table Component를 이용하면 쉽게 해결할 수 있습니다. 다음 포스트에서는 도서 리스트를 출력하는 부분을 Material Table Component를 이용하여 다시 작성해 보겠습니다.

728x90
반응형

'Web Programming > Angular - TypeScript' 카테고리의 다른 글

Angular @Input 데이터공유  (0) 2018.08.28
Angular Material Table  (0) 2018.08.28
SPA형식의 Web Application  (0) 2018.08.28
Angular 개발환경세팅 하기  (0) 2018.08.28
Angular 소개  (0) 2018.08.28

+ Recent posts