Angular 템플릿은 조건에 따라 엘리먼트를 표시하거나, 감추거나, 반복하는 흐름 제어 블록 기능을 제공합니다.
조건에 따라 표시하기: @if, @else-if, @else
@if 블록은 표현식 결과가 참으로 평가될 때 내용물을 표시합니다:
@if (a > b) {
<p>{{ a }} is greater than {{ b }}</p>
}
판단할 조건이 여러 개라면 @else if 블록이나 @else 블록을 자유롭게 활용하면 됩니다.
@if (a > b) {
{{ a }} is greater than {{ b }}
} @else if (b > a) {
{{ a }} is less than {{ b }}
} @else {
{{ a }} is equal to {{ b }}
}
표현식 계산 결과를 변수로 저장하기
@if 블록에서 조건문 판단에 사용한 데이터는 블록 안쪽에서 재사용할 수 있도록 변수로 할당해 둘 수 있습니다.
@if (user.profile.settings.startDate; as startDate) {
{{ startDate }}
}
조건문에 사용하는 표현식이 길다면 이 방식이 유용합니다.
내용 반복하기: @for
@for 블록은 콜렉션을 순회하면서 블록 안에 있는 내용물을 반복해서 렌더링합니다.
이때 콜렉션은 JavaScript 이터러블(iterable)이라면 어떤 것이라도 가능하며, Angular는 특히 Array를 다루는 성능에 최적화되어 있습니다.
@for 루프 블럭은 이렇게 사용합니다:
@for (item of items; track item.id) {
{{ item.name }}
}
Angular @for 블록은 JavaScript의 continue나 break 같이 흐름을 제어하는 구문은 지원하지 않습니다.
@for 블록을 사용할 때 왜 track을 지정해야 하나요?
track 표현식은 Angular가 데이터와 DOM 노드의 관계를 파악하는 용도로 사용합니다.
Angular가 이 관계를 파악할 수 있으면 데이터가 변경되었을 때 DOM 연산을 최소화하면서 성능을 최적화할 수 있습니다.
그래서 데이터 콜렉션을 반복할 때 애플리케이션의 렌더링 성능을 최적화하려면 track 구문을 효율적으로 작성해야 합니다.
track 표현식에는 개별 항목을 고유하게 구분할 수 있는 필드를 지정하세요.
보통은 id나 uuid와 같은 값을 사용할 것입니다.
고유하게 식별할 수 있는 필드가 없다면, 추가로 마련하는 것을 강력하게 권장합니다.
값이 변경되지 않는 정적 콜렉션이라면 인덱스 값을 표현하는 $index를 사용하는 것도 좋습니다.
값이 변경되는 것을 필드로 식별할 수 없다면 객체 자체를 활용할 수도 있습니다.
이 방식은 삼중 등호 연산자(===)를 기준으로 참조값이 같은지 판단합니다.
이 방식은 가능한 한 사용하지 않는 것이 좋습니다.
Angular가 데이터와 DOM 노드를 제대로 매칭할 수 없으면 렌더링 성능이 크게 저하될 수 있습니다.
@for (item of items; track item) {
{{ item.name }}
}
참고: *ngFor와 달리 @for 블록은 뷰 재사용을 우선합니다. 추적하는 프로퍼티가 바뀌어도 객체 참조가 같으면 엘리먼트 전체를 제거하고 다시 생성하는 대신 컴포넌트 입력값을 포함한 뷰 바인딩을 갱신합니다.
@for 블록 내부에서 사용할 수 있는 변수
@for 블록에서 사용할 수 있는 내부 변수가 몇 가지 있습니다:
| 변수 | 의미 |
|---|---|
$count |
콜렉션의 전체 개수 |
$index |
현재 반복되는 인덱스 값 |
$first |
현재 반복되는 항목이 첫번째 항목인지 여부 |
$last |
현재 반복되는 항목이 마지막 항목인지 여부 |
$even |
현재 행의 인덱스가 짝수인지 |
$odd |
현재 행의 인덱스가 홀수인지 |
이 변수들은 그대로 사용할 수 있지만, let 을 사용해서 다른 이름으로 사용할 수도 있습니다:
@for (item of items; track item.id; let idx = $index, e = $even) {
<p>Item #{{ idx }}: {{ item.name }}</p>
}
이렇게 내부 변수를 다른 이름으로 지정하는 방식은 @for 블록이 중첩되어 있을 때 특히 유용합니다.
콜렉션이 비어있을 때 - @empty 블록 사용하기
@for 블록 뒤에는 @empty 블록을 사용할 수 있습니다.
그러면 @for 블록에서 순회하는 콜렉션에 항목이 하나도 없을 때 @empty 블록이 화면에 표시됩니다:
@for (item of items; track item.name) {
<li>{{ item.name }}</li>
} @empty {
<li>There are no items.</li>
}
여러 조건 중 하나 표시하기: @switch 블록
@if 블록은 대부분의 경우에 사용할 수 있지만, 조건이 여러 개라면 @switch 블록을 사용하는 것도 고려할 만합니다.
@switch 블록은 JavaScript switch 구문과 아주 비슷합니다.
@switch (userPermissions) {
@case ('admin') {
<app-admin-dashboard />
}
@case ('reviewer')
@case ('editor') {
<app-editor-dashboard />
}
@default {
<app-viewer-dashboard />
}
}
이때 조건문은 삼중 등호 연산자(===)로 비교합니다.
@switch 블록은 다음 블록으로 연결되지 않습니다. break나 return 같이 흐름을 제어하는 구문은 존재하지 않습니다.
그리고 @case 구문은 블록 하나에 여러 조건을 함께 걸 수 있습니다.
@default 블록을 사용할 수 있습니다.
이 블록은 매칭되는 @case 블록이 없을 때 표시됩니다.
@default 블록이 없으면서 @case 블록 중 어느 것도 매칭되지 않으면, 아무것도 렌더링되지 않습니다.
모든 경우의 타입 검사하기
@switch는 모든 경우에 대한 타입 검사를 지원하므로, Angular는 컴파일 시점에 유니언 타입의 가능한 값을 모두 처리하는지 확인할 수 있습니다.
@default never;를 사용하면 처리하지 않은 경우가 남아 있지 않아야 한다고 명시적으로 선언합니다.
이후 유니언 타입이 확장되어 새로운 경우가 추가되었는데 @case로 처리하지 않으면 Angular 템플릿 타입 검사기가 오류를 보고하므로 누락한 분기를 조기에 발견할 수 있습니다.
참고: 모든 경우에 대한 검사는 TypeScript의 타입 좁히기를 사용하며, 이 기능은 변수에만 동작합니다.
switch 조건이 함수 호출이나 시그널(예: @switch (state()))이면 동작하지 않습니다.
이때는 @let mySignal = this.mySignal()처럼 시그널 값을 @let 변수에 할당해서 사용하세요.
@Component({
template: `
@switch (state) {
@case ('loggedOut') {
<button>Login</button>
}
@case ('loggedIn') {
<p>Welcome back!</p>
}
@default never;
<!-- @case ('loading')이 없으므로 에러가 발생합니다. -->
}
`,
})
export class AppComponent {
state: 'loggedOut' | 'loading' | 'loggedIn' = 'loggedOut';
}
switch 표현식이 유니언 타입 안에 중첩되면, 모든 경우를 확인할 표현식을 명시적으로 지정해야 합니다.
@Component({
template: `
@switch (state.mode) {
@case ('show') {
{{ state.menu }};
}
@case ('hide') {}
@default never(state);
}
`,
})
export class App {
state!: {mode: 'hide'} | {mode: 'show'; menu: number};
}