Skip to content

1. 기본 디렉티브


디렉티브는 Angular 애플리케이션 안에 있는 엘리먼트에 어떤 동작을 추가하는 클래스를 의미한다. Angular는 폼, 목록, 스타일 등에 적용할 수 있는 기본 디렉티브를 다양하게 제공한다.


Note


Angular 디렉티브는 구체적으로 이렇게 구분할 수 있다:


1] 컴포넌트(Components): 템플릿이 존재하는 디렉티브이다. 디렉티브 중에서는 컴포넌트 타입이 가장 많이 사용된다.

2] 어트리뷰트 디렉티브(Attribute directives): 엘리먼트, 컴포넌트, 디렉티브의 모습이나 동작을 변경하는 디렉티브이다.

3] 구조 디렉티브(Structural directives): 조건에 따라 DOM 엘리먼트를 추가하거나 제거하는 디렉티브이다.


이 가이드 문서는 Angular가 기본으로 제공하는 어트리뷰트 디렉티브와 구조 디렉티브에 대해 설명한다.


1. 기본 어트리뷰트 디렉티브

어트리뷰트 디렉티브는 HTML 엘리먼트, 어트리뷰트, 프로퍼티, 컴포넌트의 동작을 변경한다.


RouterModule이나 FormsModule과 같이 어트리뷰트 디렉티브를 제공하는 NgModule도 많다. 이 중 자주 사용하는 어트리뷰트 디렉티브는 이런 것들이 있다:


  • NgClass: CSS 클래스를 추가하거나 제거한다.
  • NgStyle: HTML 스타일을 추가하거나 제거한다.
  • NgModel: HTML 폼 엘리먼트에 양방향 데이터 바인딩을 연결한다.


Note

  • 기본 디렉티브는 모듈 외부로 공개된 API만 사용한다.
  • 외부로 공개되지 않은 API는 접근할 수 없다.


2. NgClass로 클래스 추가/제거하기

ngClass를 사용하면 CSS 클래스 여러 개를 엘리먼트에 동시에 추가하거나 제거할 수 있다.


Note

  • 클래스를 하나만 추가하거나 제거한다면 NgClass보다 클래스 바인딩을 사용하는 것이 더 좋다.


1) NgClass에 표현식 사용하기

엘리먼트에 스타일을 지정하듯이, 엘리먼트에 [ngClass]를 추가하고 이 디렉티브에 표현식을 지정할 수 있다. app.component.ts 파일에서 isSpecial 프로퍼티 값이 true로 지정되었다고 하자. 그러면 isSpecial의 값이 ngClass에 반영되면서 <div>special 클래스가 추가된다.


src/app/app.component.html
<!-- "special" 클래스는 프로퍼티 바인딩으로 켜고 끌 수 있습니다. -->
<div [ngClass]="isSpecial ? 'special' : ''">This div is special</div>


2) NgClass에 메서드 사용하기

NgClass에 메서드를 사용하려면 이 메서드를 컴포넌트 클래스에 정의해야 한다. 아래 예제에서 setCurrentClasses() 메서드는 컴포넌트의 다른 프로퍼티 값을 참조해서 객체 형태로 currentClasses 프로퍼티 값을 할당한다. 이 때 객체의 키는 CSS 클래스 이름이다. 키에 할당된 값이 ture이면 ngClass가 해당 키를 클래스로 추가한다. 키에 할당된 값이 false이면 ngClass가 해당 키를 클래스에서 제거한다.


src/app/app.component.ts
currentClasses: Record<string, boolean> = {};
/* . . . */
setCurrentClasses() {
  // CSS 클래스는 컴포넌트 프로퍼티 값에 따라 추가되거나 제거됩니다.
  this.currentClasses =  {
    saveable: this.canSave,
    modified: !this.isUnchanged,
    special:  this.isSpecial
  };
}


템플릿에서는 엘리먼트의 ngClasscurrentClasses 프로퍼티와 바인딩하면 된다:


src/app/app.component.html
<div [ngClass]="currentClasses">
  This div is initially saveable, unchanged, and special.
</div>


이렇게 구현하면 Angular는 변화를 감지할 때마다 엘리먼트에 적용되는 클래스를 계산한다. 그래서 ngOnInit()이 실행될 때 setCurrentClasses()가 처음 실행되며, 버튼을 클릭할 때마다 계속 실행된다. 이 동작은 ngClass가 유발하는 것이 아니다.


3. NgStyle로 인라인 스타일 지정하기

NgStyle을 활용하면 컴포넌트 상태에 따라 달라지는 여러 인라인 스타일을 동시에 지정할 수 있다.


NgStyle을 사용하기 위해 컴포넌트 클래스에 메서드를 추가한다. 아래 예제에서 setCurrentStyles()는 컴포넌트의 다른 프로퍼티 값을 참조해서 객체 형태로 currentStyles 프로퍼티 값을 할당한다.


src/app/app.component.ts
currentStyles: Record<string, string> = {};
/* . . . */
setCurrentStyles() {
  // CSS 스타일은 컴포넌트 프로퍼티 값에 따라 지정됩니다.
  this.currentStyles = {
    'font-style':  this.canSave      ? 'italic' : 'normal',
    'font-weight': !this.isUnchanged ? 'bold'   : 'normal',
    'font-size':   this.isSpecial    ? '24px'   : '12px'
  };
}


엘리먼트의 스타일을 지정하려면 ngStylecurrentStyles 프로퍼티와 바인딩하면 된다.


src/app/app.component.html
<div [ngStyle]="currentStyles">
  This div is initially italic, normal weight, and extra large (24px).
</div>


이렇게 구현하면 Angular는 변화를 감지할 때마다 엘리먼트에 적용되는 스타일을 계산한다. 그래서 ngOnInit()이 실행될 때 setCurrentStyles()가 처음 실행되며, 버튼을 클릭할 때마다 계속 실행된다. 이 동작은 ngStyle이 유발하는 것이 아니다.


4. ngModel로 프로퍼티 값 표시하기, 변경된 값 반영하기

NgModel 디렉티브를 활용하면 데이터 프로퍼티의 값을 화면에 표시할 수 있으며, 사용자가 이 값을 변경했을 때 클래스 프로퍼티에 반영할 수 있다.


NgModule imports 목록에 FormsModule를 불러와서 추가한다.


src/app/app.module.ts (FormsModule 불러오기)
import { FormsModule } from "@angular/forms"; // <--- FormsModule 패키지 로드
/* . . . */
@NgModule({
  /* . . . */

  imports: [
    BrowserModule,
    FormsModule, // <--- NgModule에 로드
  ],
  /* . . . */
})
export class AppModule {}


HTML <form> 엘리먼트에 [(ngModel)] 바인딩 문법을 추가하고 이 바인딩 오른쪽에 프로퍼티를 할당한다. 이 예제에서는 name을 지정한다.


src/app/app.component.html (NgModel 예제)
<label for="example-ngModel">[(ngModel)]:</label>
<input [(ngModel)]="currentItem.name" id="example-ngModel" />


[(ngModel)]라는 문법은 프로퍼티를 단순하게 바인딩하는 문법이다.


바인딩 동작을 커스터마이징하려면 [()] 문법을 사용하지 않고 프로퍼티 바인딩([])과 이벤트 바인딩(())을 나눠서 작성하면 된다. 이 때 프로퍼티 바인딩은 프로퍼티 값을 바인딩하며, 이벤트 바인딩은 이 값이 변경되는 것을 감지하는 동작을 한다. 아래 코드는 <input> 엘리먼트에 입력된 값을 대문자로 변환하는 예제 코드이다:


src/app/app.component.html
<input
  [ngModel]="currentItem.name"
  (ngModelChange)="setUppercaseName($event)"
  id="example-uppercase"
/>


NgModel은 다양한 방식으로 구현할 수 있다:


001


1) NgModel과 값 접근자(value accessor)

NgModel 디렉티브는 ControlValueAccessor가 제공되는 엘리먼트에서만 제대로 동작한다. 그리고 Angular는 표준 HTML 폼 엘리먼트에 대해서는 모두 값 접근자를 제공한다.


표준 폼 엘리먼트가 아닌 엘리먼트나 서드 파티 컴포넌트에 [(ngModel)]을 적용하려면 값 접근자를 직접 구현해야 한다.


Note

  • Angular 컴포넌트에는 값 접근자나 NgModel을 사용할 필요 없이 Angular가 제공하는 양방향 바인딩 문법을 활용하면 된다.


5. 기본 구조 디렉티브

구조 디렉티브는 HTMl의 모습을 조작한다. 구조 디렉티브는 조건에 맞는 엘리먼트를 DOM 트리에 추가하거나, 제거, 조작하는 방식으로 DOM 구조를 변형한다.


이번 섹션에서는 구조 디렉티브 중 가장 많이 사용하는 이런 디렉티브들을 알아보자:


  • NgIf: 조건에 따라 템플릿의 일부를 DOM 트리에 추가하거나 DOM 트리에서 제거한다.
  • NgFor: 배열에 있는 항목마다 템플릿 일부를 반복한다.
  • NgSwitch: 조건에 맞는 화면을 DOM 트리에 추가한다.


6. NgIf로 엘리먼트 추가/제거하기

NgIf 디렉티브를 사용하면 엘리먼트를 이 호스트 엘리먼트에 추가하거나 호스트 엘리먼트에서 제거할 수 있다.


NgIf에 할당되는 값이 false이면 Angular는 해당 엘리먼트를 자식 노드와 함께 DOM 트리에서 완전히 제거한다. 그래서 메모리 사용량과 리소스 사용량을 줄일 수 있다.


엘리먼트를 DOM 트리에 추가하거나 제거하려면 *ngIf 디렉티브에 isActive와 같은 조건 표현식을 바인딩하면 된다.


src/app/app.component.html
<app-item-detail *ngIf="isActive" [item]="item"></app-item-detail>


이렇게 작성하면 isActive 표현식이 참으로 평가되는 값을 반환할 때 NgIfItemDetailComponent를 DOM 트리에 추가한다. 그리고 표현식이 거짓으로 평가되는 값을 반환하면 NgIfItemDetailComponent를 DOM 트리에서 제거하고 해당 컴포넌트와 이 컴포넌트의 자식 컴포넌트를 모두 종료한다.


1) null 값 방지하기

기본적으로 NgIfnull 값이 바인딩되면 엘리먼트를 DOM 트리에 추가하지 않는다.


그래서 NgIf를 가드로 사용하려면 <div>*ngIf="프로퍼티"라는 식으로 구현하면 된다. 아래 코드에서는 currentCustomer 값이 존재하기 때문에 currentCustomer의 이름이 화면에 표시된다.


src/app/app.component.html
<div *ngIf="currentCustomer">Hello, {{currentCustomer.name}}</div>


하지만 프로퍼티 값이 null이면 Angular는 <div>를 DOM 트리에 추가하지 않는다. 아래 코드에서는 nullCustomer 값이 null이기 때문에 해당 <div>가 화면에 표시되지 않는다.


src/app/app.component.html
<div *ngIf="nullCustomer">Hello, <span>{{nullCustomer}}</span></div>


7. NgFor로 배열 항목 표시하기

NgFor 디렉티브를 활용하면 배열에 있는 항목을 화면에 표시할 수 있다.


1] 개별 항목마다 반복할 HTML 템플릿을 구성한다.

2] 배열을 순회하기 위해 *ngFor를 적용하고 이 디렉티브 오른쪽에 let item of items을 연결한다.


src/app/app.component.html
<div *ngFor="let item of items">{{item.name}}</div>


Angular는 "let item of items"라는 문자열을 이렇게 처리한다:


  • items 배열에 있는 개별 항목을 지역 변수 item에 할당한다.
  • item 변수는 반복되는 HTML 템플릿 안에서만 사용할 수 있다.
  • "let item of items"라는 문자열이 <ng-template>으로 변환된다.
  • 배열에 있는 항목마다 <ng-template>이 반복된다.


1) 컴포넌트 화면 반복하기

*ngFor는 컴포넌트 엘리먼트를 반복할 때도 활용할 수 있다. 아래 코드는 <app-item-detail> 컴포넌트를 반복하는 예제 코드이다.


src/app/app.component.html
<app-item-detail *ngFor="let item of items" [item]="item"></app-item-detail>


이 때 템플릿 입력 변수 item은 이런 범위에서 참조할 수 있다:


  • ngFor가 적용된 호스트 엘리먼트 안쪽에서
  • 호스트 엘리먼트의 자식 엘리먼트에서


아래 코드는 item을 문자열 바인딩하면서 한 번, <app-item-detail> 컴포넌트의 item 프로퍼티에 바인딩할 때 한 번 참조했다.


src/app/app.component.html
<div *ngFor="let item of items">{{item.name}}</div>
<!-- . . . -->
<app-item-detail *ngFor="let item of items" [item]="item"></app-item-detail>


2) *ngFor에서 index 활용하기

*ngFor로 반복되는 템플릿 안쪽에서는 index를 받아서 활용할 수도 있다.


*ngFor에 작성한 단축문법 안에 세미콜론(;)과 let i=index을 추가해 보자. 아래 코드는 indexi 변수로 받아온 다음에 이름 앞에 표시하는 예제 코드이다.


src/app/app.component.html
<div *ngFor="let item of items; let i=index">{{i + 1}} - {{item.name}}</div>


이 때 인덱스 프로퍼티는 NgFor 디렉티브 컨텍스트로 제공되며, 0부터 시작해서 배열의 항목을 순회할 때마다 증가한다.


*ngFor 표현식을 이렇게 작성하면 호스트 엘리먼트 근처에 <ng-template>이 추가되면서 배열에 있는 item 항목마다 반복되면서 item 프로퍼티가 바인딩된다.


8. 조건이 참일 때만 엘리먼트 반복하기

조건이 참일 때만 HTML 영역을 반복하려면 *ngFor를 적용한 엘리먼트 안에 *ngIf 컨테이너 엘리먼트를 추가하면 된다. 그리고 이 때 <ng-container>를 활용하면 추가 엘리먼트 단계를 생략할 수 있다.


다만, 구조 디렉티브는 DOM 트리에서 노드를 추가하거나 제거하기 때문에 엘리먼트 하나에는 구조 디렉티브도 하나만 적용할 수 있다.


1) *ngFortrackBy로 항목 추적하기

배열 항목이 변경되는 것을 추적하면 불필요한 함수 실행 횟수를 줄일 수 있다. *ngFortrackBy 프로퍼티를 사용하면 Angular는 배열에서 변경된 항목만 화면에서 갱신하고 렌더링을 새로 한다. 이 경우에는 배열 전체를 다시 불러오지 않는다.


NgFor가 추적할 값을 반환하는 메서드를 컴포넌트에 추가한다. 이번 예제에서는 항목의 id를 추적해 보자. 그러면 이미 브라우저에 렌더링 된 id 항목은 화면에 그대로 유지하며 서버로 쿼리를 보내지 않는다.


src/app/app.component.ts
trackByItems(index: number, item: Item): number { return item.id; }


단축문법 표현식에 trackBytrackByItems() 메서드를 연결한다.

src/app/app.component.html
<div *ngFor="let item of items; trackBy: trackByItems">
  ({{item.id}}) {{item.name}}
</div>


Change ids 버튼은 새로운 item.id로 항목을 추가하는 버튼이다. trackBy가 동작하는 것을 확인해 보자. Reset items 버튼을 누르면 item.id를 원래대로 되돌린다.


  • trackBy가 사용되지 않은 쪽에서는 버튼을 누르면 DOM 엘리먼트가 전부 교체된다.
  • trackBy를 사용하면 실제로 id가 변경된 엘리먼트만 교체된다.


002


9. DOM 엘리먼트 추가 없이 디렉티브 적용하기

Angular가 제공하는 <ng-container>는 스타일이나 레이아웃에 영향을 주지 않으면서 엘리먼트를 묶을 때 사용한다. <ng-container>는 실제로 DOM 트리에 추가되지 않기 때문이다.


그래서 <ng-container>는 템플릿 어느 곳에라도 자유롭게 사용할 수 있다.


<p> 엘리먼트 일부에 <ng-container>을 적용하려면 이렇게 구현하면 된다.


src/app/app.component.html (ngif-ngcontainer)
<p>
  I turned the corner
  <ng-container *ngIf="hero"> and saw {{hero.name}}. I waved </ng-container>
  and continued on my way.
</p>


003


1] FormsModule이 제공하는 ngModel 디렉티브를 불러온다.

2] 상위 Angular 모듈에 FormsModule을 등록한다.

3] 조건에 해당되는 <option>만 표시하려면 <option><ng-container>로 감싸면 된다.


src/app/app.component.html (select-ngcontainer)
<div>
  Pick your favorite hero (<label
    ><input type="checkbox" checked (change)="showSad = !showSad" />show
    sad</label
  >)
</div>
<select [(ngModel)]="hero">
  <ng-container *ngFor="let h of heroes">
    <ng-container *ngIf="showSad || h.emotion !== 'sad'">
      <option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
    </ng-container>
  </ng-container>
</select>


004


10. NgSwitch 활용하기

JavaScript switch 구문과 비슷하게, NgSwitch는 여러 엘리먼트 중에서 조건에 맞는 엘리먼트 하나를 DOM 트리에 추가하는 디렉티브이다.


NgSwitch는 디렉티브 3개로 구성된다:


  • NgSwitch: 관련 디렉티브의 동작을 조작하는 어트리뷰트 디렉티브이다.
  • NgSwitchCase: 조건에 맞으면 DOM 트리에 엘리먼트를 추가하고, 조건에 맞지 않으면 DOM 트리에서 엘리먼트를 제거하는 구조 디렉티브이다.
  • NgSwitchDefault: NgSwitchCase가 어느 경우에도 해당되지 않을 때 DOM 트리에 엘리먼트를 추가하는 구조 디렉티브이다.


<div>와 같은 엘리먼트에 [ngSwitch]를 추가하고 이 바인딩 오른쪽에 표현식을 지정한다. 이번 예제에서는 feature를 지정했다. 이 예제에서 feature 값은 문자열이지만, NgSwitch 디렉티브는 타입을 가리지 않는다.


*ngSwitchCase*ngSwitchDefault를 이렇게 적용한다.


src/app/app.component.html
<div [ngSwitch]="currentItem.feature">
  <app-stout-item *ngSwitchCase="'stout'" [item]="currentItem"></app-stout-item>
  <app-device-item
    *ngSwitchCase="'slim'"
    [item]="currentItem"
  ></app-device-item>
  <app-lost-item *ngSwitchCase="'vintage'" [item]="currentItem"></app-lost-item>
  <app-best-item *ngSwitchCase="'bright'" [item]="currentItem"></app-best-item>
  <!-- . . . -->
  <app-unknown-item *ngSwitchDefault [item]="currentItem"></app-unknown-item>
</div>


[ngSwitch] 표현식에 연결할 프로퍼티로 부모 컴포넌트에 currentItem을 선언한다.


src/app/app.component.ts
currentItem!: Item;


자식 컴포넌트에 item 입력 프로퍼티를 선언한다. 이 프로퍼티에는 부모 컴포넌트에서 바인딩되는 currentItem이 할당된다. 아래 코드 2개를 보면서 부모 컴포넌트와 자식 컴포넌트가 어떻게 연결되는지 확인해 보자. StoutItemComponent의 클래스 코드를 확인해보면 이렇다.


In each child component, here StoutItemComponent
export class StoutItemComponent {
  @Input() item!: Item;
}


005


스위치 디렉티브는 표준 HTML 엘리먼트나 웹 컴포넌트를 대상으로도 잘 동작한다. <app-best-item>는 아래 코드처럼 <div>에 적용해도 된다.


src/app/app.component.html
<div *ngSwitchCase="'bright'">Are you as bright as {{currentItem.name}}?</div>

References