기본 Directive로 간단한 구조 만들기
2.5. 기본 Directive로 간단한 구조 만들기
첫 컴포넌트를 만들었습니다. 이제 이 컴포넌트에 더 멋진 기능들을 붙여봅시다. Angular는 컴포넌트와 함께 디렉티브(Directive)를 통해서 기능을 쉽게 사용할 수 있습니다. 지금부터 Angular가 기본으로 제공하는 몇가지 기본 디렉티브를 활용해 보도록 하겠습니다.
2.5.1 ngFor
ngFor 는 HTML 구조를 반복하여 나타낼 때 사용하는 디렉티브(Directive) 입니다. 먼저 앞서 만들던 피자목록 컴포넌트의 템플릿을 다음처럼 변경해봅시다.
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList">
<b>{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
</div>
앞서 마음이 불편하셨던 분들은 이제 좀 진정됨을 느끼시죠? 훨씬 적은 코드로 더 구조적인 표현을 하고 있습니다. 하나씩 소개해보죠. 먼저 \ngFor="let pizza of pizzaList" 라는 부분이 눈에 띄입니다. pizzaList 에서 피자를 하나씩 꺼내 pizza 라는 변수로 사용하고 있습니다. ngFor 를 적용한 DOM 엘리먼트 안쪽에서 이 pizza 를 활용해 pizza.name 과 pizza.price* 를 적당한 위치에 바인딩 합니다.
브라우져를 확인해 봅시다. 변화가 없습니다. 이 반복구조를 풀이하고 나면 이전에 적었던 구조와 동일하게 나타나게 됩니다. 익숙한 표현이라 생각합니다. 반복문 for문은 많은 프로그래밍 언어에서 기초적으로 사용되고 있죠. 이렇게 표현하고 나면 생기는 장점 한 가지를 간단하게 예를 들어보겠습니다. 이제 피자목록 컴포넌트의 피자목록에 새 피자를 하나 추가하게 되었습니다. 먼저 다음과 같이 새 피자를 목록에 포함할 것입니다.
<예제> src/app/pizza-list/pizza-list.component.ts
/* ... */
export class PizzaListComponent implements OnInit {
pizzaList:Pizza[] = [
new Pizza("치즈 피자", 13500),
new Pizza("페퍼로니 피자", 12500),
new Pizza("고구마 피자", 14000),
new Pizza("버섯 피자", 12000) /* 버섯 피자가 새로 추가되었습니다. */
];
/* ... */
}
브라우져에 벌써 새로 추가한 피자가 나타납니다. 이 컴포넌트의 템플릿은 수정하지 않아도 됩니다. 이는 우리가 만든 템플릿이 ngFor 와 함께 구조적인 표현으로 상황에 대응하고 있다는 것입니다. 이제 조금 더 자세히 알아보도록 합시다.
ngFor 디렉티브에 표현은 for-of 패턴을 표시하고 있습니다. <item> of <collection>
형태로 컬렉션에서 항목을 하나씩 꺼내 대응합니다. pizzaList 라는 컬렉션에서 하나씩 꺼내 pizza 에 대응하여 사용하고 있는 것이죠. ngFor 디렉티브는 몇 가지 편한 옵션들을 제공합니다. 이들은 지역 변수로 선언하여 활용할 수 있습니다.
- index : 현 항목의 순서입니다.
- first : 이 항목이 가장 첫 번째 항목일 때 true 값 입니다.
- last : 이 항목이 가장 마지막 항목일 때 true 값 입니다.
- even : 이 항목이 짝수 항목일 때 true 값 입니다.
- odd : 이 항목이 홀수 항목일 때 true 값 입니다.
이 중에 간단하게 index를 활용해 보도록 하겠습니다. 피자목록 컴포넌트의 템플릿을 다음과 같이 수정하였습니다.
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList; let i = index">
<b>{{i+1}}. {{pizza.name}}</b>
<i>{{pizza.price}}원</i>
</div>
\ngFor="let pizza of pizzaList; let i = index" 에 세미콜론으로 구분하여 뒤에 let i = index 라는 표현이 추가되었습니다. 이제 i 라는 지역변수로 index를 사용할 수 있습니다. 피자 이름 앞에 NaN* 을 덧붙여 보면 이제 순번이 표시됩니다. 참고로 index는 0부터 시작하는 순번입니다.
2.5.2 ngStyle
ngStyle 디렉티브는 DOM 엘리먼트에 스타일 요소를 적용합니다. 가장 간단한 사용방법은 [style.<property>]="<expression>"
형태 입니다. 예제로 확인해 보겠습니다.
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList">
<b [style.color]="'red'" [style.font-size.%]="120">{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
</div>
이제 피자 이름이 커지고 색깔도 입혀졌습니다. ngStyle 표현에서 value는 DOM 엘리먼트 속성의 값이 직접 사용되는 것이 아니라 템플릿이 갖는 문맥에서 해석되는 표현입니다. {{<expression>}}
표현에서 사용하는 것처럼 말이죠. 그래서 [style.color]="red" 가 아니라 [style.color]="'red'" 처럼 표현하고 있음을 확인해주세요. 이처럼 [<directive|attribute>]="<expression>"
표현으로 다양한 표현을 하게 됩니다. 따라서, 다음과 같은 사용도 가능합니다.
<예제> src/app/pizza-list/pizza-list.component.ts
/* ... */
export class PizzaListComponent implements OnInit {
/* 속성을 추가합니다. */
color: string = 'red';
fontSize: number = 120;
/* ... */
}
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList">
<b [style.color]="color" [style.font-size.%]="fontSize">{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
</div>
이제 스타일을 어떤 변경에 맞춰 제공할 때 color 속성과 fontSize 속성을 변경하게 될 것입니다. 앞으로 보게 될 많은 디렉티브나 속성을 Angular는 이런 방식으로 다룹니다.
사실 이 예제에는 좀 재밌는 표현이 함께 있습니다. style.font-size.% 같은 표현은 실제 CSS 문법과는 조금 달라 보입니다. Angular가 스타일요소에 대해 제공하는 편의입니다. 동등한 의미로 다음과 같은 표현도 가능합니다.
<b [style.font-size]="'120%'">{{pizza.name}}</b>
<!-- 또는 -->
<b [style.font-size]="fontSize + '%'">{{pizza.name}}</b>
<!-- 또는, * fontSizePercent: string = '120%'-->
<b [style.font-size]="fontSizePercent">{{pizza.name}}</b>
우리는 보통 font-size 같은 요소는 계산의 대상이 되곤 합니다. 계산은 숫자타입을 다루고 CSS 문법은 단위를 포함한 문자열로 표현됩니다. 단위에는 'px', 'em', '%' 등이 있습니다. 이런 과정에 제공되는 편의가 우리가 처음 본 표현입니다. 이제 숫자타입 속성을 직접 사용할 수 있으니 계산에 쓸 속성과 표현에 쓸 속성을 별도로 준비하거나 치환할 필요가 없겠군요.
이렇게 보니 적용할 스타일 사항이 늘어날 수록 좀 많은 코드가 나열될 것이 예상됩니다. 이때는 ngStyle 의 다른 표현을 사용하면 편리합니다.
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList">
<b [ngStyle]="{ color: color, 'font-size.%': fontSize }">{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
</div>
스타일 요소 하나하나를 적던 표현에 비해 간결한 표현입니다. [style.<property>]="<value>"
대신 [ngStyle]="<properties>"
형태로 한꺼번에 표현하는 것입니다. 여기에서 한 가지 짚어둘 것은 font-size 에 관한 표현이 color 와 다르게 font-size.% 가 아니라 인용구로 감싸인 'font-size.%' 라는 것입니다. 흡사 자바스크립트의 객체에서 특수문자를 포함한 키를 선언할 때 인용구로 감싸는 것과 유사한 표현입니다. 앞서 [style.font-size.%]="fontSize" 로 표현할 때는 딱히 인용구로 감싸지 않았었다는 것을 보면 일관적이지 않으니 유의하면서 사용합시다.
이런 스타일 요소에 대한 표현을 템플릿에 나열해두기 보다는 컨트롤러에서 관리하도록 변경해보겠습니다.
<예제> src/app/pizza-list/pizza-list.component.ts
/* ... */
export class PizzaListComponent implements OnInit {
/* 메소드을 추가합니다. */
getStyles() {
return {
color: this.color,
'font-size.%': this.fontSize
};
}
}
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList">
<b [ngStyle]="getStyles()">{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
</div>
지금까지 스타일을 적용하는 여러가지 방법을 보여드렸습니다. 다양한 방법만큼 상황에 따라 가장 편한 표현으로 사용할 수 있습니다.
2.5.3 ngClass
스타일 요소를 직접 다루는 것보다는 스타일에 관해서는 최대한 CSS가 다루도록 맡기는 편이 좋습니다. 도큐먼트와 스타일이 분리되어 관리될수록 관리하기 용이한 어플리케이션이 됩니다. 그런 측면에서 스타일 변화를 위해서 스타일 클래스를 다루는 일은 자주 일어납니다. 이번에는 ngClass 디렉티브에 대하여 알아보겠습니다.
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList; let isEven = even" [class.disabled]="isEven">
<b>{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
</div>
브라우져를 확인해보면 짝수 인덱스를 갖는 항목들이 disabled 클래스를 적용받은 모습을 볼 수 있습니다. [class.<className>]="<predicate>"
형태는 명제(predicate)가 참이면 해당 클래스를 적용하게 됩니다. 앞서 살펴본 것처럼 even 은 ngFor 에서 사용할 수 있는 특별한 옵션입니다. 이를 isEven 라는 변수를 통해 클래스 적용을 판단할 명제로 사용합니다. 이렇게 스타일 클래스를 적용하거나 하지 않음을 다룰 수 있습니다.
이번에는 스타일을 조금 작성하며 진행해 보겠습니다.
<예제> src/app/pizza-list/pizza-list.component.css
.red {
color: red;
}
.large {
font-size: 120%;
}
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList; let isEven = even">
<b [ngClass]="{ red: isEven, large: true }">{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
</div>
ngClass 도 [ngClass]="<classes|predicates>"
형태로 표현이 가능합니다. { red: isEven, large: true } 에서 객체의 키는 어떤 스타일 클래스를 판단할 것인지를 객체의 값에는 클래스 적용 여부를 판단할 명제를 표현합니다. 이 예제에서는 isEven 이 참일 경우에 red 클래스를 적용하고, large 클래스는 항상 적용합니다. 또, ngClass 는 명제를 이용해서 판단하는 방법 외에도 클래스 배열을 사용하는 방법이 있습니다.
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList">
<b [ngClass]="['red', 'large']">{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
</div>
이제는 모든 항목에 red 와 large 클래스가 적용되고 있음을 볼 수 있습니다. 배열에는 명제 없이 클래스 종류만을 담고 있으며 이들을 모두 적용합니다. 이제 이 부분은 다음과 같이 컨트롤러의 속성으로 위임될 수 있습니다.
<예제> src/app/pizza-list/pizza-list.component.ts
/* ... */
export class PizzaListComponent implements OnInit {
/* 속성을 추가합니다. */
classes = ['red', 'large'];
/* ... */
}
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList">
<b [ngClass]="classes">{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
</div>
템플릿 표현은 간결해졌고 컨트롤러에 온 해당 속성은 필요에 따라 수정될 수 있습니다. 간단히 장난을 하나 해봅시다.
<예제> src/app/pizza-list/pizza-list.component.ts
/* ... */
export class PizzaListComponent implements OnInit {
/* red를 잠시 빼 둡니다. */
classes = ['large'];
/* ... */
ngOnInit() {
/* 2초 후에 'red'가 추가됩니다. */
setTimeout(() => this.classes.push('red'), 2000);
}
}
ngOnInit 이나 setTimeout 에 대해서 궁금하실 수도 있으나 지금은 자세히는 다루지 않겠습니다. 특히 Angular.js 경험이 있는 분들은 $timeout 같은 내부 서비스가 아닌 setTimeout 을 직접 사용하는 것에 호기심을 느낄 수도 있습니다. Angular는 Angular.js에서 발전하여 변경감지에 대해 Zone.js를 사용하고 있으며 이 부분은 6. Change Detection에서 다루고 있습니다.
브라우져를 통해 결과를 확인해 봅시다. 화면이 나타난지 2초 후에 피자 이름이 붉은색으로 바뀌는 것을 볼 수 있습니다. 지금은 간단한 장난거리지만 이렇게 우리가 필요한 변경을 템플릿에 전달할 수 있습니다. 또한 large 처럼 변경에 여지가 없는 클래스는 기본 속성으로 두고 ngClass 디렉티브와 병행할 수 있습니다.
<예제> src/app/pizza-list/pizza-list.component.ts
/* ... */
export class PizzaListComponent implements OnInit {
/* 이제 빈 배열입니다. */
classes = [];
/* ... */
ngOnInit() {
/* 2초 후에 'red'가 추가됩니다. */
setTimeout(() => this.classes.push('red'), 2000);
}
}
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList">
<b class="large" [ngClass]="classes">{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
</div>
2.5.4 ngIf
이번에 새로 추가한 버섯 피자를 사람들이 많이 찾았으면 좋겠습니다. 그래서 신제품을 표시해 볼까합니다. 그러려면 피자가 신제품인지 알아야겠죠. 먼저 할 일은 피자 모델에 신제품 여부를 추가하는 것입니다. 그리고 이를 추가하고 나면 피자목록 컴포넌트에서 에러가 감지됩니다. 새로 생성하는 피자 객체의 추가 정보가 필요하다는 내용입니다. 모두 세번째 인자에 신제품인지 여부를 새로 적어줍시다.
<예제> src/app/pizza.ts
export class Pizza {
constructor(
public name:string,
public price:number,
public isNew:boolean /* 새 속성을 추가합니다. */
) { }
}
<예제> src/app/pizza-list/pizza-list.component.ts
/* ... */
export class PizzaListComponent implements OnInit {
pizzaList:Pizza[] = [
new Pizza("치즈 피자", 13500, false),
new Pizza("페퍼로니 피자", 12500, false),
new Pizza("고구마 피자", 14000, false),
new Pizza("버섯 피자", 12000, true) /* 버섯 피자는 신제품 입니다. */
];
/* ... */
}
이번에 소개 할 ngIf 는 조건에 따라 DOM 구조를 보이거나 감추려고 할 때 사용합니다. 신제품인 경우에만 새로 나온 피자라는 표시가 나오고 싶다면 사용하기 딱인 디렉티브입니다.
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList">
<b class="large" [ngClass]="classes">{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
<span *ngIf="pizza.isNew">새로 나온 피자</span> <!-- 이 줄을 추가합니다. -->
</div>
새롭게 추가된 속성을 이용하여 \ngIf="pizza.isNew" 라고 추가합니다. ```ngIf="
- Attribute 디렉티브 : 해당 엘리먼트의 모습이나 행위에 관여하는 디렉티브
- Structural 디렉티브 : DOM 엘리먼트를 더하거나 제거하여 구조 자체를 변경하게 되는 디렉티브
대괄호 표현은 Attribute 디렉티브에 사용하고 ** 표시는 Structural 디렉티브 앞에 붙습니다. Structural 디렉티브에는 대표적으로 ngFor 와 ngIf* 가 있습니다. 이런 문법은 코드를 작성하는 사람에게도 이 코드를 읽는 사람에게도 쉽게 추가적인 정보를 전달합니다. 바로 이 Structural 디렉티브들에 의해 DOM 구조가 변경될 수 있음을 알리는 것입니다. 컴포넌트 기반 개발에서는 DOM 구조가 변경되는 순간이 컴포넌트가 생성되거나 소멸되는 시점이 되곤 합니다. 따라서 이런 정보들은 컴포넌트를 다루는 과정에 도움이 됩니다. 자세한 내용은 7. 컴포넌트 라이프사이클 에서 다루고 있습니다.
2.5.5 ngSwitch
버섯 피자를 찾는 손님이 늘어서 좋긴 한데, 사실 고구마 피자가 효자 상품입니다. 인기 좋은 고구마 피자도 사람들에게 많이 선택받았으면 합니다. 이제 피자는 신제품인지 만이 아니라 인기있는 제품인지도 표시되어야 합니다. isNew 항목보다 좀 더 적합한 구조로 변경해봅시다.
<예제> src/app/pizza.ts
export class Pizza {
constructor(
public name:string,
public price:number,
public status:string /* 속성을 변경합니다. */
) { }
}
<예제> src/app/pizza-list/pizza-list.component.ts
/* ... */
export class PizzaListComponent implements OnInit {
pizzaList:Pizza[] = [
new Pizza("치즈 피자", 13500, null),
new Pizza("페퍼로니 피자", 12500, null),
new Pizza("고구마 피자", 14000, "popular"),
new Pizza("버섯 피자", 12000, "new") /* 버섯 피자는 신제품 입니다. */
];
/* ... */
}
이제 새로 적용한 status 항목을 사용해서 템플릿을 수정하도록 하겠습니다.
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList">
<b class="large" [ngClass]="classes">{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
<!-- 표시 내용들을 추가합니다. -->
<span *ngIf="pizza.status == 'new'">새로 나온 피자</span>
<span *ngIf="pizza.status == 'popular'">인기있는 피자</span>
</div>
이제 고구마 피자에도 인기있는 피자가 표시됩니다. 연속된 ngIf 는 모두 pizza.status 를 비교합니다. 이런 구조에 사용하는 익숙한 문법을 들어보자면 switch-case 문이 있습니다. Angular의 디렉티브에도 이 switch-case 문과 같은 디렉티브가 있습니다. 이를 활용하여 내용을 변경해보겠습니다.
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList" [ngSwitch]="pizza.status"> <!-- ngSwitch 디렉티브를 추가합니다. -->
<b class="large" [ngClass]="classes">{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
<!-- 표시 내용들을 수정합니다. -->
<span *ngSwitchCase="'new'">새로 나온 피자</span>
<span *ngSwitchCase="'popular'">인기있는 피자</span>
</div>
앞서 등장했던 Attribute 디렉티브와 Structural 디렉티브가 모두 사용되고 있는 모습이 인상적입니다. 변경이 예상되는, 즉 \ngSwitchCase 가 사용되는 DOM 엘리먼트가 있는 부모 엘리먼트에 [ngSwitch]="pizza.status" 를 추가합니다 ngSwitch 는 표현식을 비교 대상으로 둡니다. 그리고 그 자식 엘리먼트에서 ```ngSwitchCase="
<예제> src/app/pizza-list/pizza-list.component.html
<div *ngFor="let pizza of pizzaList" [ngSwitch]="pizza.status">
<b class="large" [ngClass]="classes">{{pizza.name}}</b>
<i>{{pizza.price}}원</i>
<span *ngSwitchCase="'new'">새로 나온 피자</span>
<span *ngSwitchCase="'popular'">인기있는 피자</span>
<span *ngSwitchDefault>맛있는 피자</span> <!-- 우리가게 피자는 모두 맛있으니까요. -->
</div>
사람의 욕심이란 덧없군요. 결국 모든 피자에 태그가 붙고 말았습니다. 그래도 이제 소외받는 피자는 없겠네요.
2.5.6 ngNonBindable
ngNonBindable 디렉티브는 안에 표현된 Angular 표현을 해석하지 않고 있는 그대로 둡니다. {{<expression>}}
같은 표현이 해석되어 치환되기를 바라지 않는다면 해당 위치에 ngNonBindable 디렉티브를 표시하면 됩니다.
<예제> ngNonBindable 예시
<div>
<p ngNonBindable> Angular는 {{value}}를 사용하여 데이터를 바인딩합니다.</p>
</div>