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

Data Binding

Angular는 View와 Component에서 발생한 데이터의 변경 사항을 자동으로 일치시키는 기능을 제공하는데 이를 Data Binding( 데이터 바인딩 )이라고 합니다.

Angular의 데이터 바인딩은 다음과 같이 크게 두가지 종류가 있습니다.

  • Two-Way Data Binding : 일반적으로 양방향 바인딩이라고 합니다. Component와 View의 상태 정보를 자동으로 일치시켜 주는 기능입니다.

  • One-Way Data Binding : 일반적으로 단방향 바인딩이라고 합니다. Component에서 View쪽으로 혹은 View에서 Component쪽으로 한 방향으로 데이터를 바인딩 해주는 기능입니다.

위와 같이 크게 두 가지 바인딩이 있지만 실제로 양방향 바인딩은 내부적으로 두개의 단방향 바인딩으로 구성됩니다. 기존 AngularJS 는 다른 방식으로 양방향 바인딩을 제공했는데 성능상의 문제가 많아서 Angular에서는 이를 단방향 바인딩 2개를 이용해서 기존의 양방향 바인딩처럼 이용할 수 있도록 제공해 주고 있습니다.

단방향 바인딩은 다음과 같이 다시 세가지 방식으로 나누어 집니다 .

  • Interpolation : Component에서 선언한 속성을 View에서 사용하는 경우입니다. 다음의 형태를 이용합니다.

{{ value }}

  • Property binding : View의 DOM이 소유한 HTML Element property를 []를 이용하여 binding하는 경우입니다. 다음의 형태로 이용합니다.
[property]="value" 
  • Event bidning : View의 DOM에 대한 Event handler로 Component의 method를 사용하는 경우입니다. 다음의 형태로 이용합니다.
(event)="function"

위에서 간단하게 Data Binding의 종류와 형태를 살펴보았는데 이전 예제를 이용해서 각각을 코드로 살펴보겠습니다.


 Interpolation

예제를 통해 interpolation의 사용법을 알아보겠습니다. 우리가 작성하고 있는 예제에서search-box.component.html을 다음과 같이 수정합니다.


<div class="example-container">
  <mat-toolbar class="search-toolbar-style">Search Keyword : {{ keyword }}</mat-toolbar>
  <mat-form-field>
    <input matInput placeholder="Search Keyword">
  </mat-form-field>
  <button mat-raised-button color="warn">Search!</button>
</div>

위의 코드에서 {{ keyword }} 부분을 찾을 수 있는데 이 표현이 바로 interpolation이라고 부르는 단방향 바인딩 입니다. keyword 라는 이름의 Component 속성을 찾아 그 값을 View에 표현하라는 것이지요.

따라서 우리의 Component에는 keyword라는 이름의 속성이 존재해야 합니다.

search-box.component.ts 파일을 열어 class안에 해당 속성을 추가합니다.

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 {

  keyword = 'java';
  
  constructor() { }

  ngOnInit() {
  }

}

SearchBoxComponent class안의 keyword 속성에 있는 Java란 값이 View에 그대로 출력되게 됩니다. 실행시켜서 확인해 보시면 될 듯 합니다.


 Event Binding

Event bidnging 역시 단방향 바인딩의 한 종류로 DOM의 Event Handler로 Component의 method를 활용할 수 있는 방법입니다.

search-box Component의 View에서 Search 버튼을 클릭하면 입력된 키워드가 위쪽 Toolbar 영역에 출력되도록 코드를 작성하면 다음과 같습니다.

아래는 수정된 search-box.component.html입니다.


<div class="example-container">
  <mat-toolbar class="search-toolbar-style">Search Keyword : {{keyword}}</mat-toolbar>
  <mat-form-field>
    <input matInput #inputKeyword placeholder="Search Keyword">
  </mat-form-field>
  <button mat-raised-button color="warn"
          (click)="setKeyword(inputKeyword.value)">Search!</button>
</div>

위의 코드에서 #inputKeyword 라는 걸 보실 수 있을 텐데 Angular의 Template Reference Variable입니다.

Template Reference Variable은 DOM의 HTML Element에 대한 참조 변수입니다. 쉽게 설명해서 HTML Element에 변수를 하나 지정했다고 보시면 됩니다. 이렇게 참조 변수를 선언해 놓으면 template내의 JavaScript코드에서 #기호를 제외한 변수이름으로 참조가 가능합니다.

setKeyword(inputKeyword.value)에서 알 수 있듯이 Template Reference Variable inputKeyword를 # 기호없이 사용해서 변수를 참조하고 있습니다. 입력상자이기 때문에 value속성을 이용해서 입력된 값을 알아내고 있는 거지요.

한가지 주의해야 하는 것은 이런 Template Reference Variable은 HTML Template상에서만 사용할 수 있습니다. 물론 위의 예와 같이 Event Binding을 이용해서 Component class로 전달할 수 있지만 기본적으로 Component class와는 별개로 동작합니다.

참고로 Agnular는 interpolation을 사용할 때 Safe Navigation Operator를 이용할 수 있습니다. ? 기호로 표현되며 Component class의 속성값이 null이거나 undefiend일 경우 interpolation을 이용하면 오류가 발생할 여지가 있습니다. 이때 ?를 이용하면 그런 오류를 만났을 때 처리를 종료하고 에러를 발생시키지 않게됩니다.

예를 들자면 {{ book?.btitle }} 이런식으로 사용합니다. book이라는 객체가 null인 경우 문제가 생길 수 있죠. 이런 오류를 방지할 때 사용됩니다.

아래는 수정된 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 {

  keyword = 'Java';

  constructor() { }

  ngOnInit() {
  }

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

위의 코드는 버튼을 클릭했을 때 Template Reference Variable을 이용하여 키워드 입력상자에서 사용자가 입력한 검색 keyword 값을 가져와 Component의 method를 호출하여 Component 속성의 값을 변화시킵니다. 이렇게 변경된 Component의 속성은 interpolation을 통해 다시 View에 출력되게 됩니다.


 Two-Way Data Binding

이번에는 양방향 바인딩에 대해서 알아보겠습니다. 위에서 설명한 Search버튼은 입력된 키워드를 기반으로 실제 검색을 하기 위한 버튼이지 검색어를 위쪽 Toolbar 영역에 출력하기 위한 버튼은 아닙니다. 해서 키워드 입력상자( input ) 상자에 키워드를 입력할 때 사용자가 입력한 내용이 위쪽 Toolbar 영역에 출력되도록 처리해보겠습니다.

양방향 바인딩을 사용하는 가장 쉬운 방법은 FormsModule이 제공하는 NgModel directive를 이용하는 것입니다. 따라서 먼저 FormsModule을 import하는 부분부터 처리해 주셔야 합니다. book-search.module.ts 파일에 FormsModule에 대한 import 처리를 합니다.

// 양방향 바인딩을 위한 FormsModule import
import { FormsModule } from '@angular/forms';

다음과 같이 바인딩할 요소의 속성에 [(ngModel)] 과 함께 바인딩할 대상을 선언하시면 됩니다. 다음은 수정된 search-box.component.html 입니다.


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

실행을 시켜보시면 키워드 입력상자에 글자를 입력할 때 상단의 키워드 표시영역에 같이 출력되는 것을 보실 수 있습니다. 이렇게 양방향 바인딩이 설정되어 있을 때 사용자가 글자를 입력하면 NgModel로 바인딩한 값이 변경이 됩니다. 이 때 이벤트가 하나 발생하는데 이 이벤트를 처리하기 위해 ngModelChange를 이용할 수 있습니다.

위의 코드를 약간 수정해 다음과 같이 작성해보죠.


<div class="example-container">
  <mat-toolbar class="search-toolbar-style">Search Keyword : {{keyword}}</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>

(ngModelChange)에 바인딩 된 inputChange()는 search-box.component.ts 안에 class의 method로 정의되어 있어야 합니다. 데이터가 변경될 때 자동적으로 이벤트가 발생되고 바인딩된 method를 통해 특정 로직을 수행할 수 있습니다.

영문일 경우는 문제없이 잘 수행되지만 한글일 경우는 약간의 문제가 있습니다. 바로바로 화면에 적용되지 않는 것이죠. 양방향 바인딩은 기본적으로 문자 입력이 완료된 시점에 compositionend라는 browser 이벤트가 발생하고 이에 따라 바인딩을 처리합니다. 하지만 영문과 다르게 한글은 조합형 문자이기 때문에 글자가 다 만들어 지기 전까지는 해당 이벤트가 발생하지 않고 따라서 화면에 출력되지 않게 되는 것입니다.

이 문제를 해결하기 위해서는 Angular에서 제공하는 COMPOSITION_BUFFER_MODE 설정을 변경하시면 됩니다. 설정하는 방법은 아래의 코드를 참조하시면 됩니다.

아래는 book-search.module.ts 파일 입니다.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BookSearchMainComponent } from './book-search-main/book-search-main.component';
import { SearchBoxComponent } from './search-box/search-box.component';
import { ListBoxComponent } from './list-box/list-box.component';
import { DetailBoxComponent } from './detail-box/detail-box.component';

import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material';
import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatCardModule } from '@angular/material/card';

// 양방향 바인딩을 위한 FormsModule import
import { FormsModule } from '@angular/forms';

// COMPOSITION_BUFFER_MODE import
import { COMPOSITION_BUFFER_MODE } from '@angular/forms';

@NgModule({
  imports: [
    CommonModule,
    MatFormFieldModule,
    MatInputModule,
    MatButtonModule,
    MatToolbarModule,
    MatCardModule,
    FormsModule
  ],
  providers: [
    {
      provide: COMPOSITION_BUFFER_MODE,
      useValue: false
    }
  ],
  declarations: [BookSearchMainComponent, 
                 SearchBoxComponent, 
                 ListBoxComponent, 
                 DetailBoxComponent]
})
export class BookSearchModule { }

angular-hangul-input


 Property Binding

Property binding을 이용하면 DOM 상의 HTML Element에 대한 property를 Component의 속성과 바인딩 할 수 있습니다. 주의하실 점이 하나 있는데 property binding은 HTML Element의 Attribute에 값을 binding하는 것이 아니라 HTML이 browser에 의해 parsing되면 메모리에 DOM이 생성되는데 이 DOM의 HTML ELement에 대한 property에 값이 binding된다는 것입니다.

이번에는 detail-box Component를 이용해서 한번 알아보죠.

일단 완성된 화면은 다음과 같습니다.

detail-box-view

제목, 저자, ISBN, 가격, 출판일, 이미지 정보는 interpolation을 이용해 출력하고 property binding을 이용하여 만약 책의 가격이 20000을 초과하면 구입버튼을 disabled 시키게 처리했습니다.

detail-box-component.css에서 사용된 CSS는 다음과 같습니다.

.example-card {
  width: 500px;
  margin: 0 auto;
}

.example-header-image {
  background-image: url('/assets/images/book-icon.jpg');
  background-size: cover;
}

.book-image {
  width: 100px !important;
}

.detail-header-style {
  font-family: Georgia;
}

아래는 detail-box.component.html 입니다.


<mat-card class="example-card">
  <mat-card-header class="detail-header-style">
    <div mat-card-avatar class="example-header-image"></div>
    <mat-card-title>제목 : {{book.btitle}}</mat-card-title>
    <mat-card-subtitle>저자 : {{book.bauthor}}</mat-card-subtitle>
  </mat-card-header>
  <img mat-card-image class="book-image" src="{{book.bimgurl}}">
  <mat-card-content>
    <p>
      ISBN : {{book.bisbn}}, 도서 가격 : {{book.bprice }}, 출판일 : {{book.bdate}}
    </p>
  </mat-card-content>
  <mat-card-actions>
    <button mat-button
            mat-raised-button color="primary"
            [disabled]="book.bprice > 20000">바로 구입</button>
  </mat-card-actions>
</mat-card>

book 객체의 속성으로 바인딩 시켰습니다. 당연히 detail-box.component.ts 안에 class 속성으로 book 객체가 정의되어 있겠네요. 주의해서 보셔야 할 부분은

<button mat-button
        mat-raised-button color="primary"
        [disabled]="book.bprice > 20000">바로 구입</button>

부분입니다. disabled 라는 속성에 property binding을 이용해 조건을 걸었습니다. 현재 책 값이 20000원을 초과하면 “바로 구입”버튼을 비활성화 시키는 것이죠. 사용하는 법을 잘 기억해 두시면 될 듯 합니다.

다음은 데이터가 들어있는 detail-box.component.ts 입니다.

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

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

  book = {
    btitle: 'Head First Design Patterns: 스토리가 있는 패턴 학습법',
    bauthor: '에릭 프리먼외 3명',
    bprice: 28000,
    bdate: '2005년 08월',
    bisbn: '89-7914-340-0',
    bimgurl: 'http://image.hanbit.co.kr/cover/_m_1340m.gif'
  };

  constructor() { }

  ngOnInit() {
  }

}

이번 포스트에서는 Angular의 Data Binding에 대해서 살펴보았습니다. 다음 포스트는 지금까지 배운 내용과 Directive에 대한 내용을 섞어 약간 복잡하지만 책의 리스트를 출력하는 list-box Component와 연결된 View를 살펴보도록 하겠습니다. 먼저 기본적인 HTML Table Element을 이용해 보고 그 다음에 Angular Material Table Component을 이용해 리스트를 출력해보도록 하겠습니다.

728x90
반응형
728x90
반응형

먼저 실습의 결과화면을 한번 보고 개념적으로 무엇을 할 것인지 파악한 후 코드를 살펴보면 될 듯 싶습니다. 여기를 누르면 실습의 결과 Web Application을 볼 수 있습니다. 상단에 Navigation Bar가 있고 각 메뉴를 클릭하면 내용이 그에 맞게 변하는 SPA형식의 Web Application입니다.

UI 부분은 Bootstrap을 살짝 이용했습니다. CSS를 안쓰니 워낙 안예뻐서요. 약간 개념적으로 이상한 얘기도 좀 나오는데 천천히 이해하시면 됩니다. 일단 만든 Application이 어떤 형태인지는 이해하는것은 그리 어렵지 않아보입니다.

그럼 한번 시작해보죠.


 SPA( Single Page Application )

요즘 만들어지는 거의 대부분의 Front End Web Application은 SPA 형태입니다. Web Application이 하나의 Web Page로 구성되는 것이죠. 이와 대비되는 개념이 기존에 우리가 많이 했었던 JSP, ASP, PHP같은 것들입니다. 이 기술들은 모두 SSR(Server Side Rendering)기반의 Round Trip 방식의 Web Application을 개발하는데 사용됩니다.

기존의 Web Application은 Client가 새로운 Web Page를 요청할 때 마다 서버쪽에서 Web Page를 동적으로 만들어서 Client Browser에게 전송하는 방식을 취했습니다. 이 방식의 장,단점을 잠깐 살펴봐야 합니다. 장점은 서버쪽에서 모든 작업이 이루어지기 때문에 개발하기가 용이하다는 점입니다. 이미 개발방식도 정형화되어 있고 jQuery 정도만 익혀서 결과 Web Page에 적용하는 식으로 Client쪽 처리도 쉽게 할 수 있습니다.

기존 Desktop 환경에서는 이 방식이 크게 문제가 되지 않았습니다. 하지만 Web Application의 사용환경이 Desktop환경에서 Mobile환경으로 넘어가면서 다음과 같은 문제가 발생하기 시작합니다.

  • 클라이언트가 새로운 페이지를 요청하고 받을 때 마다 web browser 화면 전체가 결과 Page로 Refresh됩니다. 필요한 데이터만 받아서 필요한 부분만 갱신하면 되지 전체 페이지를 다 받아서 새로 고침하는 것은 네트워크 사용량을 생각해봐도 비효율적입니다.

  • Mobile환경에서 Client는 이미 Native App을 사용하는 방식으로 UX가 확고하게 굳어져 있습니다. 따라서 우리의 Web Application도 마치 Native App처럼 동작하도록 만들어 제공해야 한다는 것이죠.

결국 위와 같은 문제를 해결하기 위해 나온 모던 웹 패러다임이 바로 SPA입니다. Network traffic의 감소 및 사용성 관점에서 상당히 가치있는 Front End 개발방식이라고 볼 수 있습니다. 하지만 SPA도 단점이 없는 것은 아닙니다. 일반적으로 두가지의 단점을 많이 이야기 합니다.

  • 초기 로딩 속도 문제
  • 검색엔진최적화(SEO, Search Engine Optimization) 문제

실습인데 설명이 너무 길어지는군요. 이 문제는 여기서 따로 언급하지는 않겠습니다. 여하간 SEO 문제를 해결하기 위해 Angular Router를 이용하여 PathLocationStrategy라는 Location 정책을 이용해 우리의 Angular Application을 작성하겠습니다.


 Module 생성

우리는 이미 AppModule이라고 불리는 Root Module을 가지고 있습니다. 여기에 추가적으로 다른 Module을 만들어서 개발할 수 있습니다. 먼저 Module에 대해 간단히 알아보고 우리 프로그램에 맞게 Module을 구성해 보도록 하겠습니다.

Angular의 Module은

연관성이 있는 Angular의 구성요소들을 하나의 단위로 묶은 것

을 지칭합니다. 또한 Angular Application은 크게 본다면 이런 Module의 집합이라고 할 수 있습니다. JavaScript에서 말하는 Module과는 다른 개념입니다.

아주 간단한 application인 경우 Root Module 하나로 구성할 수 있지만 일반적으로 여러개의 Module을 이용해서 구성하게 됩니다. 일반적으로 다음과 같은 형태의 Module을 이용합니다.

  • Feature Module

    특정 화면을 구성하는 구성요소를 묶어서 Module로 관리할 수 있습니다. 우리예제에서 Home은 단일 View로 구성되어 있습니다. 따로 Module을 만들어서 Home에 대한 구성요소를 관리해도 됩니다. 하지만 우리 예제에서는 Home에 대한 Component를 Root Module에서 관리하도록 하겠습니다. 

    하지만 도서 검색이나 영화 검색같은 경우 여러 요소들이 필요할 테고 각각을 Module로 따로 관리하는게 좋습니다. Application안에 여러 화면이 존재할 경우 각 화면별로 Module화를 시키지 않으면 추후에 구성요소의 관리가 어려워짐은 당연합니다.

    또 Application 전역에서 사용하는 구성요소들을 따로 묶어서 Module로 만들 수도 있습니다. 이런 기능들은 Root Module에 import가 될 필요가 있는 것들로 간단한 예를 들자면 Authentification Module이나 Routing Module등이 있습니다.


  • Shared Module

    Feature Module에 의해서 공통적으로 사용되는 구성요소들을 묶어서 Module로 관리할 수 있습니다. 주로 Feature Module에서 공통적으로 사용되는 Directive나 Pipe같은 것들이 포함됩니다.

먼저 Routing Module을 생성하고 그 안에서 Router를 구성하고 등록해보겠습니다.
그 후 Routing Module을 Root Module에서 가져다 사용하는 식으로 작성을 해 보죠.

command 창을 열고 다음의 명령을 이용하여 새로운 Module을 하나 생성합니다.

ng generate module app-routing

정상적으로 실행되면 src/app/app-routing 폴더가 생성되고 그 안에 app-routing.module.ts 파일이 생성됩니다.

조금뒤에 이 Module안에 Router를 작성하게 되겠죠. 우리가 가지고 있는 app.module.ts라는 Root Module에 Router를 생성, 등록 해도 됩니다. 하지만 실제 Application을 작성할 때는 사용되는 Routing이 많아지게 되는데 이때는 Routing Module로 빼서 관리하시는게 좋습니다.


 각 Routing이 사용할 Component 생성

각 Routing 경로가 사용할 Component를 생성합니다. 그런데 위에서 특정 화면을 구성하는 구성요소를 Module화 시켜서 사용하는게 좋다고 했으니 우리의 2가지 화면에 대해 Module을 생성하고 이 안에 Component를 생성하도록 하겠습니다.

Home은 아까 언급했듯이 별다른 기능이 없는 View이기 때문에 따로 Module로 생성하지 않고 도서 검색에 대한 Module과 영화 검색에 대한 Module만 만들도록 하겠습니다.

Home 화면을 담당할 Component는 src/app/pages 하단에 생성하고 Root Module에서 직접 import해서 사용하도록 생성합니다.

command 창에서 Angular CLI를 이용해 Component를 생성합니다.

ng generate component pages/home

이제 도서 검색을 위한 Module을 생성하고 그 안에 Component를 생성합니다.

ng generate module bookSearch 
ng generate component bookSearch/bookSearchMain

마지막으로 영화 검색을 위한 Module을 생성하고 그 안에 Component를 생성합니다.

ng generate module movieSearch 
ng generate component movieSearch/movieSearchMain

정상적으로 수행되면 아래의 그럼처럼 각각의 폴더안에 파일들이 생성되게 됩니다.

step1-scaffolding-folder


 Routing Module 수정

이제 Routing Module을 수정하여 Router를 구성합니다. app-routing.module.ts를 다음과 같이 수정합니다.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

// Angular Router Module import
import { Routes, RouterModule } from "@angular/router";

// Routing 처리를 할 각각의 Component import
import { HomeComponent } from "../pages/home/home.component";
import { BookSearchMainComponent } from
    "../book-search/book-search-main/book-search-main.component";
import { MovieSearchMainComponent } from
    "../movie-search/movie-search-main/movie-search-main.component";

// Router 생성( path 표시할 때 Root path에 대한 '/'는 제외 )
const routers: Routes = [
  { path : '', component : HomeComponent },
  { path : 'book', component : BookSearchMainComponent },
  { path : 'movie', component : MovieSearchMainComponent }
];

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forRoot(routers)
  ],
  declarations: [],
  exports: [RouterModule]
})
export class AppRoutingModule { }

 Root Module 수정

Routing Module이 만들어졌으니 이제 Root Module에서 Routing Module을 불러들이는 코드를 작성해야 합니다.

app.module.ts 파일의 내용입니다.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

// BrowserAnimationsModule import 구문 추가
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

// MatTableModule import 구문 추가
import { MatTableModule } from '@angular/material/table';

import { AppComponent } from './app.component';
import { HomeComponent } from './pages/home/home.component';

// Feature Module import
import { BookSearchModule } from "./book-search/book-search.module";
import { MovieSearchModule } from "./movie-search/movie-search.module";

// Routing Module import
import { AppRoutingModule } from "./app-routing/app-routing.module";

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    MatTableModule,
    AppRoutingModule,
    BookSearchModule,
    MovieSearchModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

 Navigation Menu 작성

이제 Navigation Menu를 만들어야 합니다. Routing Module을 이용해 Router의 구성과 등록이 완성되었으나 해당 Routing Path에 대한 View를 어디에 표시할지는 아직 지정하지 않았습니다. app.component.html을 수정해 RouterOutlet directive를 이용해 View의 rendering위치를 지정해야 합니다.

여기서는 Bootstrap을 이용해서 화면을 구성했습니다. Bootstrap의 예제중에 Blog 예제가 있는데 그 형식을 가져다가 사용했습니다. Bootstrap을 사용하기 위해서는 다음과 같이 Bootstrap을 설치하여야 합니다.

npm install bootstrap@4.0.0-beta.2

버전에 약간 주의하셔야 합니다. 현재 최신 버전(bootstrap@4.0.0-beta.3)의 Bootstrap은 Angular CLI로 Production build를 했을 때 bundling 오류가 발생합니다.

설치가 완료되면 Bootstrap을 import하셔야 합니다. .angular-cli.json 파일을 다음과 같이 수정합니다.

   "styles": [
        "../node_modules/bootstrap/dist/css/bootstrap.min.css",
        "styles.css"
   ],

이제 app.component.html 파일을 수정합니다.

<nav>
    <a routerLink="/">Home</a>
    <a routerLink="/book">도서검색</a>
    <a routerLink="/movie">영화검색</a>
</nav>
<router-outlet></router-outlet>

쉽게 보기 위해 Bootstrap은 걷어내고 실제 필요한 부분만 명시했습니다. 주의해서 보셔야 할 건 routerLink 입니다. <a>의 href 속성을 이용하면 서버에 request를 보내게되니 href를 이용하지 않습니다. 각각의 메뉴를 클릭했을 때 해당 URL을 Router에 전달하고 Router에 의해서 Component가 선택되서 <router-outlet></router-outlet>안에 Component가 지정한 HTML이 출력되게 됩니다.

결과가 잘 나오나요? Home 화면의 내용을 바꾸실려면 src/app/pages/home 폴더 안에 home.component.html을 적절히 수정하시면 됩니다.

참고로 다음과 같은 routerLinkActive directive를 이용하면 routerLink directive의 값과 현재 browser URL이 정확히 일치할 때 특정 style의 class를 지정할 수 있습니다.

<nav>
    <a routerLink="/">Home</a>
    <a routerLink="/book"
       [routerLinkActiveOptions]="{ exact: true }"
       routerLinkActive="menuActiveClass">>도서검색</a>
    <a routerLink="/movie">영화검색</a>
</nav>
<router-outlet></router-outlet>

위의 예처럼 링크를 클릭해서 특정 경로로 Routing을 할 수도 있지만 버튼을 클릭했을 때 처럼 프로그램적으로 Routing을 변경해야 하는 경우도 있을 수 있습니다. 이런경우는 Angular Router의 navigate method를 이용하시면 됩니다.

import { Router } from '@angular/router';
...
...
constructor(private route:Router) { }
...
...
gotoBook() {
    // Router객체에 대해 method호출
    this.route.navigate(['book']);
}

현재까지 우리가 작성한 Angular Project에 대해 production build를 진행하실려면 다음과 같이 하시면 됩니다.

ng build --prod --base-href=/my-base-url/


 도서 검색 화면

3개의 메뉴 중 Home 메뉴는 단일 페이지 이기 때문에 적절하게 수정해서 화면에 보여주면 됩니다. 먼저 최종적으로 만들어진 도서 검색 화면을 한번 보고 View을 어떻게 구성할지 개념적으로 확인하면 될 듯 싶습니다. 아래의 그림이 최종 만들어진 도서검색 화면입니다.

exercise-result

총 4개의 View로 구성되어 있습니다. 파란색으로 되어 있는 가장 큰 View안에 검색결과를 보여주는 영역과 도서 종류를 선택하는 Select Box가 들어 있습니다. 부모 View안에 빨간색으로 되어있는 3개의 자식 View가 포함되어 있네요. 맨 위의 View는 검색어를 입력할 수 있는 View이고 제일 아래의 View는 검색어에 해당하는 책에 대한 리스트를 표현하는 View입니다. 가운데 View는 리스트에서 특정 책을 선택하면 그 책의 내용을 자세하게 출력해주는 View입니다.

일단 화면을 만드는데 집중하고 실제 프로그램이 동작하는 로직에 관련된 부분은 나중에 service를 설명하면서 추가하도록 하겠습니다.


 AppComponent 수정

이전에 만들어 놓은 mySearchProject를 수정하여 도서검색 화면을 만들고 그에 따른 Component들을 생성, 등록까지 진행하도록 하겠습니다.

src/app 폴더안에 app.component.ts 파일을 열면 다음과 같은 내용을 보실 수 있습니다.

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css', './blog.css']
})
export class AppComponent {
  title = 'app';
}

이 Component가 우리의 Root Component입니다.

selector: 'app-root' 설정에 의해 이 Component는 template 코드내에서 <app-root></app-root>로 되어 있는 부분을 rendering 한다는 것을 알 수 있습니다.
또한 templateUrl에 의해 app.component.html을 이용해 rendering한다는 것도 파악할 수 있겠네요.

index.html을 보시면 다음과 같이 <app-root></app-root>로 되어있는 부분이 보이고 이 부분이 우리 Component에 의해서 rendering되게 됩니다.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>MySearchProject</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!-- Material Icon 설정. -->
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
  <app-root></app-root>
</body>
</html>

결국 index.html안의 <app-root></app-root> 부분은 app.component.html의 내용으로 rendering된다는 것을 파악할 수 있습니다.

이제 app.component.html의 코드를 보죠. 위에서 Routing Module을 이용했기 때문에 다음과 같은 코드가 나옵니다. Bootstrap 코드도 포함시켰습니다.

<div class="container">
  <header class="blog-header py-3">
    <div class="row flex-nowrap justify-content-between align-items-center">
      <div class="col-4 pt-1">

      </div>
      <div class="col-4 text-center">
        <a class="blog-header-logo text-dark" href="#">my Search Project</a>
      </div>
      <div class="col-4 d-flex justify-content-end align-items-center">

      </div>
    </div>
  </header>

  <div class="nav-scroller py-1 mb-2">
    <nav class="nav d-flex justify-content-between">
      <a class="p-2 text-muted" routerLink="/">Home</a>
      <a class="p-2 text-muted" routerLink="/book">도서 검색</a>
      <a class="p-2 text-muted" routerLink="/movie">영화 검색</a>
      <a class="p-2 text-muted" href="#"></a>
      <a class="p-2 text-muted" href="#"></a>
      <a class="p-2 text-muted" href="#"></a>
      <a class="p-2 text-muted" href="#"></a>
      <a class="p-2 text-muted" href="#"></a>
      <a class="p-2 text-muted" href="#"></a>
      <a class="p-2 text-muted" href="#"></a>
      <a class="p-2 text-muted" href="#"></a>
      <a class="p-2 text-muted" href="#"></a>
    </nav>
  </div>

  <div class="jumbotron p-3 p-md-5 text-dark rounded bg-warning">
    <div class="col-md-12 px-0">
      <router-outlet></router-outlet>
    </div>
  </div>

</div>

Bootstrap을 이용했기 때문에 Style에 관련된 코드가 많네요. 여하간 Navigation Menu를 작성하고 각 Menu를 클릭하면 Router에게 routerLink에 명시된 경로를 전달하게 됩니다. 그러면 Router는 해당 경로에 매핑되는 Component를 찾게되고 해당 Component는 <router-outlet></router-outlet>위치에 View를 Rendering 하게 됩니다.

결과적으로 도서 검색 메뉴를 클릭하면 <router-outlet></router-outlet> 부분에 book-search-main.component.html의 내용이 rendering되게 되고 이제 그 내용을 수정해서 화면을 다시 구성합니다. View를 추가하기 위해 Component를 생성하는 작업을 진행해야 합니다.


 Component 추가

book-search-main.component.html은 내부에 3개의 View 영역을 포함하고 있습니다.

  • 첫번째 영역 : 검색 키워드를 입력하고 검색 버튼을 눌러 실행시키는 View 영역.
  • 두번째 영역 : 검색된 책을 선택하면 책의 세부정보가 출력되는 View 영역.
  • 세번째 영역 : 검색된 책들의 리스트를 출력하기 위한 View 영역.

이렇게 3개의 View를 이용해서 화면을 구성할 것이고 3개의 Component를 추가하여 화면을 구성하려 합니다.

Angular CLI를 이용하여 다음과 같이 실행해서 새로운 Component를 추가합니다.

ng generate component bookSearch/search-box

Angular CLI의 generate를 이용하여 Component 생성 시 Component를 구성하는 관련 파일들을 자동으로 손쉽게 생성할 수 있습니다. generate를 다 쓰지 않고 앞글자인 g 만 써도 됩니다. generate는 뒤에 어떤 요소를 생성할 것인지 그리고 요소명은 무엇인지를 받아 특정 요소를 생성하게 됩니다. 즉, Component만 생성할 수 있는건 아닙니다.

생성된 src/app/book-search/search-box 폴더 안에 있는 search-box.component.ts 파일을 열어서 selector를 확인해보니 app-search-box로 지정되어 있습니다. 우리가 template 코드에서 <app-search-box></app-search-box>을 이용하면 이 Component가 해당 영역을 rendering하게 되겠네요.

이와 유사하게 2개의 Component를 더 생성합니다. 다음과 같이 실행해서 새로운 Component를 추가합니다.

ng generate component bookSearch/detail-box

ng generate component bookSearch/list-box

자 이제 생성된 각각의 Component에 대한 selector를 참조해 src/app/book-search/book-search-main 안의 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="form-group col-md-4">
    <label for="inputState">도서 종류</label>
    <select id="inputState" class="form-control">
      <option selected>선택하세요...</option>
      <option>국내외도서</option>
      <option>국내도서</option>
      <option>국외도서</option>
    </select>
  </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>

또한 src/app/book-search/book-search-main 안의 book-search-main.component.css의 내용을 다음과 같이 작성합니다. CSS에 대한 설명은 생략합니다.

.bookSearch-outer {
  font-family: Georgia !important;
  width: 70%;
  text-align: center;
  margin: 0 auto;
}

 각 Component의 View 작성

이제 각각의 Component의 templateUrl에 명시된 html을 Angular Material을 이용해 우리가 사용할 화면을 만들어 냅니다.

제일 먼저 상단의 search-box 영역에 대한 HTML을 작성합니다.

다음과 같이 src/app/book-search/search-box 폴더안의 search-box.component.html의 내용을 수정합니다.

<div class="example-container">
  <mat-toolbar class="search-toolbar-style">Search Keyword : </mat-toolbar>
  <mat-form-field>
    <input matInput placeholder="Search Keyword">
  </mat-form-field>
  <button mat-raised-button color="warn">Search!</button>
</div>

search-box.component.css의 내용은 다음과 같이 수정합니다.

.search-toolbar-style {
  font-family: Georgia;
  color: white;
  background-color: teal;
  margin-bottom: 20px;
}

Angular Material을 이용했기 때문에 해당 Element에 대한 Material Module을 book-search.module.ts안에 import해 줍니다.

다음은 book-search.module.ts 파일의 내용입니다.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BookSearchMainComponent } from './book-search-main/book-search-main.component';
import { SearchBoxComponent } from './search-box/search-box.component';
import { ListBoxComponent } from './list-box/list-box.component';
import { DetailBoxComponent } from './detail-box/detail-box.component';

import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material';
import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar';

@NgModule({
  imports: [
    CommonModule,
    MatFormFieldModule,
    MatInputModule,
    MatButtonModule,
    MatToolbarModule
  ],
  declarations: [BookSearchMainComponent, 
                 SearchBoxComponent, 
                 ListBoxComponent, 
                 DetailBoxComponent]
})
export class BookSearchModule { }

일단 여기까지 작성하고 실행을 시켜보면 다음과 같은 화면을 볼 수 있습니다.

search-box-view

화면이 잘 나오나요? 아직 데이터 바인딩과 이벤트에 대한 내용은 설명하지 않았기 때문에 키워드를 입력하거나 버튼을 눌러도 아무런 반응을 하지 않습니다.

이와 비슷하게 가운데 detail-box의 View는 다음과 같이 작성하시면 됩니다. list-box는 데이터 바인딩에 대한 내용을 배워야지 사용할 수 있으니 list-box의 View는 데이터 바인딩에 대해 설명하고 작성하겠습니다.

아래는 detail-box Component에 대한 각각의 코드입니다.

detail-box.component.css 파일입니다. src/assets/images 폴더안에 book-icon.jpg파일을 하나 넣어두셔야 합니다.

.example-card {
  width: 500px;
  margin: 0 auto;
}

.example-header-image {
  background-image: url('/assets/images/book-icon.jpg');
  background-size: cover;
}

.book-image {
  width: 100px !important;
}

.detail-header-style {
  font-family: Georgia;
}

detail-box.component.html 파일입니다.

<mat-card class="example-card">
  <mat-card-header class="detail-header-style">
    <div mat-card-avatar class="example-header-image"></div>
    <mat-card-title>제목 : Angular 일주일 완성</mat-card-title>
    <mat-card-subtitle>저자 : 홍길동</mat-card-subtitle>
  </mat-card-header>
  <img mat-card-image class="book-image" src="">
  <mat-card-content>
    <p>
      ISBN : 123-456, 도서 가격 : 5000, 출판일 : 2017년 12월
    </p>
  </mat-card-content>
  <mat-card-actions>
    <button mat-button mat-raised-button color="primary">바로 구입</button>
  </mat-card-actions>
</mat-card>

Material Card Layout Component를 사용했기 때문에 해당 Module에 대한 처리를 book-search.module.ts에 해주어야 하겠죠?

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BookSearchMainComponent } from './book-search-main/book-search-main.component';
import { SearchBoxComponent } from './search-box/search-box.component';
import { ListBoxComponent } from './list-box/list-box.component';
import { DetailBoxComponent } from './detail-box/detail-box.component';

import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material';
import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar';

import { MatCardModule } from '@angular/material/card';

@NgModule({
  imports: [
    CommonModule,
    MatFormFieldModule,
    MatInputModule,
    MatButtonModule,
    MatToolbarModule,
    MatCardModule
  ],
  declarations: [BookSearchMainComponent, 
                 SearchBoxComponent, 
                 ListBoxComponent, 
                 DetailBoxComponent]
})
export class BookSearchModule { }

 결과 화면

지금까지 작성한 내용을 실행시켜보면 다음과 같은 화면을 보실 수 있습니다.

step-1-result


이번 포스트에서는 Component를 추가하고 Angular Material을 이용해 View를 작성해보았습니다. 다음 포스트에서는 데이터 바인딩에 대한 내용을 알아보도록 하겠습니다.

728x90
반응형

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

Angular @Input 데이터공유  (0) 2018.08.28
Angular Material Table  (0) 2018.08.28
Angular 예제 따라하기  (0) 2018.08.28
Angular 개발환경세팅 하기  (0) 2018.08.28
Angular 소개  (0) 2018.08.28
728x90
반응형

Angular Material

Material Design은 모바일과 데스크탑 그리고 그 외 다양한 디바이스들의 application을 개발할 때 하나의 일관된 디자인을 적용하고자 Google이 공개한 design guideline입니다.

머티리얼 디자인(Material Design, 코드명: Quantum Paper)이란 플랫 디자인의 장점을 살리면서도 빛에 따른 종이의 그림자 효과를 이용하여 입체감을 살리는 디자인 방식을 말한다. 2014년 구글이 안드로이드 스마트폰에 적용하면서 널리 퍼지기 시작했다. 플랫 디자인과 마찬가지로 최소한의 요소만을 사용하여 대상의 본질을 표현하는 디자인 기법인 미니멀리즘(minimalism)을 추구한다. 
( 자료출처 : https://ko.wikipedia.org )

제가 Design에 대한 관심과 지식이 상대적으로 많지 않아서 Material Design에 대한 정의는 위키에서 가져왔습니다.

여하간 이런 Material Design에 대한 concept을 Angular application에 적용하기 위해 만든 Component가 바로 Angular Material입니다.

우리의 예제는 이 Angular Material로 작성할 것이기 때문에 Angular Material을 어떻게 사용하는지에 대해서 먼저 알아볼 필요가 있습니다.

참고로 Angular Material은 정식버전이 나온지 얼마 되지 않았습니다. 해서 생각만큼 많은 Component와 기능을 지원하고 있지 못합니다. Angular Material Official HomePage의 공식 문서 역시 아직은 좀 사용하기 불편합니다. 차차 나아지겠죠 ^^;;

우리예제는 Bootstrap도 같이 이용합니다. Bootstrap에 대해서는 그 때 알아보기로 하겠습니다.

자 그럼 Angular Material에 대해 알아보도록 하죠.


 Angular Material & Angular Material CDK 설치

제일 먼저 해야 할 일은 Angular Material과 Angular Material CDK를 설치하는 것입니다.

우리가 만들어 놓은 mySearchProject에서 command 창을 열고 다음 명령을 수행시켜 package를 설치합니다. ( 현재 경로는 C:/mySearchProject 입니다. )

npm install --save @angular/material @angular/cdk

angular-material-install

--save option은 npm@5부터 기본 option으로 바뀌었습니다. 즉, --save는 생략하셔도 됩니다. 잘 아시겠지만 저 명령을 수행하면 package.json의 dependencies에 설치된 패키지와 버전 정보가 기록되게 됩니다. 참고로 만약 개발 시에만 사용하는 package를 설치하실려면 --save-dev option을 주시면 됩니다. TypeScript와 같은것은 사실 transpiler이기 때문에 개발시에만 필요하고 실제 배포까지 할 필요는 없으니 TypeScript같은 건 --save-dev로devDependencies에 포함시키는게 좋습니다. 그런데 우리는 그냥 전역으로 설치해서 쓰고 있죠 ^^;;

여하간 약간의 시간이 지나면 package가 설치됩니다.


 Angular Animation Module 설치

몇몇개의 Material Component는 Angular Animation Module에 의존성을 가지고 있습니다. 따라서 다음 명령을 수행시켜 Angular Animation Module을 설치해야 합니다.

npm install --save @angular/animations

이 @angular/animations module은 내부적으로 WebAnimation API을 이용합니다. 그런데 모든 browser들이 이 API를 지원하는건 아닙니다. 만약 WebAnimation API를 지원하지 않는 browser를 이용할 경우는 여기를 클릭해서 나온 내용에 따라 따로 처리해 주셔야 합니다.

이제 우리 project에서 src/app 폴더 안에 있는 Root Module인 app.module.ts 파일을 열어 다음과 같이 수정합니다.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

// BrowserAnimationsModule import 구문 추가
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';


import { AppComponent } from './app.component';


@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule  // BrowserAnimationsModule 추가
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

 사용할 Component import

설치가 다 되었으니 이제 사용할 Angular Material Module을 import해야 합니다. 사실 이부분은 나중에 View에 대한 HTML을 작성할 때 해야 되는 작업입니다. 어떤 Component를 이용하여 View를 구성할지 결정이 되어야 import할 수 있기 때문이지요.

나중에 추가할 내용이지만 일단 하나만 먼저 연습삼아 추가해보도록 하겠습니다. 여기서 사용할 것은 Material Table Component입니다. 해당 Component를 사용하기 위해 import하는 작업이 필요합니다. 추후에 구현에 필요한 Component들이 더 추가되어야 하는데 이런 Component들은 실습을 진행하면서 추가하도록 하겠습니다.

우리 project에서 src/app 폴더 안에 있는 Root Module인 app.module.ts 파일을 열어 다음과 같이 수정합니다.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

// BrowserAnimationsModule import 구문 추가
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

// Material Table Component 사용을 위한 MatTableModule import
import { MatTableModule } from '@angular/material/table';

import { AppComponent } from './app.component';


@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,  // BrowserAnimationsModule 추가
    MatTableModule            // MatTableModule 추가
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

 사용할 Theme 설정

application에 적용할 Theme를 설정해 보도록 하죠. 기본적으로 제공되는 built-in theme는 현재 4가지가 존재하는데 그 중 하나를 설정하시면 됩니다.

우리는 Angular CLI를 이용하고 있기 때문에 src 폴더 안에 있는 styles.css 파일을 열어서 다음의 코드를 넣어주면 됩니다. styles.css 파일은 우리 application 전역에 적용되는 global CSS 파일입니다.

@import '~@angular/material/prebuilt-themes/indigo-pink.css';

만약 다른 theme를 이용하고 싶으면 여기 를 참조해 다른 이름의 CSS 파일을 이용하시면 됩니다.


 Gestures 지원을 위한 HammerJS 설치

사용자 Gestures를 지원하기 위해 몇몇개의 Material Component들은 HammerJS에 의존하고 있습니다. 그렇게 때문에 이 HammerJS 역시 설치하고 설정까지 잡아줘야 합니다.

npm install --save hammerjs

설치가 끝나면 우리 application의 시작지점(entry point)인 main.ts 파일을 열어서 제일 상단에 다음과 같은 코드를 추가해 줘야 합니다.

// hammerjs import 추가
import 'hammerjs';

 Material Icon 사용을 위한 설정

Material은 쉽게 사용할 수 있는 Icon을 제공해 줍니다. 마치 Font-Awesome처럼 사용할 수 있습니다. Material Icon을 사용하기 위해서는 index.html을 다음과 같이 수정합니다.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>BookSearchAngular</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!-- Material Icon 설정 -->
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
  <app-root></app-root>
</body>
</html>

Material Icon의 사용방법에 대한 자세한 내용은 여기 를 확인해 보시면 됩니다.


Angular Material을 사용하기 위한 설정이 끝났습니다. 이제 Angular Material Official HomePage의 Components 메뉴를 클릭해 사용할 Component의 종류와 사용방법을 찾아 적절하게 작성만 해 주면 될 듯 싶습니다. 다음 포스트에서 Angular Material을 이용해 화면구성과 Component 설정을 해 보도록 하겠습니다.

728x90
반응형

+ Recent posts