Skip to content

4. 목록 표시하기


이번 튜토리얼에서는 히어로의 목록을 화면에 표시하고, 이 중에서 히어로 하나를 선택해서 상세 정보를 표시하도록 히어로들의 여행 앱을 수정해 보자.


Note


1. 히어로 목(mock) 생성하기

먼저, 히어로의 목록을 화면에 표시할 때 사용할 히어로 데이터가 필요하다.


최종적으로 리모트 데이터 서버에서 데이터를 받아올 것이다. 하지만 지금은 히어로 목을 생성하고 이 데이터들을 서버에서 받아온 것으로 간주하자.


src/app/mock-heroes.ts 파일을 생성한다. 이 파일에 HEROES 배열을 상수로 선언하고 다른 파일에서 참조할 수 있도록 파일 외부로 공개할 것이다. 파일의 내용은 다음과 같이 작성한다.


src/app/mock-heroes.ts
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 목 데이터를 로드한다.


src/app/heroes/heroes.component.ts (HEROES 로드)
import { HEROES } from "../mock-heroes";


그리고 클래스에 heroes 프로퍼티를 선언하고 위에서 로드한 HEROES 배열을 바인딩한다.


src/app/heroes/heroes.component.ts
export class HeroesComponent implements OnInit {
  heroes = HEROES;
}


1) *ngFor로 히어로 목록 표시하기

HeroesComponent 템플릿 파일을 열고 다음과 같이 수정한다:


  • 제일 위에 <h2>를 추가한다.
  • 그 밑에 순서 없는 목록 HTML 태그(<ul>)를 추가한다.
  • <ul> 태그 사이에 <li>를 추가해서 hero의 프로퍼티를 표시한다.
  • 스타일을 지정하기 위해 CSS 클래스를 추가한다.(CSS 스타일은 조금 뒤에 추가한다.)


그러면 다음과 같은 템플릿이 구성된다:


heroes.component.html (히어로 목록 템플릿)
<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>이다.
  • heroesHeroesComponent 클래스에 선언된 목록이다.
  • hero는 목록을 순회할 때마다 할당되는 히어로 객체이다.


Note

  • ngFor 앞에 별표(*)가 붙는 것에 주의한다.


이제 브라우저가 갱신되면 히어로의 목록이 화면에 표시된다.


2) 스타일 꾸미기

히어로 목록은 보기 좋게 표시하는 것이 좋으며, 사용자가 어떤 항목에 마우스를 올리거나 선택하면 시각적인 반응을 보여주는 것도 좋다.


첫 번째 튜토리얼에서는 styles.css 파일에 애플리케이션 전역 스타일을 지정했다. 하지만 이 스타일시트에는 히어로의 목록을 꾸미는 스타일이 존재하지 않는다.


이 때 styles.css에 더 많은 스타일을 추가할 수도 있지만, 이렇게 작성하면 컴포넌트를 추가할 때마다 스타일시티의 내용이 점점 많아진다.


이 방식보다는 컴포넌트와 관련된 파일-클래스 코드, HTML, CSS-을 한 곳에서 관리하면서 특정 컴포넌트에 해당하는 스타일만 따로 정의하는 것이 더 좋다.


이렇게 구현하면 컴포넌트를 재사용하기 편해지며 전역 스타일이 변경되더라도 컴포넌트 스타일에 영향을 주지 않는다.


컴포넌트에 적용되는 스타일은 @Component.styles 배열에서 인라인으로 정의할 수 있고, 여러 파일에 작성하고 @Component.styleUrls 배열로 지정할 수도 있다.


Angular CLI로 HeroesComponent를 생성하면 이 컴포넌트에 스타일을 지정하는 heroes.component.css 파일을 자동으로 생성하고 @Component.styleUrls 목록에 추가한다.


src/app/heroes/heroes.component.ts (@Component)
@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> 태그에 다음과 같이 클릭 이벤트를 바인딩한다.


heroes.component.html (템플릿 일부)
<li *ngFor="let hero of heroes" (click)="onSelect(hero)"></li>


위 코드는 Angular의 이벤트 바인딩 문법이다.


이렇게 이벤트를 바인딩하면 Angular가 <li> 엘리먼트에서 발생하는 click 이벤트를 감지할 수 있다. 그래서 사용자가 <li> 엘리먼트를 클릭하면 Angular는 onSelect(hero) 표현식을 실행한다.


다음 섹션에서는 HeroesComponentonSelect() 메서드를 정의해서 *ngFor로 반복한 히어로 엘리먼트 중에서 사용자가 클릭한 엘리먼트를 하이라이트 처리해보자.


2) 클릭 이벤트 핸들러 추가하기

컴포넌트의 hero 프로퍼티를 selectedHero로 변경하지만 이 프로퍼티에 값을 직접 할당하지는 않는다. 왜냐하면 애플리케이션이 실행되는 시점에 선택된 히어로는 없기 때문이다.


그 다음에는 onSelect() 메서드를 추가한다. 이 메서드는 템플릿에서 선택된 히어로를 컴포넌트의 selectedHero 변수에 할당한다.


src/app/heroes/heroes.component.ts (onSelect)
selectedHero?: Hero;
onSelect(hero: Hero): void {
  this.selectedHero = hero;
}


3) 상세화면 영역 추가하기

지금까지 만든 앱에서 컴포넌트 템플릿에는 히어로 목록이 표시된다. 이제는 이 목록에서 히어로 한 명을 클릭했을 때 해당 히어로의 상세정보를 표시하기 위해 상세정보에 해당하는 템플릿을 추가해야 한다. 다음 내용을 heroes.component.html 파일의 목록 아래에 추가한다:


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>


이제 브라우저가 갱신되면 애플리케이션이 더이상 동작하지 않는다.


이 때 브라우저의 개발자 도구를 열어서 콘솔창을 보면 다음과 같은 에러 메시지를 확인할 수 있다:


HeroesComponent.html:3 ERROR TypeError: Cannot read property 'name' of undefined


무슨 일이 일어난 걸까?

앱이 시작되고 나면 selectedHero를 선언하면서 의도한 대로 selectedHero 값이 undefined이다.


그래서 {{selectedHero.nam}}와 같이 템플릿에서 selectedHero의 프로퍼티를 참조하는 바인딩 표현식은 선택된 히어로가 존재하지 않기 때문에 동작하지 않는다.


수정하기 - 빈 화면은 *ngIf로 감추기

컴포넌트는 selectedHero 프로퍼티의 값이 존재할 때만 선택된 히어로의 상세화면을 보여줘야 한다.


히어로의 상세정보를 표현하는 HTML을 <div>로 감싼다. 그리고 Angular가 제공하는 *ngIf 디렉티브를 <div>에 추가하고 이 디렉티브의 표현식으로 selectedHero를 지정한다.


src/app/heroes/heroes.component.html (*ngIf)
<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) 선택된 항목 스타일 지정하기

001


이 스타일은 이전에 추가한 스타일에 있는 .selected CSS 클래스가 적용된 것이다. 사용자가 선택한 항목에 이 클래스를 적용하려면 사용자가 클릭한 <li> 엘리먼트에 .selected 클래스를 적용하기만 하면 된다.


Angular가 제공하는 클래스 바인딩 문법을 사용하면 특정 조건에 따라 CSS 클래스를 추가하거나 제거할 수 있다. 스타일을 지정하려는 엘리먼트에 [class.some-css-class]="some-condition"와 같은 문법을 추가하면 된다.


이 예제에서는 HeroesComponent 템플릿의 <li> 엘리먼트에 [class.selected]와 같은 문법으로 클래스를 바인딩한다:


heroes.component.html (selected CSS 클래스 토글하기)
[class.selected]="hero === selectedHero"


그러면 selectedHero와 같은 히어로가 있는 줄에 selected CSS 클래스가 추가된다. 그리고 컴포넌트 프로퍼티에 있는 값과 다르다면 이 클래스가 제거된다.


이렇게 수정된 <li> 코드는 다음과 같다.


heroes.component.html (히어로 목록)
<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 스타일 클래스를 적용하거나 적용하지 않을 수 있다.

References