Component Data Sharing
이번 포스트는 Component 간의 데이터 공유에 대해서 알아보고 그 내용을 기반으로 우리 mySearchProject의 도서검색 부분을 완성해 나가도록 하겠습니다.
최종적으로 완성된 프로그램은 여기를 클릭하시면 실행시켜 보실 수 있습니다.
실행시켜 보시면 이전에 비해 세가지 기능이 추가되었습니다.
- 도서 검색 시 도서 종류(국내도서, 국외도서, 국내외도서)에 대한 Filtering이 가능합니다.
- 키워드 입력 후 Search버튼을 클릭하면 해당 키워드에 대한 책만 list-box에 출력됩니다.
- list-box에 출력된 책 중 하나를 선택하면 해당 책에 대한 세부내역을 detail-box에 출력합니다.
이 기능들을 구현하려면 Component간의 데이터 공유 방법을 아셔야 합니다. Component간 데이터를 공유하는 방법은 여러가지가 있는데 하나씩 살펴보면서 우리 코드에 적용해 보겠습니다.
그럼 천천히 한번 살펴보기로 하죠.
@Input Decorator
이전에 View의 포함관계를 설명하면서 Component Tree
에 대한 언급을 한 적이 있습니다. Component간의 부모-자식 관계가 성립되면 서로간의 데이터 연결통로가 생성됩니다. 이를 통해 부모 Component와 자식 Component간의 데이터 공유가 이루어질 수 있습니다.
먼저 부모 Component에서 자식 Component로 데이터를 전달하는 방법에 대해서 알아보죠.
만약 부모 Component가 사용자 입력양식을 가지고 있다면 Client에 의해서 사용자 입력양식의 상태값이 변경될 수 있고 그 상태값를 자식 Component와 공유할 필요가 있습니다. 우리 예제로 설명하자면 상위 Component인 book-search-main
Component에서 Client가 설정한 도서 종류가 하위 Component인 search-box
Component에 전해져야 제대로 된 검색을 수행할 수 있다는 말입니다.
이런 경우 부모 Component는 property binding
을 이용해 자식 Component에게 데이터를 전달해 줄 수 있습니다. 이렇게 전달된 데이터는 @Input
decorator에 의해서 자식 Component에서 사용될 수 있습니다.
우리 예제를 수정해서 Client가 Select Box에서 선택한 도서 종류 정보가 하위 Component인 search-box
Component와 공유되는지 확인해 보겠습니다.
book-search-main.component.html
파일의 내용을 다음과 같이 수정합니다.
<div class="bookSearch-outer">
<div class="d-flex align-items-center p-3 my-3 text-white-50 bg-purple rounded box-shadow">
<img class="mr-3" src="assets/images/search-icon.png" alt="" width="48" height="48">
<div class="lh-100">
<h5 class="mb-0 text-white lh-100">Search Result</h5>
</div>
</div>
<div class="example-container">
<mat-form-field>
<mat-select placeholder="도서종류"
#bookCategorySelect
[(ngModel)]="selectedValue"
(ngModelChange)="changeValue(bookCategorySelect.value)">
<mat-option *ngFor="let category of bookCaterory"
[value]="category.value">
{{ category.viewValue }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<div>
<app-search-box [bookCategory]="displayCategoryName"></app-search-box>
</div>
<div>
<app-detail-box></app-detail-box>
</div>
<div>
<app-list-box></app-list-box>
</div>
</div>
위의 코드에서 다음의 코드를 주의해서 보시면 됩니다.
<mat-select placeholder="도서종류"
#bookCategorySelect
[(ngModel)]="selectedValue"
(ngModelChange)="changeValue(bookCategorySelect.value)">
<mat-option *ngFor="let category of bookCaterory"
[value]="category.value">
{{ category.viewValue }}
</mat-option>
</mat-select>
이전에 배웠던 Tempalte Reference Variable과 양방향 바인딩을 이용해 Client가 도서 종류를 변경하면 changeValue()
method가 호출됩니다.
이 method는 book-search-main.component.ts
안에 정의되어 있어야 하겠죠.
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-book-search-main',
templateUrl: './book-search-main.component.html',
styleUrls: ['./book-search-main.component.css',
'./offcanvas.css']
})
export class BookSearchMainComponent implements OnInit {
selectedValue = null;
displayCategoryName = null;
bookCaterory = [
{value: 'all', viewValue: '국내외도서'},
{value: 'country', viewValue: '국내도서'},
{value: 'foreign', viewValue: '국외도서'}
];
constructor() { }
ngOnInit() {
}
changeValue(category: string): void {
for(let element of this.bookCaterory ) {
if(element.value == category) {
this.displayCategoryName = element.viewValue;
}
}
}
}
method의 하는일을 보니 Client가 선택한 도서 종류를 가지고 displayCategoryName
이라는 속성의 값을 변경하고 있습니다.
이 displayCategoryName
속성이 바로 자식 Component인 search-box Component에게 전달되는 데이터입니다.
다시 위쪽의 book-search-main.component.html
의 내용을 보면 아래와 같은 코드가 있습니다.
<div>
<app-search-box [bookCategory]="displayCategoryName"></app-search-box>
</div>
search-box Component에 property binding을 이용해 bookCategory
라는 이름으로 displayCategoryName
속성을 바인딩해 놓은걸 확인하실 수 있습니다.
이제 search-box.component.ts
의 내용을 보죠
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-search-box',
templateUrl: './search-box.component.html',
styleUrls: ['./search-box.component.css']
})
export class SearchBoxComponent implements OnInit {
@Input() bookCategory:string;
keyword = null;
constructor() { }
ngOnInit() {
}
setKeyword(keyword: string): void {
this.keyword = keyword;
}
inputChange(): void {
}
}
@Input
decorator를 볼 수 있습니다. @Input
decorator를 이용하기 위해서 import를 해 줘야 하는것도 잊지 마시구요.
@Input() bookCategory:string;
위의 코드처럼 bookCategory
라는 이름으로 부모 Component가 property binding으로 전달해준 데이터를 받을 수 있습니다.
이 속성을 View에 interpolation을 이용해서 출력하면 될 듯 합니다.
다음은 View에 rendering되는 search-box.component.html
입니다.
<div class="example-container">
<mat-toolbar class="search-toolbar-style">
Search Keyword : {{keyword}}
<ng-container *ngIf="bookCategory != null">
( {{bookCategory}} )
</ng-container>
</mat-toolbar>
<mat-form-field>
<input matInput #inputKeyword placeholder="Search Keyword"
[(ngModel)]="keyword" (ngModelChange)="inputChange()">
</mat-form-field>
<button mat-raised-button color="warn"
(click)="setKeyword(inputKeyword.value)">Search!</button>
</div>
Toolbar부분에 {{keyword}}
와 함께 {{bookCategory}}
를 이용해서 속성과 binding시킨 걸 확인할 수 있습니다. 만약 bookCategory값이 null이면 출력되지 않게끔 built-in directive를 이용해 처리했습니다.
위의 코드에서 보듯이 자식 Component인 select-box
Component는 자신에게 데이터를 주는 부모 Component가 어떤 Component인지는 알 필요가 없습니다. 단지 전달된 데이터를 사용할 수 있도록 해주는 property의 이름과 data type만이 필요할 뿐이죠. Component간의 Loosely Coupling을 유지하면서 데이터를 공유할 수 있습니다.
기본적인 @Input
decorator를 사용하는 방법에 대해 설명했는데 몇개의 응용이 있습니다.
우리는 부모 Component의 book-search-main.component.html
에서 property binding을 이용해 bookCategory
라는 이름의 property를 사용했습니다. 이를 사용하기 위해서 자식 Component인 search-box
Component에서 역시 같은 이름으로 사용했구요. 만약 다른이름으로 사용하실려면 아래와 같이 처리하시면 됩니다.
@Input('bookCategory') mySelected:string;
bookCategory라는 이름의 property 대신 mySelected
property를 사용할 수 있습니다. interpolation 역시 mySelected으로 사용해야겠죠.
지금까지의 예는 모두 부모 Component가 전달해 준 데이터를 그대로 가져다 사용하는 방식입니다. 만약 부모 Component가 전달해 준 데이터를 가공해서 자식 Component에서 사용하려면 어떻게 해야 할까요?
setter
를 이용하면 이 작업을 할 수 있습니다. 우리의 코드가 이렇게 바뀌겠네요.
import { Component, OnInit, Input } from '@angular/core';
@Component({
selector: 'app-search-box',
templateUrl: './search-box.component.html',
styleUrls: ['./search-box.component.css']
})
export class SearchBoxComponent implements OnInit {
//@Input() bookCategory:string;
//@Input('bookCategory') mySelected:string;
_bookCategory: string;
@Input()
set bookCategory(value: string) {
if( value != null ) {
// 추가적인 작업이 들어올 수 있습니다.
this._bookCategory = 'category: ' +value;
} else {
this._bookCategory = value;
}
}
keyword = null;
constructor() { }
ngOnInit() {
}
setKeyword(keyword: string): void {
this.keyword = keyword;
}
inputChange(): void {
}
}
사용되는 setter의 이름과 부모 Component가 property binding하는 property의 이름이 같아야 합니다. interpolation은 _bookCategory
으로 변경되어야 하겠네요.
한가지 추가적으로 기억하셔야 할 점은 이렇게 부모 Component가 자식 Component에게 데이터를 전달해 줄 때 이 방식이 call-by-value
방식이 아닌 call-by-reference
방식이라는 것입니다. 즉, 우리의 예제에서 부모 Component와 자식 Component가 둘 다 bookCategory
를 reference하고 있는 형태입니다. 이렇게 연결된 상태에서 부모 Component가 해당 property의 값을 변경시키면 그 값을 자식 Component가 공유하고 있으므로 변경된 값을 그 즉시 사용할 수 있는 것이죠.
이렇게 생각하면 자식 Component가 공유되고 있는 property의 값을 변경하면 그 변경 내용이 부모 Component에게 영향을 미쳐야 합니다. 하지만 실제로 코드 작업을 해 보면 그렇지 않다는 것을 확인하실 수 있습니다. 왜 이런 현상이 발생할까요? 만약 자식 Component에서 변경된 값이 부모 Component에게 영향을 주게끔 처리하면 나중에 이 공유데이터의 변화를 예측하기 힘들어지게 됩니다. 데이터의 변경을 tracking하기 힘들어진다는 것이죠. 지금은 간단한 경우이니 별 문제가 안되지만 프로그램이 커지게 되면 이런 데이터의 공유 문제가 프로그램의 구현과 디버깅을 힘들게 하는 원인이 됩니다.
Angular는 Stateful Component
와 Stateless Component
의 개념이 있습니다. Stateful Component는 다른말로 Smart Component 라고도 하는데 이 Component는 데이터의 정보를 변경하거나 저장할 수 있습니다. 하지만 Dumb Component라고 불리는 Stateless Component는 단지 상태 정보를 참조만 해서 이용할 수 있습니다. 우리의 예제에서 상위 Component인 book-search-main
Component는 Stateful Component입니다. 반면 자식 Component인 search-box
Component는 Stateless Component이구요. 그렇기 때문에 자식 Component에서 공유된 변수에 대한 변경을 해 주어도 상위 Component에 영향을 미치지 않게 되는 것입니다. 조금 어려운 개념인데 이 Stateful과 Stateless에 대해 조금 더 알고싶으시면 여기를 참조하시면 됩니다.
쉽게 말하자면 @Input
decorator를 이용하면 부모 Component에서 자식 Component에게 데이터를 전달할 수 있지만 그 반대는 허용되지 않는군요. 이 문제를 해결하기 위해 @Output
decorator를 사용할 수 있습니다. 즉, 자식 Component에서 변경된 사항을 부모 Component에게 전달하는 방법이 따로 있다는 것이죠. 다음 포스트에서는 @Output decorator에 대해서 알아보도록 하겠습니다.
'Web Programming > Angular - TypeScript' 카테고리의 다른 글
Angular @ViewChild 데이터공유 (0) | 2018.08.28 |
---|---|
Angular @Output 데이터공유 (0) | 2018.08.28 |
Angular Material Table (0) | 2018.08.28 |
Angular 예제 따라하기 (0) | 2018.08.28 |
SPA형식의 Web Application (0) | 2018.08.28 |