728x90
반응형

Directive

이번 포스트에서는 Angular의 Directive에 대해서 알아보겠습니다. 지금까지 예제를 작성하면서 여러 directive를 사용해 왔는데요. 그에 대해서 조금 더 정리를 해 보려 합니다.

Directive는 DOM의 모양이나 동작을 지시하기 위한 명령이라고 생각하시면 됩니다. 크게 HTML Element형태로 사용되거나 Element의 attribute의 형태로 사용되게 됩니다. 예를 들면 우리가 사용했던 ngIf 같은 attribute를 떠올리시면 됩니다. Routing에서 사용했던 <router-outlet></router-outlet> Element 역시 directive입니다.

이렇게 directive는 DOM을 제어하기 위한 용도로 사용되는데 DOM을 제어하기 위해서 우리는 지금까지 Component를 이용했었습니다. Component로 제어하면 되지 굳이 directive로 DOM을 제어할 필요가 있느냐 하는 의문이 들 수 있습니다. 하지만 Component는 원칙적으로 자신이 rendering하는 View에 대해서만 관심이 있습니다. 여러 View들이 공통적으로 사용하는 Element나 Element의 attribute같은 것들을 따로 directive로 지정해 사용하면 SRP관점에서 봤을 때도 타당하고 Component의 복잡도를 낮출 수 있으며 유지보수와 같은 관리적인 측면에서도 더 나은 형태로 구현이 가능합니다.

Component 역시 큰 의미에서 directive입니다. Component는 directive이면서 View를 가지고 있고 자식 Component 또한 가질 수 있습니다. 하지만 directive는 View를 가지고 있지 않고 자식 directive 또한 가질 수 없습니다.

Directive는 크게 다음과 같은 4가지 종류로 구분 할 수 있습니다.

  • Component Directive

    우리가 알고 있는 Component입니다. selector에서 우리가 directive를 지정해 사용하게 됩니다.


  • Attribute Directive

    HTML Element의 Attribute로 사용됩니다. built-in 형태로는 ngClass 같은 것들이 있습니다.


  • Structural Directive

    DOM의 구성을 제어하기 위한 directive로 ngIf , ngForngSwitch 등이 있습니다.


  • Custom Directive

    built-in 형태로 만들어진 directive가 아닌 직접 만들어서 사용하는 directive를 지칭합니다.


이 중 Component에 대해서는 많이 사용해 봤으니 Structural Directive와 Custom Directive에 대해서 알아보도록 하겠습니다.


 Structural Directive

우리 예제에서도 사용했던 ngIfngFor등을 지칭합니다. 단, 이것들은 built-in된 형태입니다. 이런 구조적 directive를 우리가 직접 만들어서 사용할 수도 있습니다. ngIf와 ngFor를 사용하실 때 몇가지 알아두셔야 하는 사항이 있습니다.

일반적으로 다음과 같이 코드처리 합니다.

<div *ngIf="hero" class="name"></div>

hero가 null과 undefined가 아니면 <div>를 DOM에 추가하고 interpolation을 이용해 값을 출력하는 코드입니다.

이 코드는 사실 다음과 같이 변형되어서 실행됩니다.

<ng-template [ngIf]="hero">
  <div class="name"></div>
</ng-template>

ng-template을 이용해서 조건을 통해 <div>의 처리를 결정합니다.

비교해서 보셔야 할 것으로 ng-container가 있습니다. 위의 코드는 다음의 코드로 표현이 가능합니다.

<ng-container *ngIf="hero">
  <div class="name"></div>
</ng-container>

Angular는 동일한 Element에 두개 이상의 *ngIf*ngFor*ngSwitch를 사용할 수 없습니다. for문을 돌리면서 if문을 이용해 비교하고 싶은 경우에는 문제가 됩니다. 이 같은 경우를 해결하기 위해 ng-container를 제공하는 것이라 보시면 됩니다.


 Custom Directive

기존에 만들었던 mySearchProject에 간단하게 Custom Directive를 추가하고 어떻게 이용하는지 살펴보도록 하겠습니다.

command 창을 열고 다음의 명령을 실행해서 directive를 하나 생성합니다. 현재 command 창의 working directory는 src/app 입니다.

ng generate directive textColor

두개의 파일이 생성됩니다. Angular CLI의 이름규칙에 의해 text-color.directive.ts 라는 이름의 파일이 생성됩니다.

그 내용을 다음과 같이 수정합니다.

import {Directive, ElementRef, Renderer2} from '@angular/core';

@Directive({
  selector: '[myColor]'
})
export class TextColorDirective {

  constructor(elementref: ElementRef, renderer: Renderer2) {
    renderer.setStyle(elementref.nativeElement,'color','darkred');
  }

}

위의 directive 내용은 HTML Element의 속성으로 myColor 가 사용되었을 경우 해당 Element를 DOM에 rendering 할 때 글자색을 darkred로 설정하라는 것입니다.

이렇게 directive를 생성하면 사용하기 위해서 Root Module에 등록해야 합니다. Angular CLI를 이용했기 때문에 이미 등록이 되어 있습니다. 이제 실제 해당 directive가 정상적으로 동작하는지 확인해보면 될 듯 합니다.

pages/home 폴더에 있는 home.component.html을 수정해서 해당 directive를 사용해 보죠.

<h1>HOME</h1>
<hr>
<p myColor>이 Web Application은 Angular 강좌를 위한 Test App입니다.
<p>이 강좌는 다음의 내용을 포함합니다. </p> <ul> <li>Angular의 기본 구조</li> <li>Angular CLI</li> <li>Component</li> <li>Template - Template Reference Variable</li> ... ... ...

위와 같이 myColor라는 directive를 사용할 수 있습니다. 해당 P Element의 text 글자는 darkred로 출력되겠네요.

가장 직관적인 예를 들기 위해 text color를 변경하는걸로 처리를 했는데 그 외 DOM을 제어하는 다른 기능들도 할 수 있습니다.

이번에는 이벤트 처리를 한번 해 보죠. 위의 예에서 해당 P Element를 클릭했을 때 alert()이 수행되게 할려면 다음과 같이 처리하시면 됩니다.

import {Directive, ElementRef, HostListener, Renderer2} from '@angular/core';

@Directive({
  selector: '[myColor]'
})
export class TextColorDirective {

  @HostListener('click', ['$event']) elementClick(e) {
    // e안에는 event객체가 들어온다.
    // this는 directive 객체를 지칭.
    alert(e.srcElement.innerHTML)
  }

  constructor(elementref: ElementRef, renderer: Renderer2) {
    renderer.setStyle(elementref.nativeElement,'color','darkred');
  }

}

이벤트를 처리하기 위해 @HostListener decorator를 이용했습니다. 여기서 host라는 표현이 나오는데 지금 우리 예제에서 myColor 속성을 적용한 HTML Element를 host라고 지칭합니다.

이벤트 객체를 얻기 위해 처리하는 부분을 조심해서 보시면 됩니다.

추가적으로 @Input decorator를 이용해서 directive가 값을 전달 받을 수 있습니다. 원래 @Input은 부모 Component가 자식 Component 에게 데이터를 전달해 주기 위해 사용했었는데 directive에게도 값을 전달 할 수 있습니다. 사용하는 Component때 했던 것과 동일합니다.

이번 포스트는 Directive에 대해서 정리해보았습니다. 더 많은 기능이 있지만 기본적으로 이런 용도로 사용된다는 정도만 알고 계시면 될 듯 합니다.

728x90
반응형

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

Angular Component LifeCycle  (0) 2018.08.28
Angular Pipe  (0) 2018.08.28
Angular Material Table Event  (0) 2018.08.28
Angular Service RxJS  (0) 2018.08.28
Angular Service Mediator Pattern  (0) 2018.08.28
728x90
반응형

 Performance Issue

먼저 얘기할 부분은 Angular의 Performance 입니다.

기존 AngularJS에 비해서 Angualr는 여러 관점에서 성능향상이 이루어졌습니다. 하나씩 간단하게 살펴보겠습니다.

  • Digest Loop로 인한 성능 저하 문제가 개선되었습니다. 우리 예제에서 다뤄보았던 양방향 바인딩 기억하시나요? AngularJS에서는 이 양방향 바인딩을 처리하기 위해 watcher라는걸 사용합니다. 이 watcher라는 놈이 수행될 때 마다 digest loop가 수행되어 양방향 바인딩을 처리하는 구조였습니다. 양방향 바인딩이 많아질수록 watcher가 증가하고 결과적으로 digest loop의 수행도 같이 증가해서 결국 성능이 저하되는 문제가 발생했던 겁니다. 지금의 Angualr는 전통적인 양방향 바인딩을 다른 방식으로 처리함으로써 이 문제를 해결했습니다.

    현재 Agnular에서 양방향 바인딩은 내부적으로 Property binding과 Event binding으로 변환되어서 처리됩니다. 만약 Angular의 Change Detection에 대해 자세히 알고 싶으시면 여기를 참고하세요.


  • Angular는 AoT Compile을 지원합니다. AoT(Ahead of Time) compile이란 사전 컴파일을 지칭합니다. 런타임에 컴파일을 하지 않고 미리 컴파일을 해 놓기 때문에 실행 속도를 더 높일 수 있는 것이죠. 안드로이드 개발을 해 보신 분이라면 Dalvik과 ART를 알고 계실 겁니다. Dalvik은 JIT 컴파일러를 이용하고 ART는 AoT 컴파일러를 사용합니다. 둘을 비교해보자면 JIT(Just-In-Time)은 실행시점에 소스코드를 컴파일하는 방식입니다. 설치는 빠르게 되겠지만 실행이 좀 느리겠지요. AoT는 설치시점에 소스코드를 컴파일하는 방식입니다. 따라서 설치가 조금 느려지지만 실행시 빠르게 실행되는 장점을 가지고 있습니다.


  • Angular뿐만 아니라 모든 Front-End Framework이 가지는 문제점 중 하나는 바로 SPA(Single Page Application) 구조에서 오는 rendering의 속도 문제입니다. 우리는 기존에 SSR(Server Side Rendering)을 이용해 프로그램을 했었습니다. 예를 들면, JSP 이런것들 이지요. SSR의 장점은 일단 클라이언트에게 빠르게 화면을 보여줄 수 있다는 겁니다. 하지만 라이브러리가 다 로딩된 후에야 Client와 interaction이 가능합니다. 반면에 SPA 구조는 모든 라이브러리가 다 로딩되어야 Client가 화면을 볼 수 있습니다. 즉, Client가 초기에 화면을 보기까지 로딩시간이 걸린다는 것입니다. CSR(Client Side Rendering) 이라고 불리는 이 방식은 화면이 뜨자마자 바로 interaction이 가능하다는 장점이 있습니다. 요즘에는 이 둘을 섞어서 최적화를 꾀하고 있습니다. 이와 관련된 사항으로 Angular는 Lazy Loading을 사용합니다. Lazy Loading(지연 로딩)은 SPA의 단점을 극복하기 위한 하나의 방법으로 사용하는 모든 라이브러리를 한꺼번에 다 불러들이지 않고 필요한 시점에 필요한 라이브러리만 로딩하는 방식입니다. 이를 이용해 로딩 시간을 단축할 수 있기 때문입니다.

 Angular CLI

그 다음 살펴볼 부분은 Angular CLI입니다. 우리 예제 프로그램을 작성하기 위해 사용했던 것 기억하시지요?

Angular CLI(Command Line Interpreter)는 command 명령어를 이용해서 프로젝트의 생성 및 Scaffolding, build, 개발서버를 이용한 실행과 테스팅까지 수행할 수 있는 개발지원 도구 입니다. 간단하지 않는 프로젝트의 구조 생성 및 설정 그리고 Coding Convention등을 자동으로 잡아주기 때문에 편하게 개발환경을 구축하고 표준적 방식으로 프로젝트를 진행할 수 있도록 도와주는 아주 고마운 도구이지요.

다음은 일반적으로 많이 사용되는 Angular CLI의 명령어와 알아두어야 할 사항들 입니다.

  • 프로젝트의 생성
    ng new <project_name>
    

    위와 같은 형태로 프로젝트를 생성하면 기본 폴더 구조와 파일이 생성됩니다. 이전에도 언급했지만 이렇게 생성된 프로젝트의 구조와 Coding Convention이 Angular의 표준 스타일입니다. 또한 필요한 의존 Module이 같이 설치됩니다. 만약 의존 Module을 설치하고 싶지 않을 경우 --skip-install option을 이용해 프로젝트를 생성하시면 됩니다.


  • 내장 개발서버를 이용한 프로젝트 실행
    ng serve
    

    위와 같은 명령을 이용하면 내장 개발 서버를 이용해 우리 프로젝트를 deploy할 수 있습니다. 기본적으로 사용하는 port는 4200 입니다. 만약 다른 포트를 사용하고 싶을 때는 --port option을 이용해 포트번호를 변경할 수 있습니다. 그리고 --ooption을 이용하면 시스템에 설정되어 있는 default browser를 실행해 http://localhost:4200 으로 접속까지 해 줍니다.

    이 내장 개발서버는 LiveReload 기능을 지원합니다. 쉽게말하자면 우리가 코드를 수정하면 그 내용을 컴파일하여 즉각 반영하고 browser를 refresh까지 시켜준다는 것이지요. 개발을 편하게 가져갈 수 있습니다.


  • 프로젝트 구성요소 추가
    ng generate <구성요소> <구성요소명>
    

    위와 같은 명령을 이용하면 프로젝트에 새로운 Angular 구성요소를 추가할 수 있습니다. 우리는 componentservice 구성요소를 추가해 봤지요. 그 외에 다음과 같은 Angular 구성요소들을 추가할 수 있습니다.

    • directive
    • pipe
    • module
    • class
    • interface
    • enum
    • guard

    이런 구성요소가 어떤 역할을 하는지는 나중에 하나씩 살펴봐야 겠네요. 기존에 우리 프로젝트에서 추가했던 search-box Component를 가지고 간단하게 Naming Rule에 대해 정리를 한번 해 보겠습니다.

    다음과 같은 명령을 실행하면 새로운 Component가 하나 만들어 집니다.

    ng generate component search-box
    

    생성되는 파일은 총 4개입니다. option을 설정하면 파일이 만들어지지 않을수도 있습니다. 즉, CSS, HTML, SPEC파일을 생성하지 않을수도 있다는 말입니다. CSS와 HTML을 inline방식으로 사용할 수 있으니까요. SPEC은 테스트를 위한 파일입니다.

    그런데 만들어지는 파일의 이름이 좀 특이합니다. 다음과 같은 형태로 만들어졌습니다.

    search-box-component.css     
    search-box-component.html    
    search-box-component.ts
    search-box-component.spec.ts
    

    파일명이 모두 search-box.component.* 형태로 만들어집니다. Angular는 Naming의 혼란을 방지하기 위해 케밥 표기법(Kebab case)을 이용해서 Angular 구성요소의 이름을 붙이게 됩니다. 사실 우리에게 익숙한 표기법은 Camel case입니다. 각 단어의 첫글자를 소문자로 쓰고 붙어있는 단어들은 대문자로 시작하는 표기법입니다. Java의 기본 표기법이죠. 참고로 Pascal case도 있습니다. Camel case와 유사한데 Pascal case는 첫 글자도 대문자로 시작합니다. Kebab-case는 하이픈(-)을 이용한 표기법입니다. Angular는 기본적으로 Kebab case를 Angular 구성요소 이름을 붙일 때 사용한다는걸 기억하시면 됩니다. 참고로 class의 이름은 Pascal case를 이용합니다.

    이렇게 만들어진 search-box-component.ts 파일의 내용을 살펴보죠

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

    위의 코드에서 selector가 어떤 역할을 하는지 아실겁니다. 이 selector에 명시된 이름의 tag를 다른 template에서 이용하면 해당 tag 위치에 우리 Component에서 지정한 template이 표시되게 됩니다. 이 selector에 명시된 이름이 결국 <app-search-box></app-search-box>형태로 어딘가에서 이용된다는 말이죠. 이 selector에 지정된 이름을 붙일 때 접두어 app을 이용해 Kebab case로 표현합니다. 기존의 HTML tag와 혼동되는걸 피하기 위해서라고 생각하시면 됩니다.

    참고로 이 접두어는 .angular-cli.json 설정파일에서 다른것으로 지정할 수 있습니다.

    우리는 CSS와 HTML을 따로 파일을 만들어서 사용했지만 inline형태로 이용할 수 있습니다. 아래와 같이 말이죠.

    import { Component, OnInit } from '@angular/core';
      
    @Component({
      selector: 'app-search-box',
      template: `<h1>Hello<h1>
                 <p>
                  this is a sample text
                 </p>
                `,
      style: `.myStyle {
                  color : red
              }
             `
    })
    export class SearchBoxComponent implements OnInit {
      
      constructor() { }
      
      ngOnInit() {
      }  
    }  
    

    여기서는 backquote ( ` ) 기호를 이용하는 template string을 사용했습니다. 다른 Angular 구성요소를 추가하는 방법과 이용법은 이후 포스트에서 하나씩 실습을 통해 알아보도록 하겠습니다.


  • 프로젝트 Build
    ng build
    

    위와 같은 명령을 이용하면 우리 프로젝트를 Angular CLI를 이용해 Build할 수 있습니다. 기존에는 SystemJS를 이용해서 bundling을 했지만 현재는 webpack을 이용해 bundling합니다. option을 따로 주지 않고 build를 진행하면 개발환경 build를 수행하게 됩니다. 만약 production build를 수행하시려면 --prod option을 이용하시면 됩니다. 또한 deploy를 위한 base url을 설정할 때는 --base-href option을 이용하시면 됩니다. Angular CLI Build에 관한 보다 자세한 option 사항은 여기를 참조하시면 됩니다.


이번 포스트는 기존에 설명하기 좀 난감(?)했던 성능문제라던지 Angular CLI의 Naming Rule에 대해 보충설명을 진행했습니다. 만약 Angular CLI에 대해 더 자세하게 알고 싶으시면 여기를 살펴보시면 됩니다.

728x90
반응형
728x90
반응형

Angular Material Table Event

이번 포스트는 list-box Component의 Angular Material Table의 row를 클릭하면 해당 책의 세부정보를 detail-box Component의 View에 출력하는 부분을 작성해 보겠습니다. 이 작업 역시 BehaviorSubject를 이용해 선택한 책의 정보를 자동으로 갱신해 출력할 수 있도록 처리하겠습니다.

Service에 Client에 의해 선택된 책의 세부정보가 담긴 객체정보가 존재해야 합니다.detail-box Component는 이 정보를 subscribe해야 하고 만약 Client에 의해 다른 책이 선택된다면 이 정보를 갱신해주면 될 듯 합니다.

다음은 http-support.service.ts 파일의 내용입니다.

import { Injectable } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { BehaviorSubject } from "rxjs/BehaviorSubject";

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

@Injectable()
export class HttpSupportService {

  books: IBook[];

  // Client에 의해 선택된 책의 정보 - 초기화
  selectedBook: IBook = {
    bauthor: '',
    bdate: '',
    btranslator: '',
    bpublisher: '',
    btitle: '',
    bprice: 0,
    bisbn: '',
    bimgurl: ''
  };

  constructor(private http: HttpClient) { }

  updateBooks: BehaviorSubject<IBook[]> = new BehaviorSubject<IBook[]>(this.books);
  
  // selectedBook에 대한 BehaviorSubject 객체 생성
  updateSelectedBook: BehaviorSubject<IBook> = new BehaviorSubject<IBook>(this.selectedBook);

  getJsonData(url:string, name:string, category:string, keyword:string) {
    this.http.get<IBook[]>(`${url}${name}`)
        .subscribe(res => {
           let tmp = null;
           // 도서종류와 검색어를 이용한 도서 데이터 Filtering 시작
           if( category == 'all' ) {
             tmp = res.filter(function(item,idx,arr) {
               if(item.btitle.includes(keyword)) {
                 return true;
               } else {
                 return false;
               }
             });
           } else if( category == 'country') {
             tmp = res.filter(function(item,idx,arr) {
               if(item.btitle.includes(keyword)) {
                 return true;
               } else {
                 return false;
               }
             }).filter(function(item,idx,arr) {
               if(item.btranslator == '') {
                 return true;
               } else {
                 return false;
               }
             });
           } else if( category == 'foreign') {
             tmp = res.filter(function(item,idx,arr) {
               if(item.btitle.includes(keyword)) {
                 return true;
               } else {
                 return false;
               }
             }).filter(function(item,idx,arr) {
               if(item.btranslator != '') {
                 return true;
               } else {
                 return false;
               }
             });
           }
          // 도서종류와 검색어를 이용한 도서 데이터 Filtering 끝
          this.updateBooks.next(tmp);
          //this.books = tmp;
          //console.log(this.books);
        });
  }

  getBooks(): IBook[] {
    return this.books;
  }
}

이제 Material Table row event를 처리해야 합니다. 이 부분은 API 사용이기 때문에 별다른 설명이 없습니다. 다음의 코드를 이용하시면 됩니다.

다음은 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">  </mat-cell>
    </ng-container>

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

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

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

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

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

아래쪽에 <mat-row>에 대한 이벤트 처리만 유의해서 보시면 됩니다. Table의 각 row를 클릭할 때 마다 rowSelect()를 이용해 method를 호출하면서 현재 선택된 row 객체를 인자로 넘겨줍니다.

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

import { Component, OnInit } from '@angular/core';
import { MatTableDataSource } from '@angular/material';
import { MatPaginator } from '@angular/material';
import { ViewChild } from '@angular/core';
import { HttpSupportService } from "../http-support.service";
import { SelectionModel } from '@angular/cdk/collections';

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[];

  // event 처리
  selection = new SelectionModel<IBook>(false, []);

  @ViewChild(MatPaginator) paginator: MatPaginator;

  constructor(private httpSupportService:HttpSupportService) {
    this.httpSupportService.updateBooks.subscribe(data => {
      this.books = data;
      this.dataSource = new MatTableDataSource<IBook>(this.books);
      this.dataSource.paginator = this.paginator;
    })
  }

  rowSelect(row) {
    this.selection.select(row);
    this.httpSupportService.updateSelectedBook.next(this.selection.selected[0]);
  }

}

SelectionModel을 이용해 사용자가 선택한 row에 대한 정보를 획득합니다. Service의 updateSelectedBook의 next()를 호출하여 detail-box Component에서 구독하고 있을 선택된 책의 정보를 갱신합니다.

마지막으로 detail-box.component.ts 파일입니다.

import { Component, OnInit } from '@angular/core';
import {HttpSupportService} from "../http-support.service";

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

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

  book: IBook;

  constructor(private httpSupportService:HttpSupportService) {
    this.httpSupportService.updateSelectedBook.subscribe(selectedBook => {
      this.book = selectedBook;
    });
  }

  ngOnInit() {
  }

}

기존에 sample data를 삭제하고 Service를 이용하여 updateSelectedBook 객체를 subscribe하게끔 처리했습니다.

Table의 이벤트 처리하는 부분만 잘 보시면 됩니다. 데이터 공유는 이전 포스트에서 설명했던 방식 그대로 똑같이 적용해서 처리했습니다.

여기까지해서 기본적인 도서 검색에 대한 프로그램을 완성했습니다. 다음 포스트들은 실습과 약간 무관하지만 알아야 하는 부분들에 대해서 정리해 보겠습니다.

728x90
반응형

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

Angular Pipe  (0) 2018.08.28
Angular Directive  (0) 2018.08.28
Angular Service RxJS  (0) 2018.08.28
Angular Service Mediator Pattern  (0) 2018.08.28
Angular Service  (0) 2018.08.28
728x90
반응형

Angular RxJS

이번 포스트는 RxJS를 이용해 데이터를 공유하는 방법에 대해서 알아보겠습니다. RxJS에 대한 이론적인 내용은 다른 포스트에서 알아보기로 하고 여기서는 RxJS를 이용해 우리 예제를 어떻게 구현할 수 있나에 초점을 맞추어 보도록 하죠.

지금까지 작성한 우리 예제 프로그램은 search-box Component에서 Search 버튼을 클릭하면 Service의 method가 호출되서 JSON 데이터를 확보합니다. 이 데이터를 자동으로 list-box Component에 전달할 방법이 없었기 때문에 list-box Component에서 Service를 이용해 데이터를 가져가는 방식으로 동작했습니다.

여기서는 BehaviorSubject class를 이용해 보도록 하겠습니다. 이 BehaviorSubject는 Observable의 특별한 형태라고 보시면 됩니다. 연관된 데이터를 쉽게 구독할 수 있는 method와 데이터를 변경하기 위한 method등 사용하기 쉬운 몇가지 method를 제공해서 보다 쉽게 RxJS 기능을 이용할 수 있도록 고안된 class입니다.

먼저 http-support.service.ts 파일에 BehaviorSubject class 타입의 객체를 하나 생성합니다. 연관된 데이터를 생성자의 인자로 이용해서 객체를 생성합니다.

updateBooks: BehaviorSubject<IBook[]> = new BehaviorSubject<IBook[]>(this.books);

새로운 JSON 데이터가 만들어지면 updateBooks에 데이터를 밀어 넣습니다. 다음과 같은 method를 이용합니다.

this.updateBooks.next(tmp);
//this.books = tmp;

기존에는 JSON 데이터를 만들어서 this.books에 바로 assign했는데 이 데이터를 updateBooks의 method를 이용하여 books에 밀어 넣습니다.

이제 list-box Component를 수정할 차례입니다. 데이터를 가져오기 위한 버튼은 필요없으니 HTML에서 삭제합니다. 또한 버튼이 클릭되었을 때 Service에서 데이터를 가져오는 method도 의미가 없으니 삭제해야 겠네요.

list-box Component는 단순합니다. 주입된 Service의 updateBooks를 구독하고 있다가 데이터가 변경되면 변경된 데이터를 자동으로 가져와 Table의 DataSource에 injection하는 코드만 작성하면 됩니다. 생성자에서 처리하면 될 듯 합니다.

  constructor(private httpSupportService:HttpSupportService) {
    this.httpSupportService.updateBooks.subscribe(data => {
      this.books = data;
      this.dataSource = new MatTableDataSource<IBook>(this.books);
      this.dataSource.paginator = this.paginator;
    })
  }

BehaviorSubject class의 도움을 받으면 RxJS의 이론적인 내용과 Observable에 대한 내용을 잘 몰라도 쉽게 구현이 가능합니다. 하지만 나중에 RxJS의 이론적인 내용은 한번 보셔야 합니다.

마지막으로 부모 Component의 초기화 버튼을 누르면 list-box에 출력된 도서 내용도 초기화 되어야 합니다. 이 부분은 위의 내용을 응용해 구현해 보세요!!

우리 프로그램이 거의 완성되어가고 있습니다. 다음은 list-box에서 책을 선택하면 해당 책의 세부정보가 detail-box에 출력되도록 처리해 보겠습니다.

728x90
반응형

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

Angular Directive  (0) 2018.08.28
Angular Material Table Event  (0) 2018.08.28
Angular Service Mediator Pattern  (0) 2018.08.28
Angular Service  (0) 2018.08.28
Angular Content Projection 데이터공유  (0) 2018.08.28
728x90
반응형

Service Mediator Pattern

이번 포스트는 Service의 개념을 이용해서 Component간 데이터를 공유하는 Service Mediator Pattern에 대해서 알아보겠습니다.

먼저 간단한 경우부터 살펴보기로 하죠.

도서종류와 검색어를 입력하고 Search! 버튼을 클릭하면 Service를 이용해서 JSON 파일로부터 데이터를 읽어들입니다. 원래는 RESTful 서버를 이용해서 JSON 데이터를 가져와야 하지만 우리는 RESTful 서버를 이용하지 않으니 일단 JSON 파일로 부터 데이터를 읽어들이고 데이터를 filtering해서 사용하겠습니다.

먼저 부모 Component인 book-search-main Component에서 선택된 도서종류를 search-box Component에서 사용해야 하므로 선택된 도서종류에 대한 값을 search-boxComponent에서 사용할 수 있도록 코드를 수정합니다.

다음은 book-search-main.component.html 중 일부 입니다.

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

@Input decorator로 데이터를 받기 위해 search-box.component.ts를 수정해야 합니다.

다음은 search-box.component.ts 파일입니다.

import {
  Component, OnInit,
  Input, Output, EventEmitter
} from '@angular/core';
import { HttpSupportService } from "../http-support.service";
import {JSON_DATA_CONFIG, JsonConfig} from "./json-config";


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

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

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

  }

  @Output() searchEvent = new EventEmitter();

  keyword = null;

  constructor(private httpSupportService:HttpSupportService,
              private jsonConfig:JsonConfig) { }

  ngOnInit() {
  }

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

    this.httpSupportService.getJsonData(
      this.jsonConfig.url,
      this.jsonConfig.name,
      this.selectedValue,
      this.keyword);
  }

  inputChange(): void {

  }
}

부모 Component로부터 받은 도서종류와 Client로부터 입력받은 keyword를 가지고 injection된 Service의 method를 호출합니다.

다음은 http-support.service.ts 파일입니다.

import { Injectable } 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;
}

@Injectable()
export class HttpSupportService {

  books: IBook[];
  constructor(private http: HttpClient) { }

  getJsonData(url:string, name:string, category:string, keyword:string) {
    this.http.get<IBook[]>(`${url}${name}`)
        .subscribe(res => {
           let tmp = null;
           // 도서종류와 검색어를 이용한 도서 데이터 Filtering 시작
if( category == 'all' ) { tmp = res.filter(function(item,idx,arr) { if(item.btitle.includes(keyword)) { return true; } else { return false; } }); } else if( category == 'country') { tmp = res.filter(function(item,idx,arr) { if(item.btitle.includes(keyword)) { return true; } else { return false; } }).filter(function(item,idx,arr) { if(item.btranslator == '') { return true; } else { return false; } }); } else if( category == 'foreign') { tmp = res.filter(function(item,idx,arr) { if(item.btitle.includes(keyword)) { return true; } else { return false; } }).filter(function(item,idx,arr) { if(item.btranslator != '') { return true; } else { return false; } }); } // 도서종류와 검색어를 이용한 도서 데이터 Filtering 끝 this.books = tmp; console.log(this.books); }); } getBooks(): IBook[] { return this.books; } }

Filtering처리를 해야해서 코드가 좀 길어졌네요. 알기 쉽게 좀 풀어서 코드를 작성했습니다. Filtering처리된 JSON 데이터를 얻어와서 일단 books 속성에 저장했습니다. 그리고 list-box Component에서 데이터를 가져가기 위해 getBooks() method를 하나 작성했습니다.

이제 데이터를 가져가는 list-box Component를 살펴보면 됩니다. list-box Component에서 데이터를 가져가기 위한 버튼을 하나 준비합니다.

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

<br>
<button mat-raised-button color="warn"
        (click)="getData()">Get DATA!</button>
<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">  </mat-cell>
    </ng-container>

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

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

    <ng-container matColumnDef="bprice">
      <mat-header-cell *matHeaderCellDef> Price </mat-header-cell>
      <mat-cell *matCellDef="let element">  </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 상단에 Get DATA!라는 버튼을 만들고 event binding을 시켰습니다. 마지막으로 list-box.component.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';
import {HttpSupportService} from "../http-support.service";

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 httpSupportService:HttpSupportService) {
  }

  getData(): void {
    this.books = this.httpSupportService.getBooks();
    this.dataSource = new MatTableDataSource<IBook>(this.books);
    this.dataSource.paginator = this.paginator;
  }

}

주입된 Service객체를 이용해서 Service에 저장되 있는 JSON데이터를 가져다가 Table의 DataSource에 설정하게 됩니다.


내용이 좀 많지만 천천히 따라가면서 살펴보시면 어렵지 않게 이해하실 수 있습니다. 그림으로 보자면 아래와 같은 형태입니다.

service-mediator-pattern


동작은 잘 하지만 list-box Component에 데이터를 가져오기 위해서 버튼을 한번 더 클릭해야 한다는 것이 좀 그렇네요. Service에 의해서 데이터가 공유되는 건 확인했지만 새로 검색을 해서 데이터가 변경되면 당연히 list쪽에서는 데이터가 자동으로 변경되지 않습니다.

이 문제는 RxJS를 이용해서 처리할 수 있습니다. 다음 포스트에서는 RxJS를 이용해서 데이터의 흐름을 subscribe(구독) 하고 구독하고 있는 데이터를 어떻게 변경해야 하는지에 대해서 알아보겠습니다.

728x90
반응형

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

Angular Material Table Event  (0) 2018.08.28
Angular Service RxJS  (0) 2018.08.28
Angular Service  (0) 2018.08.28
Angular Content Projection 데이터공유  (0) 2018.08.28
Angular @ViewChild 데이터공유  (0) 2018.08.28

+ Recent posts