/ ANGULAR

Angular 강좌(9) - 데이터공유(@Input)

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


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 ComponentStateless 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에 대해서 알아보도록 하겠습니다.

End.


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