4. 목록 표시하기
이번 튜토리얼에서는 히어로의 목록을 화면에 표시하고, 이 중에서 히어로 하나를 선택해서 상세 정보를 표시하도록 히어로들의 여행 앱을 수정해 보자.
1. 히어로 목(mock) 생성하기
먼저, 히어로의 목록을 화면에 표시할 때 사용할 히어로 데이터가 필요하다.
최종적으로 리모트 데이터 서버에서 데이터를 받아올 것이다. 하지만 지금은 히어로 목을 생성하고 이 데이터들을 서버에서 받아온 것으로 간주하자.
src/app/
에 mock-heroes.ts
파일을 생성한다. 이 파일에 HEROES
배열을 상수로 선언하고 다른 파일에서 참조할 수 있도록 파일 외부로 공개할 것이다. 파일의 내용은 다음과 같이 작성한다.
import { Hero } from "./hero";
export const HEROES: Hero[] = [
{ id: 11, name: "Dr Nice" },
{ id: 12, name: "Narco" },
{ id: 13, name: "Bombasto" },
{ id: 14, name: "Celeritas" },
{ id: 15, name: "Magneta" },
{ id: 16, name: "RubberMan" },
{ id: 17, name: "Dynama" },
{ id: 18, name: "Dr IQ" },
{ id: 19, name: "Magma" },
{ id: 20, name: "Tornado" },
];
2. 히어로 표시하기
HeroesComponent
클래스 파일을 열고 HEROES
목 데이터를 로드한다.
그리고 클래스에 heroes
프로퍼티를 선언하고 위에서 로드한 HEROES
배열을 바인딩한다.
export class HeroesComponent implements OnInit {
heroes = HEROES;
}
1) *ngFor
로 히어로 목록 표시하기
HeroesComponent
템플릿 파일을 열고 다음과 같이 수정한다:
- 제일 위에
<h2>
를 추가한다. - 그 밑에 순서 없는 목록 HTML 태그(
<ul>
)를 추가한다. <ul>
태그 사이에<li>
를 추가해서hero
의 프로퍼티를 표시한다.- 스타일을 지정하기 위해 CSS 클래스를 추가한다.(CSS 스타일은 조금 뒤에 추가한다.)
그러면 다음과 같은 템플릿이 구성된다:
<h2>My Heroes</h2>
<ul class="heroes">
<li><span class="badge">{{hero.id}}</span> {{hero.name}}</li>
</ul>
하지만 위의 템플릿으로 구성하면 'hero' 속성이 존재하지 않기 때문에 오류가 표시된다. 각 개별 히어로에 액세스하고 모두 나열하려면 <li>
에 *ngFor
를 추가하여 히어로 목록을 반복한다:
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
*ngFor
는 항목을 반복하는 Angular 디렉티브이다. 이 디렉티브는 목록에 있는 항목마다 호스트 엘리먼트를 반복한다.
이 예제에서
- 호스트 엘리먼트는
<li>
이다. heroes
는HeroesComponent
클래스에 선언된 목록이다.hero
는 목록을 순회할 때마다 할당되는 히어로 객체이다.
Note
ngFor
앞에 별표(*
)가 붙는 것에 주의한다.
이제 브라우저가 갱신되면 히어로의 목록이 화면에 표시된다.
2) 스타일 꾸미기
히어로 목록은 보기 좋게 표시하는 것이 좋으며, 사용자가 어떤 항목에 마우스를 올리거나 선택하면 시각적인 반응을 보여주는 것도 좋다.
첫 번째 튜토리얼에서는 styles.css
파일에 애플리케이션 전역 스타일을 지정했다. 하지만 이 스타일시트에는 히어로의 목록을 꾸미는 스타일이 존재하지 않는다.
이 때 styles.css
에 더 많은 스타일을 추가할 수도 있지만, 이렇게 작성하면 컴포넌트를 추가할 때마다 스타일시티의 내용이 점점 많아진다.
이 방식보다는 컴포넌트와 관련된 파일-클래스 코드, HTML, CSS-을 한 곳에서 관리하면서 특정 컴포넌트에 해당하는 스타일만 따로 정의하는 것이 더 좋다.
이렇게 구현하면 컴포넌트를 재사용하기 편해지며 전역 스타일이 변경되더라도 컴포넌트 스타일에 영향을 주지 않는다.
컴포넌트에 적용되는 스타일은 @Component.styles
배열에서 인라인으로 정의할 수 있고, 여러 파일에 작성하고 @Component.styleUrls
배열로 지정할 수도 있다.
Angular CLI로 HeroesComponent
를 생성하면 이 컴포넌트에 스타일을 지정하는 heroes.component.css
파일을 자동으로 생성하고 @Component.styleUrls
목록에 추가한다.
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
그러면 heroes.component.css
파일을 열어서 HeroesComponent
에 적용되는 CSS 스타일을 작성할 수 있다. 지금은 이 코드를 생략한다. 이 파일의 내용은 이 문서의 아래쪽 최종코드 리뷰에서 확인할 수 있다.
Note
@Component
메타데이터에 지정된 스타일과 스타일시트 파일은 이 컴포넌트에만 적용된다.- 그래서
heroes.component.css
에 정의된 스타일은HeroesComponent
에만 적용되며 이 컴포넌트 밖에 있는 HTML이나 다른 컴포넌트에 영향을 주지 않는다.
3. 상세정보 표시하기
사용자가 목록에서 히어로를 클릭하면 이 히어로에 대한 상세정보가 상세정보 화면에 표시되어야 한다.
이번 섹션에서는 히어로 아이템이 클릭되는 이벤트를 감지하고, 클릭 이벤트가 발생했을 때 상세화면을 업데이트하는 방법을 알아보자.
1) 클릭 이벤트 바인딩하기
<li>
태그에 다음과 같이 클릭 이벤트를 바인딩한다.
위 코드는 Angular의 이벤트 바인딩 문법이다.
이렇게 이벤트를 바인딩하면 Angular가 <li>
엘리먼트에서 발생하는 click
이벤트를 감지할 수 있다. 그래서 사용자가 <li>
엘리먼트를 클릭하면 Angular는 onSelect(hero)
표현식을 실행한다.
다음 섹션에서는 HeroesComponent
에 onSelect()
메서드를 정의해서 *ngFor
로 반복한 히어로 엘리먼트 중에서 사용자가 클릭한 엘리먼트를 하이라이트 처리해보자.
2) 클릭 이벤트 핸들러 추가하기
컴포넌트의 hero
프로퍼티를 selectedHero
로 변경하지만 이 프로퍼티에 값을 직접 할당하지는 않는다. 왜냐하면 애플리케이션이 실행되는 시점에 선택된 히어로는 없기 때문이다.
그 다음에는 onSelect()
메서드를 추가한다. 이 메서드는 템플릿에서 선택된 히어로를 컴포넌트의 selectedHero
변수에 할당한다.
selectedHero?: Hero;
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
3) 상세화면 영역 추가하기
지금까지 만든 앱에서 컴포넌트 템플릿에는 히어로 목록이 표시된다. 이제는 이 목록에서 히어로 한 명을 클릭했을 때 해당 히어로의 상세정보를 표시하기 위해 상세정보에 해당하는 템플릿을 추가해야 한다. 다음 내용을 heroes.component.html
파일의 목록 아래에 추가한다:
<h2>{{selectedHero.name | uppercase}} Details</h2>
<div><span>id: </span>{{selectedHero.id}}</div>
<div>
<label for="hero-name">Hero name: </label>
<input id="hero-name" [(ngModel)]="selectedHero.name" placeholder="name" />
</div>
이제 브라우저가 갱신되면 애플리케이션이 더이상 동작하지 않는다.
이 때 브라우저의 개발자 도구를 열어서 콘솔창을 보면 다음과 같은 에러 메시지를 확인할 수 있다:
무슨 일이 일어난 걸까?
앱이 시작되고 나면 selectedHero
를 선언하면서 의도한 대로 selectedHero
값이 undefined
이다.
그래서 {{selectedHero.nam}}
와 같이 템플릿에서 selectedHero
의 프로퍼티를 참조하는 바인딩 표현식은 선택된 히어로가 존재하지 않기 때문에 동작하지 않는다.
수정하기 - 빈 화면은 *ngIf
로 감추기
컴포넌트는 selectedHero
프로퍼티의 값이 존재할 때만 선택된 히어로의 상세화면을 보여줘야 한다.
히어로의 상세정보를 표현하는 HTML을 <div>
로 감싼다. 그리고 Angular가 제공하는 *ngIf
디렉티브를 <div>
에 추가하고 이 디렉티브의 표현식으로 selectedHero
를 지정한다.
<div *ngIf="selectedHero">
<h2>{{selectedHero.name | uppercase}} Details</h2>
<div><span>id: </span>{{selectedHero.id}}</div>
<div>
<label for="hero-name">Hero name: </label>
<input id="hero-name" [(ngModel)]="selectedHero.name" placeholder="name" />
</div>
</div>
이제 브라우저가 갱신되고 나면 히어로의 목록이 다시 화면에 표시된다. 이 때 상세화면 영역은 비어있다. 목록에 있는 히어로 중 하나를 클릭해 보자. 앱이 다시 정상적으로 동작하는 것을 확인할 수 있다. 그리고 선택한 히어로의 상세정보가 히어로들의 목록을 표시하는 부분 아래에 표시되는 것을 확인할 수 있다.
어떻게 동작하는 것일까?
selectedHero
의 값이 undefined
이면 ngIf
는 히어로의 상세정보를 표현하는 부분을 DOM에서 제거한다. 그래서 selectedHero
를 바인딩하지 못하는 에러는 발생하지 않는다.
그리고 사용자가 히어로를 선택하면 selectedHero
의 값이 비어있지 않기 때문에 ngIf
가 히어로의 상세정보를 표현하는 부분을 DOM에 추가한다.
4) 선택된 항목 스타일 지정하기
이 스타일은 이전에 추가한 스타일에 있는 .selected
CSS 클래스가 적용된 것이다. 사용자가 선택한 항목에 이 클래스를 적용하려면 사용자가 클릭한 <li>
엘리먼트에 .selected
클래스를 적용하기만 하면 된다.
Angular가 제공하는 클래스 바인딩 문법을 사용하면 특정 조건에 따라 CSS 클래스를 추가하거나 제거할 수 있다. 스타일을 지정하려는 엘리먼트에 [class.some-css-class]="some-condition"
와 같은 문법을 추가하면 된다.
이 예제에서는 HeroesComponent
템플릿의 <li>
엘리먼트에 [class.selected]
와 같은 문법으로 클래스를 바인딩한다:
그러면 selectedHero
와 같은 히어로가 있는 줄에 selected
CSS 클래스가 추가된다. 그리고 컴포넌트 프로퍼티에 있는 값과 다르다면 이 클래스가 제거된다.
이렇게 수정된 <li>
코드는 다음과 같다.
<li
*ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)"
>
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
4. 최종코드 리뷰
그리고 이번 문서에서 다룬 파일의 내용은 다음과 같다.
import { Hero } from "./hero";
export const HEROES: Hero[] = [
{ id: 11, name: "Dr Nice" },
{ id: 12, name: "Narco" },
{ id: 13, name: "Bombasto" },
{ id: 14, name: "Celeritas" },
{ id: 15, name: "Magneta" },
{ id: 16, name: "RubberMan" },
{ id: 17, name: "Dynama" },
{ id: 18, name: "Dr IQ" },
{ id: 19, name: "Magma" },
{ id: 20, name: "Tornado" },
];
import { Component, OnInit } from "@angular/core";
import { Hero } from "../hero";
import { HEROES } from "../mock-heroes";
@Component({
selector: "app-heroes",
templateUrl: "./heroes.component.html",
styleUrls: ["./heroes.component.css"],
})
export class HeroesComponent implements OnInit {
heroes = HEROES;
selectedHero?: Hero;
constructor() {}
ngOnInit() {}
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
}
<h2>My Heroes</h2>
<ul class="heroes">
<li
*ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)"
>
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<div *ngIf="selectedHero">
<h2>{{selectedHero.name | uppercase}} Details</h2>
<div><span>id: </span>{{selectedHero.id}}</div>
<div>
<label for="hero-name">Hero name: </label>
<input id="hero-name" [(ngModel)]="selectedHero.name" placeholder="name" />
</div>
</div>
/* HeroesComponent's private CSS styles */
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #eee;
margin: 0.5em;
padding: 0.3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li:hover {
color: #2c3a41;
background-color: #e6e6e6;
left: 0.1em;
}
.heroes li.selected {
background-color: black;
color: white;
}
.heroes li.selected:hover {
background-color: #505050;
color: white;
}
.heroes li.selected:active {
background-color: black;
color: white;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #405061;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: 0.8em;
border-radius: 4px 0 0 4px;
}
input {
padding: 0.5rem;
}
5. 정리
이번 튜토리얼을 진행하면서 다음과 같은 내용에 대해 알아봤다.
- 히어로들의 여행 앱은 화면에 히어로의 목록을 표시한다.
- 사용자는 히어로를 한 명 선택할 수 있으며, 히어로를 선택하면 이 히어로의 상세정보를 확인할 수 있다.
- 목록을 표시할 때는
*ngFor
를 사용한다. - 특정 조건에 따라 DOM에 HTML 템플릿을 추가하거나 제거하려면
*ngIf
를 사용한다. class
바인딩을 사용하면 CSS 스타일 클래스를 적용하거나 적용하지 않을 수 있다.