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
728x90
반응형

Service

이번 포스트는 Angular의 Service에 대해서 알아보겠습니다. 먼저 Service의 기본적인 사항들을 알아본 후 이를 통해 데이터를 공유하는 Service Mediator Pattern으로 넘어가면 될 듯 하네요.

이 Service는 Angular에만 존재하는 개념이 아닙니다. 객체지향 프로그래밍을 다뤄보신 분은 익히 들어본 개념입니다. 특히 Spring과 같은 Framework이나 MVC Pattern을 다뤄보신 분들이라면 쉽게 이해하실 수 있는 내용입니다.

우리는 Angular를 하고 있으니 여기에 맞춰 설명을 하자면 Component는 View를 표현하고 관리하는게 주된 역할입니다. 즉, 데이터를 받아와서 View에 출력한다던지 View의 값이 변경되면 그걸 또 어떻게 처리한다던지하는 View와 밀접한 로직을 Component class가 가지고 있게 됩니다.

만약 View를 처리하는 로직 이외의 별도의 로직이 필요하면 그 로직은 어디에 두는것이 좋을까요? 로그인 처리를 할때 필요한 인증로직이라던지 혹은 서버와의 데이터 통신을 위한 REST 서버의 호출같은 로직을 그냥 필요할 때마다 Component안에 집어 넣어서 처리하는게 좋을까요?

우리는 CBD(Component Based Development)를 하고 있습니다. 각각의 Component는 자신의 주된 관심사에 집중하게끔 코드를 작성해야 합니다. 객체지향설계에서 얘기하는 SRP(Single Responsibility Principle)을 생각하시면 됩니다. Component안에 다른 관심사가 존재하면 Component의 독립성이 보장되지 못하고 결국 중복 코드가 발생하며 Component의 재사용과 유지보수에 문제가 발생하게 되겠죠.

그래서 위에서 얘기한 별도의 로직들은 다른 곳에서 관리할 필요가 있습니다. Service라는 걸 이용해서 이 로직들을 작성하고 다른 Component에서 이 Service를 가져다가 사용하는 식으로 관리를 하면 SoC(Separation of Concern)원칙에 잘 들어맞을 거 같습니다.

이렇게 Component와 Service를 분리해서 작성하고 Component에서 Service를 사용하는 건 좋은데 사용할 때 문제가 하나 있습니다. Dependency라는게 생기는 거죠. 쉽게 단위 코드로 Component에서 Service를 사용하는 예를 한번 보죠. 아래는 Component class입니다.

MyService service = new MyService();
service.getUserAuth('moon9342');

pseudocode 입니다. Component class안에서 직접 Service 객체를 생성해서 이용하는 경우입니다. 이런 경우 우리 Component는 Service에 의존하게 됩니다. 이걸 Dependency Relationship(의존관계)이 존재한다 라고 표현하기도 합니다. 이 때 Component class의 입장에서 Service 객체를 Dependency라고 표현합니다.

이렇게 의존관계가 성립되면 Service가 변경되었을 때 우리 Component는 그에 따른 영향을 받을 수 밖에 없습니다. 연관관계가 강하게 성립되어서 서로 독립적으로 사용하는게 힘들어지는것이고 재사용이나 유지보수에 문제가 생기게 되겠네요.

이 문제를 해결하는 Design Pattern이 바로 DI(Dependency Injection)입니다. 우리 Service객체(Dependency)를 사용하는 객체인 Component에게 주입해서 사용하는 것입니다. 주입하는 방법은 일반적으로 constructor를 이용하는 방법과 setter를 이용하는 방법이 있는데 Angular는 constructor injection을 지원합니다.

즉, Component가 직접 Service를 new keyword로 생성하는 것이 아니라 Angular Framework이 Service를 Component가 사용할 있도록 Service객체를 생성해서 Component에게 넣어주는 방식입니다. 이걸 IoC(Inversion of Control)라고 합니다. Angular Framework은 IoC Container입니다.

이론적인 배경을 간단히 설명했으니 이제 Service를 우리 예제에 추가해보겠습니다. Angular application은 Module의 집합입니다. Module은 크게 Feature Module과 Shared Module이 있다는 얘기 혹시 기억하시나요? 어디에 Service를 생성하느냐 하는 문제인데 사실 case-by-case입니다. 특성상 여러 Feature Module에서 사용하는 공통 로직의 개념이면 따로 Shared Module을 만들어서 그 안에 Service를 포함시키는게 좋습니다. 하지만 우리 예제처럼 bookSearch Module에서만 사용할 생각이면 해당 Module안에 포함시키는게 더 좋은 선택이겠죠.


 Service 생성

다음의 코드를 이용해 우리 bookSearch Module에 서비스를 하나 추가합니다.

command 창을 열어서 다음과 같은 명령어를 실행시킵니다.

ng generate service HttpSupport

성공적으로 수행되면 현재 Module 폴더에 2개의 파일이 생성됩니다. 하나는 SPEC 파일이고 나머지 하나가 바로 Service 입니다.

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

import { Injectable } from '@angular/core';

@Injectable()
export class HttpSupportService {

  constructor() { }

}

주의해서 보셔야 하는 부분은 @Injectable decorator입니다. 해당 class가 다른 class에 주입(Injection)될 수 있다는걸 의미합니다. 아까도 설명했듯이 주입은 생성자를 이용하게 되고 주입과정은 Angular Framework이 담당합니다.

이제 이 안에 JSON 데이터를 가져오는 코드를 작성해야 합니다. 우리 예제의 list-boxComponent는 View를 rendering할 때 HttpClient를 이용해 JSON데이터를 가져와 Material Table로 화면에 바로 출력합니다. 이 부분을 변경해야겠죠. search-boxComponent에서 Search! 버튼을 클릭하면 HttpClient를 이용해서 데이터를 가져와서 그 데이터를 list-box가 사용할 수 있도록 처리해야 합니다.

원래는 Back End 프로그램도 하나 작성해서 RESTful 서비스 하는걸 예로 들어야 하는데 서버쪽 프로그램이 없으니 그냥 JSON 파일로 부터 데이터를 받는걸로 처리했습니다.

다음은 수정된 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() {
    this.http.get<IBook[]>('assets/data/book.json')
        .subscribe(res => {
           this.books = res;
           console.log(this.books);
        });
  }
}

interface IBook도 저런식으로 코드마다 등장해서는 안되겠죠. 원래 따로 빼서 관리해야 합니다. 하지만 예제를 좀 이해하기 쉽도록 그냥 중복해서 썻습니다. ^^;;

constructor(private http: HttpClient) { }

생성자로 인자가 하나 들어옵니다. 사실 이것도 HttpClient 타입의 객체가 우리 서비스 안으로 Injection되는 것입니다. 생성자에 인자를 받으면서 Access Modifier를 이용하면 class안에 속성으로 자동 지정됩니다. 여기서는 private으로 Injection된 HttpClient 객체를 받았습니다.

getJsonData() method가 호출되면 Injection받은 HttpClient 객체를 이용해서 파일로부터 JSON 데이터를 읽어들인 후 console에 정상적으로 읽었는지 출력합니다.


 Service Injection

위에서 생성한 Service 객체를 search-box Component에 Injection한 후 사용해 보겠습니다.

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

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

import { HttpSupportService } from "../http-support.service";

@Component({
  selector: 'app-search-box',
  templateUrl: './search-box.component.html',
  styleUrls: ['./search-box.component.css'],
  providers: [
    HttpSupportService
  ]
})
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(private httpSupportService:HttpSupportService) { }

  ngOnInit() {
  }

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

    this.httpSupportService.getJsonData();

  }

  inputChange(): void {

  }
}

기존 코드에서 변경된 부분을 살펴보면

import { HttpSupportService } from "../http-support.service";

기본적으로 import는 시켜줘야 사용할 수 있겠지요.

constructor(private httpSupportService:HttpSupportService) { }

constructor를 이용해 Service가 Injection되었습니다.

this.httpSupportService.getJsonData();

Injection받은 Service의 method를 호출하는 부분입니다.

@Component({
  selector: 'app-search-box',
  templateUrl: './search-box.component.html',
  styleUrls: ['./search-box.component.css'],
  providers: [
    HttpSupportService
  ]
})

Angular Framework에 어떤 class가 Injection이 되는지 알려줘야 합니다. Component의 Metadata부분에 providers를 이용해 처리해야 합니다.

실행해보면 정상적으로 console에 JSON데이터가 출력되는걸 확인하실 수 있습니다.


 Injector

기본적으로 Angular Framework은 dependency객체를 어떻게 생성해야 하는지 알지 못합니다. 그래서 우리가 Component의 Metadata를 이용해서 providers에 그 정보를 명시했었지요. 이 정보를 근간으로 Injector가 의존객체를 생성하고 주입합니다.

정리하자면 Component가 생성될 때 Angualr는 Injection에 필요한 객체를 Injector에 요청합니다. 이 Injector는 이미 생성한 객체들을 담고 있는 Container를 유지하고 있는데 이 안에 객체가 있으면 바로 주입하고 그렇지 않으면 의존객체를 생성한 후 주입하게 됩니다.

그림으로 표현하면 다음과 같습니다.

angular-injector

( 이미지 출처 : https://angular.io/guide/architecture )

여기서 주의해야 할 점이 있는데 각각의 Component 각자 하나씩의 Injector를 가지고 있습니다. Component는 tree형식으로 구성되니 Injector 역시 tree형태로 구성이 되게됩니다. 만약 Injection요청에 대한 내용이 현재 Component의 providers부분에 명시되어 있지 않으면 부모 Component의 providers에서 검색하게 됩니다. 이렇게 부모로 타고 올라가면서 의존객체를 찾게 되는것이죠. 만약 상위 Component에서 의존객체를 생성해 놓았으면 하위 Component에서 따로 선언하지 않아도 사용이 가능합니다.

또한 Component의 providers에 등록해 놓을 수도 있지만 Module의 providers에도 등록할 수 있습니다. 이런 경우 해당 Module안에 있는 모든 Component들이 해당 의존모듈을 사용할 수 있게 되겠네요. 최상위 Component인 Root Component가 가지고 있는 Root Injector는 Application 전역에서 사용가능한 의존모듈을 가지고 있게 되겠네요.


 Provider

위에서 설명했듯이 Module안에 providers로 등록한 의존객체는 Module안에서 사용이 가능합니다. Component에서 등록한 의존객체는 자신과 자식 Component에서 사용이 가능하지요.

이렇게 보면 Module과 Component에 등록하는게 크게 차이가 없어 보이지만 Module에 등록하는 경우 의존객체는 하나의 객체가 생성되서 사용됩니다. 즉, Singleton 형태로 사용된다는 것이죠. 반면 Component에 등록된 의존객체는 해당 Component가 생성될 때 마다 의존객체가 따로 생성되게 됩니다.

따라서 정보공유를 목적으로 하는 Service Mediator Pattern을 이용할 경우 일반적으로 Module에 의존객체를 등록해서 사용하는것이 좋습니다.

이 provider에 대해서 조금만 더 알아보도록 하죠.

Component안에서 의존객체를 등록하려면 다음의 코드를 이용합니다.

@Component({
  selector: 'app-search-box',
  templateUrl: './search-box.component.html',
  styleUrls: ['./search-box.component.css'],
  providers: [
    HttpSupportService
  ]
})

우리는 지금 의존객체라는 표현을 쓰면서 객체만이 주입되는식으로 표현했는데 실제 객체뿐만 아니라 Value도 주입할 수 있습니다. 일단 먼저 의존객체를 주입하는 방식에 대해서 코드를 조금만 상세히 표현해 보겠습니다. 위의 코드는 사실 밑의 코드의 축약형 입니다.

@Component({
  selector: 'app-search-box',
  templateUrl: './search-box.component.html',
  styleUrls: ['./search-box.component.css'],
  providers: [
    {
      provide: HttpSupportService,    // 데이터 타입
      useClass: HttpSupportService    // 실제 객체를 생성하기 위해 필요한 class
    }
  ]
})

provide의 값과 useClass의 값이 같을 경우 축약형으로 표현할 수 있습니다. provide는 만들어지는 객체의 데이터 타입입니다. useClass는 실제 객체를 생성하기 위해 사용되는 class명이구요. 당연히 두개가 틀릴 수 있습니다. interface를 이용하거나 duck typing을 이용하면 서로 다른 데이터 타입과 class를 사용할 수 있습니다.

duck typing에 대해서는 여기를 클릭하시면 간단한 내용을 확인하실 수 있습니다.

이번에는 의존객체가 아닌 고정값을 주입하는 방법에 대해서 알아보겠습니다. 일반적인 예는 configuration 값을 주입받는 경우입니다. 간단하게 환경설정파일을 하나 만들어서 그 안에 configuration내용을 채워놓고 그 값을 주입받아 보겠습니다.

command 창을 열어 다음의 명령을 실행해서 하나의 class를 생성합니다. 현재 command 창의 working directory는 search-box Component의 위치입니다.

ng generate class jsonConfig

json-config.ts 파일이 생성됩니다. 해당 파일에 다음과 같이 우리가 사용하는 JSON 파일에 대한 경로와 파일명을 설정정보로 입력합니다.

export class JsonConfig {
  url: string;
  name: string;
}

export const JSON_DATA_CONFIG: JsonConfig = {
  url: 'assets/data/',
  name: 'book.json'
};

다음은 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: HttpSupportService,
      useClass: HttpSupportService
    },
    {
      provide: JsonConfig,
      useValue: JSON_DATA_CONFIG
    }
  ]
})
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(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);

  }

  inputChange(): void {

  }
}

기존에 비해 몇가지 사항이 달라졌습니다. Value를 Injection받을 때 어떻게 처리해야 하는지를 유심히 보시면 됩니다.

service의 method를 호출할 때 주입값을 가지고 method를 호출하기 때문에 service의 코드도 변경해야 합니다.

다음은 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) {
    this.http.get<IBook[]>(`${url}${name}`)
        .subscribe(res => {
           this.books = res;
           console.log(this.books);
        });
  }
}

마지막으로 한가지가 더 남아있습니다. 의존객체를 생성할 때 객체를 그대로 사용하는게 아니라 특정 로직을 거쳐 의존객체를 생성해 사용할 수 있습니다. 객체지향에서 나왔던 Factory Pattern 생각하시면 됩니다. 이 부분은 여기서 따로 설명하지는 않겠습니다.


 Optional Dependency

Optional Dependency는 의존객체의 주입이 필수가 아니라는 것을 의미합니다. @Optionaldecorator를 이용하면 의존객체가 존재하지 않더라도 프로그램 오류가 나지 않습니다.

단순히 생성자에서 의존객체를 주입받을 때 @Optional decorator를 명시하시면 됩니다. 물론 의존객체가 들어오지 않을때의 로직처리는 해 주어야 합니다.

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

이번 포스트에서는 Angular의 Service에 대해서 알아보았습니다. 기본적인 Service의 사용방법을 먼저 숙지하신 후 이 Service를 이용해 데이터를 공유하는 방법으로 넘어가면 될 듯 보입니다. 다음 포스트는 Service Mediator Pattern을 이용한 Component간 데이터 공유에 대해서 알아보도록 하겠습니다.

728x90
반응형
728x90
반응형

Content Projection

이번 포스트는 과거 AngularJS에서 transclusion이라고 불리었던 Content Projection에 대해서 알아보겠습니다. 쉽게 말하자면 부모 Component가 자식 Component에게 template을 전달해 줄 수 있는 기능입니다. 이 역시 우리 예제에서는 필요없는 부분이지만 간단하게 내용을 추가해서 알아보겠습니다.

다음은 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 #resultStatus 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>
    <button mat-raised-button color="primary"
            (click)="clearCondition()">검색 초기화</button>
    <button mat-raised-button color="primary"
            (click)="changeDOM()">DOM 직접 변경</button>
  </div>

  <div>
    <app-search-box [bookCategory]="displayCategoryName"
                    (searchEvent)="changeTitleBar($event)">
      <p>Content Projection!</p>
      <p>First Paragraph</p>
      <p>Second Paragraph</p>
    </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)">
  <p>Content Projection!</p>
  <p>First Paragraph</p>
  <p>Second Paragraph</p>
</app-search-box>

부분을 보면 하위 Component를 포함시키면서 3개의 <p> Element를 전달한 것을 보실 수 있습니다. 이렇게 부모 Component가 자식 Component에게 특정 template을 전달해 줄 수 있는 기능이라고 생각하시면 됩니다. 자식 Component인 search-box.component.html은 다음과 같이 작성합니다.


<div class="example-container">
  <mat-toolbar #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>
  <ng-content></ng-content>
</div>

다른 부분은 다 동일하고 맨 마지막에 <ng-content></ng-content> directive가 보입니다. 이 directive가 부모 Component가 전달해 준 template으로 치환되게 됩니다.

다음에는 Service에 대해서 알아본 후 이를 이용한 데이터 공유 방법인 Service Mediator Pattern에 대해서 알아보도록 하겠습니다.

728x90
반응형

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

Angular Service Mediator Pattern  (0) 2018.08.28
Angular Service  (0) 2018.08.28
Angular @ViewChild 데이터공유  (0) 2018.08.28
Angular @Output 데이터공유  (0) 2018.08.28
Angular @Input 데이터공유  (0) 2018.08.28

+ Recent posts