/ ANGULAR

Angular 강좌(14) - Service Mediator Pattern

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


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-box Component에서 사용할 수 있도록 코드를 수정합니다.

다음은 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(구독) 하고 구독하고 있는 데이터를 어떻게 변경해야 하는지에 대해서 알아보겠습니다.

End.


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