13.7 Angular Component - Built-in directive

빌트인 디렉티브

angular Logo

1. 빌트인 디렉티브(Built-in directive)란?

디렉티브(Directive / 지시자)는 “DOM의 모든 것(모양이나 동작 등)을 관리하기 위한 지시(명령)”이다. HTML 요소 또는 어트리뷰트의 형태로 사용하여 디렉티브가 사용된 요소에게 무언가를 하라는 지시(directive)를 전달한다.

디렉티브는 애플리케이션 전역에서 사용할 수 있는 공통 관심사를 컴포넌트에서 분리하여 구현한 것으로 컴포넌트의 복잡도를 낮추고 가독성을 향상시킨다. 컴포넌트도 뷰를 생성하고 이벤트를 처리하는 등 DOM을 관리하기 때문에 큰 의미에서 디렉티브로 볼 수 있다.

간단한 예제를 살펴보자. textBlue 디렉티브는 해당 요소의 텍스트 컬러를 파란색으로 변경한다.

// text-blue.directive.ts
import { Directive, ElementRef, Renderer2 } from '@angular/core';

@Directive({
  selector: '[textBlue]'
})
export class TextBlueDirective {
  constructor(el: ElementRef, renderer: Renderer2) {
    renderer.setStyle(el.nativeElement, 'color', 'blue');
  }
}

textBlue 디렉티브는 요소의 어트리뷰트로 사용한다. 단, 디렉티브는 모듈의 declarations 프로퍼티에 등록되어야 한다.

// app.component.ts
import { Component } from '@angular/core';
@Component({
  selector: 'app-root',
  template: `
    <div textBlue>textBlue directive</div>
  `
})
export class AppComponent { }

이전 버전인 AngularJS에는 70개 이상의 빌트인 디렉티브가 존재하였으나 컴포넌트 기반의 Angular 디렉티브는 단순화되어 아래와 같이 3가지 유형의 디렉티브를 제공한다.

컴포넌트 디렉티브(Component Directives)
컴포넌트의 템플릿을 표시하기 위한 디렉티브이다. @Component 데코레이터의 메타데이터 객체의 seletor 프로퍼티에 임의의 디렉티브의 이름을 정의한다.
어트리뷰트 디렉티브(Attribute Directives)
어트리뷰트 디렉티브는 HTML 요소의 어트리뷰트와 같이 사용하여 해당 요소의 모양이나 동작을 제어한다. ngClass, ngStyle와 같은 빌트인 어트리뷰트 디렉티브가 있다.
구조 디렉티브(Structural Directives)
구조 디렉티브는 DOM 요소를 반복 생성(ngFor), 조건에 의한 추가 또는 제거(ngIf, ngSwitch)를 통해 DOM 레이아웃(layout)을 변경한다.

이 장에서는 템플릿에 관련한 빌트인 디렉티브인 어트리뷰트 디렉티브와 구조 디렉티브에 집중하기로 한다. 커스텀 디렉티브는 디렉티브에서 자세히 살펴보도록 하자.

2. 빌트인 어트리뷰트 디렉티브(Built-in attribute directive)

어트리뷰트 디렉티브는 HTML 요소의 어트리뷰트와 같이 사용하여 해당 요소의 모양이나 동작을 제어한다. ngClass, ngStyle와 같은 빌트인 디렉티브가 있다.

2.1 ngClass

여러개의 CSS 클래스를 추가 또는 제거한다. 한개의 클래스를 추가 또는 제거할 때는 클래스 바인딩을 사용하는 것이 좋다.

<element [ngClass]="expression">...</element>

ngClass 디렉티브는 우변의 표현식을 평가한 후 HTML class 어트리뷰트를 변경한다. HTML class 어트리뷰트에 의해 이미 클래스가 지정되어 있을 때 ngClass 디렉티브는 HTML class 어트리뷰트를 병합(merge)하여 새로운 HTML class 어트리뷰트를 작성한다. 사용 방법은 아래와 같다.

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

@Component({
  selector: 'app-root',
  template: `
    <ul>
      <!-- 문자열에 의한 클래스 지정  -->
      <li [ngClass]="stringCssClasses">bold blue</li>
      <!-- 배열에 의한 클래스 지정  -->
      <li [ngClass]="ArrayCssClasses">italic red</li>
      <!-- 객체에 의한 클래스 지정  -->
      <li [ngClass]="ObjectCssClasses">bold red</li>
      <!-- 컴포넌트 메소드에 의한 클래스 지정 -->
      <li [ngClass]="getCSSClasses('italic-blue')">italic blue</li>
    </ul>
  `,
  styles: [`
    .text-bold   { font-weight: bold; }
    .text-italic { font-style: italic; }
    .color-blue  { color: blue; }
    .color-red   { color: red; }
  `]
})
export class AppComponent {
  state = true;

  // 문자열 클래스 목록
  stringCssClasses = 'text-bold color-blue';
  // 배열 클래스 목록
  ArrayCssClasses = ['text-italic', 'color-red'];
  // 객체 클래스 목록
  ObjectCssClasses = {
    'text-bold': this.state,
    'text-italic': !this.state,
    'color-blue': !this.state,
    'color-red': this.state
  };

  // 클래스 목록을 반환하는 컴포넌트 메소드
  getCSSClasses(flag: string) {
    let classes;
    if (flag === 'italic-blue') {
      classes = {
        'text-bold': !this.state,
        'text-italic': this.state,
        'color-red': !this.state,
        'color-blue': this.state
      };
    } else {
      classes = {
        'text-bold': this.state,
        'text-italic': !this.state,
        'color-red': this.state,
        'color-blue': !this.state
      };
    }
    return classes;
  }
}

클래스 바인딩은 표현식 또는 클래스 리스트를 나타내는 문자열을 바인딩한다. ngClass 디렉티브는 문자열, 배열, 객체를 바인딩할 수 있다.

2.2 ngStyle

여러개의 HTML 인라인 스타일을 추가 또는 제거한다. 한개의 인라인 스타일을 추가 또는 제거할 때는 스타일 바인딩을 사용하는 것이 좋다.

<element [ngStyle]="expression">...</element>

ngStyle 디렉티브는 우변의 표현식을 평가한 후 HTML style 어트리뷰트를 변경한다. HTML style 어트리뷰트에 의해 이미 스타일이 지정되어 있을 때 ngStyle 디렉티브는 HTML style 어트리뷰트를 병합(merge)하여 새로운 HTML style 어트리뷰트를 작성한다. 사용 방법은 아래와 같다.

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

@Component({
  selector: 'app-root',
  template: `
    <div>
      Width: <input type="text" [(ngModel)]="width">
      <button (click)="increaseWidth()">+</button>
      <button (click)="decreaseWidth()">-</button>
    </div>
    <div>
      Height: <input type="text" [(ngModel)]="height">
      <button (click)="increaseHeight()">+</button>
      <button (click)="decreaseHeight()">-</button>
    </div>
    <button (click)="isShow=!isShow">{{ isShow ? 'Hide' : 'Show' }}</button>
    <!-- 스타일 지정  -->
    <div
      [ngStyle]="{
        'width.px': width,
        'height.px': height,
        'background-color': bgColor,
        'visibility': isShow ? 'visible' : 'hidden'
      }">
    </div>
  `
})
export class AppComponent {
  width = 200;
  height = 200;
  bgColor = '#4caf50';
  isShow = true;

  increaseWidth()  { this.width  += 10; }
  decreaseWidth()  { this.width  -= 10; }
  increaseHeight() { this.height += 10; }
  decreaseHeight() { this.height -= 10; }
}

스타일 바인딩은 표현식을 바인딩한다. ngStyle 디렉티브는 객체를 바인딩할 수 있다.

3. 빌트인 구조 디렉티브(Built-in structural directive)

구조 디렉티브는 DOM 요소를 반복 생성(ngFor), 조건에 의한 추가 또는 제거를 수행(ngIf, ngSwitch)을 통해 뷰의 구조를 변경한다.

  • 구조 디렉티브에는 * 접두사를 추가하며 []을 사용하지 않는다.
  • 하나의 호스트 요소(디렉티브가 선언된 요소)에는 하나의 구조 디렉티브만을 사용할 수 있다.

3.1 NgIf

NgIf 디렉티브는 우변 표현식의 연산 결과가 참이면 해당 요소(호스트 요소)를 DOM에 추가하고 거짓이면 해당 요소(호스트 요소)를 DOM에서 제거한다. 우변의 표현식은 true 또는 false로 평가될 수 있어야한다.

<element *ngIf="expression">...</element>

NgIf 디렉티브 앞에 붙은 *(asterisk)는 아래 구문의 문법적 설탕(syntactic sugar)이다. 즉 위 코드는 아래의 코드로 변환된다.

<ng-template [ngIf]="expression">
  <element>...</element>
</ng-template>

Angular는 *ngIf를 만나면 호스트 요소를 template 태그로 래핑하고 *ngIf를 프로퍼티 바인딩으로 변환한다. NgFor와 NgSwitch 디렉티브도 동일한 패턴을 따른다.

버튼 클릭에 의해 요소를 show/hide하는 간단한 예제를 살펴보자.

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

@Component({
  selector: 'app-root',
  template: `
    <!-- ngIf에 의한 show/hide -->
    <p *ngIf="isShow">Lorem ipsum dolor sit amet</p>

    <!-- 스타일 바인딩에 의한 show/hide -->
    <p [style.display]="isShow ? 'block' : 'none'">Lorem ipsum dolor sit amet</p>
    <button (click)="isShow=!isShow">{{ isShow ? 'Hide' : 'Show' }}</button>
  `,
  styles: [`
    p { background-color: #CDDC39; }
  `]
})
export class AppComponent {
  isShow = true;
}

NgIf 디렉티브를 사용하지 않고 스타일 바인딩 또는 클래스 바인딩을 사용하여 요소의 표시/비표시를 구현할 수도 있다. 하지만 스타일 바인딩 또는 클래스 바인딩에 의해 비표시된 요소는 브라우저에 의해 렌더링이 되지 않지만 DOM에 남아있다. NgIf 디렉티브에 의해 제거된 요소는 DOM에 남아있지 않고 완전히 제거되어 불필요한 자원의 낭비를 방지한다.

show-hide

NgIf에 의해 제거된 요소는 DOM에 남아있지 않는다.

Angular 4부터 ngIf else가 추가되었다. ngIf 우변의 표현식이 참이면 호스트 요소를 DOM에 추가하고 거짓이면 else 이후에 기술한 ng-template 요소의 자식을 DOM에 추가한다.

<!-- if else -->
<element *ngIf="expression; else elseBlock">Truthy condition</element>
<ng-template #elseBlock>Falsy condition</ng-template>

<!-- if else -->
<element *ngIf="expression; then thenBlock else elseBlock"></element>
<ng-template #thenBlock>Truthy condition</ng-template>
<ng-template #elseBlock>Falsy condition</ng-template>

<!-- if -->
<element *ngIf="expression; then thenBlock"></element>
<ng-template #thenBlock>Truthy condition</ng-template>

간단한 예제를 살펴보자.

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

@Component({
  selector: 'app-root',
  template: `
    <div>
      <input type="radio" id="one" name="skill" (click)="mySkill=$event.target.value" value="HTML"> 
      <label for="one"> HTML</label>
      <input type="radio" id="two" name="skill" (click)="mySkill=$event.target.value" value="CSS"> 
      <label for="two"> CSS</label>
    </div>

    <div *ngIf="mySkill==='HTML'; else elseBlock">HTML</div>
    <ng-template #elseBlock><div>CSS</div></ng-template>

    <div *ngIf="mySkill==='HTML'; then thenBlock_1 else elseBlock_1"></div>
    <ng-template #thenBlock_1><div>HTML</div></ng-template>
    <ng-template #elseBlock_1><div>CSS</div></ng-template>
  `
})
export class AppComponent {
  mySkill = 'HTML';
}

3.2 NgFor

NgFor 디렉티브는 컴포넌트 클래스의 컬렉션을 반복하여 호스트 요소(NgFor 디렉티브가 선언된 요소) 및 하위 요소를 DOM에 추가한다. 컬렉션은 일반적으로 배열을 사용한다.

<element *ngFor="let item of items; let i=index">...</element>

<element *ngFor="let item of items; let i=index; let odd=odd; trackBy: trackById">...</element>

NgFor 디렉티브 앞에 붙은 *(asterisk)는 아래 구문의 문법적 설탕(syntactic sugar)이다. 즉 위 코드는 아래의 코드로 변환된다.

<ng-template ngFor let-item [ngForOf]="items">
  <element>...</element>
</ng-template>

<ng-template ngFor let-item [ngForOf]="items" let-i="index" let-odd="odd" [ngForTrackBy]="trackById">
  <element>...</element>
</ng-template>

위 코드는 컴포넌트 클래스의 프로퍼티 items을 바인딩한 후 items의 갯수만큼 순회하며 개별 항목을 item에 할당한다. item(템플릿 입력 변수 / template input variable)은 호스트 요소 및 하위 요소에서만 유효한 로컬 변수이다. items에 해당하는 바인딩 객체는 일반적으로 배열을 사용하지만 반드시 배열만 사용할 수 있는 것은 아니다. ES6의 for…of에서 사용할 수 있는 이터러블(iterable)이라면 사용이 가능하다. 단 문자열, Map, 배열이 아닌 일반 객체는 사용할 수 없다.

인덱스를 취득할 필요가 있는 경우, 인덱스를 의미하는 index를 사용하여 변수에 인덱스를 할당받을 수 있다. index 이외에도 first, last, even, odd와 같은 로컬 변수가 제공된다. 자세한 내용은 NgFor API reference를 참조하기 바란다.

<element *ngFor="let item of items; let i=index">...</element>

컴포넌트 클래스 프로퍼티의 배열을 리스트로 출력하는 예제를 살펴보자.

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

interface User { 
  id: number; 
  name: string 
}

@Component({
  selector: 'app-root',
  template: `
    <!-- user를 추가한다 -->
    <input type="text" #name placeholder="이름을 입력하세요">
    <button (click)="addUser(name.value)">add user</button>
    <ul>
      <!-- users 배열의 length만큼 반복하며 li 요소와 하위 요소를 DOM에 추가한다 -->
      <li *ngFor="let user of users; let i=index">
        {{ i }}: {{ user.name }}
        <!-- 해당 user를 제거한다 -->
        <button (click)="removeUser(user.id)">X</button>
      </li>
    </ul>
    <pre>{{ users | json }}</pre>
  `
})
export class AppComponent {
  users: User[] = [
    { id: 1, name: 'Lee' },
    { id: 2, name: 'Kim' },
    { id: 3, name: 'Baek' }
  ];

  // user를 추가한다
  addUser(name: string) {
    if (name) {
      this.users.push({ id: this.getNewUserId(), name: name });
    }
  }

  // 해당 user를 제거한다.
  removeUser(userid: number) {
    this.users = this.users.filter(({ id }) => id !== userid);
  }

  // 추가될 users의 id를 반환한다
  getNewUserId(): number {
    return this.users.length ? Math.max(...this.users.map(({ id }) => id)) + 1 : 1;
  }
}

users 배열의 length만큼 반복하며 li 요소와 하위 요소를 DOM에 추가한다. 템플릿의 for of 구문에서 사용된 user 변수는 users 배열의 개별요소를 일시적으로 저장하며 호스트 요소의 하위 요소에서만 유효한 로컬 변수이다.

NgFor 디렉티브는 컬렉션 데이터(users)가 변경되면 컬렉션과 연결된 모든 DOM 요소를 제거하고 다시 생성한다. 이는 컬렉션의 변경 사항을 추적(tracking)할 수 없기 때문이다. 때문에 크기가 매우 큰 컬렉션을 다루는 경우, 퍼포먼스 상의 문제를 발생시킬 수 있다. NgFor 디렉티브는 퍼포먼스를 향상시키기 위한 기능으로 trackBy를 제공한다.

trackBy 기능을 추가하여 위 예제를 수정하여 보자.

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

interface User { 
  id: number; 
  name: string 
}

@Component({
  selector: 'app-root',
  template: `
    <!-- user를 추가한다 -->
    <input type="text" #name placeholder="이름을 입력하세요">
    <button (click)="addUser(name.value)">add user</button>
    <ul>
      <!-- users 배열의 length만큼 반복하며 li 요소를 DOM에 추가한다 -->
      <!-- 변경을 트랙킹을 할 수 있도록 trackBy를 추가하였다. -->
      <li *ngFor="let user of users; let i=index; trackBy: trackByUserId">
        {{ i }}: {{ user.name }}
        <!-- 해당 user를 제거한다 -->
        <button (click)="removeUser(user.id)">X</button>
      </li>
    </ul>
    <pre>{{ users | json }}</pre>
  `
})
export class AppComponent {
  users: User[] = [
    { id: 1, name: 'Lee' },
    { id: 2, name: 'Kim' },
    { id: 3, name: 'Baek' }
  ];

  // user를 추가한다
  addUser(name: string) {
    if (name) {
      this.users.push({ id: this.getNewUserId(), name: name });
    }
  }

  // 해당 user를 제거한다.
  removeUser(userid: number) {
    this.users = this.users.filter(({ id }) => id !== userid);
  }

  // 추가될 users의 id를 반환한다
  getNewUserId(): number {
    return this.users.length ? Math.max(...this.users.map(({ id }) => id)) + 1 : 1;
  }

  // 변경 트래킹 기준을 반환한다.
  trackByUserId(index: number, user: User) {
    // user.id를 기준으로 변경을 트래킹한다.
    return user.id; // or index
  }
}

user 객체의 id 프로퍼티를 사용하여 변경을 트랙킹할 수 있도록 trackByUserId 메소드를 추가하였다. 이때 user 객체의 id 프로퍼티는 유니크하여야 한다. user 객체의 id 프로퍼티를 사용하지 않고 trackByUserId에 인자로 전달된 index를 사용하여도 무방하다.

add user 또는 X 버튼을 클릭하면 해당 user를 추가/제거한다. 예를 들어 3번째 user 객체를 제거하면 users의 변경을 DOM에 반영하여야 한다. 이때 trackBy를 사용하지 않는 경우 NgFor는 DOM을 다시 생성한다. trackBy를 사용한 경우 user.id를 기준으로 컬렉션의 변경을 트래킹하기 때문에 퍼포먼스가 향상된다.

일반적인 경우 NgFor는 충분히 빠르기 때문에 trackBy에 의한 퍼포먼스 최적화는 기본적으로 필요하지 않다. trackBy는 퍼포먼스에 문제가 있는 경우에만 사용한다.

3.3 NgSwitch

NgSwitch 디렉티브는 switch 조건에 따라 여러 요소 중에 하나의 요소를 선택하여 DOM에 추가한다. JavaScript의 switch문과 유사하게 동작한다.

<element [ngSwitch]="expression">
  <!-- switch 조건이 'case1'인 경우 DOM에 추가 -->
  <element *ngSwitchCase="'case1'">...<element>
  <!-- switch 조건이 'case2'인 경우 DOM에 추가 -->
  <element *ngSwitchCase="'case2'">...<element>
  <!-- switch 조건과 일치하는 ngSwitchCase가 없는 경우 DOM에 추가 -->
  <element *ngSwitchDefault>...<element>
</element>

NgSwitch 디렉티브 앞에 붙은 *(asterisk)는 아래 구문의 문법적 설탕(syntactic sugar)이다. 즉 위 코드는 아래의 코드로 변환된다.

<element [ngSwitch]="expression">
  <ng-template [ngSwitchCase]="'case1'">
    <element>...</element>
  </ng-template>
  <ng-template [ngSwitchCase]="'case2'">
    <element>...</element>
  </ng-template>
  <ng-template ngSwitchDefault>
    <element>...</element>
  </ng-template>
</element>

NgSwitch 디렉티브를 활용한 예제는 아래와 같다.

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

@Component({
  selector: 'app-root',
  template: `
    <input type='text' [(ngModel)] ="num" placeholder="숫자를 입력하세요">
    <div [ngSwitch]="num">
      <div *ngSwitchCase="'1'">One</div>
      <div *ngSwitchCase="'2'">Two</div>
      <div *ngSwitchCase="'3'">Three</div>
      <div *ngSwitchDefault>This is Default</div>
    </div>
  `
})
export class AppComponent {
  num: string;
}

Reference

Back to top
Close