Angular 튜터리얼 - 라우팅

Routing

Tour of Heroes 앱에 대한 새로운 요구 사항이 있습니다.

  • Dashboard View를 추가합니다.
  • Heroes와 대시보드 보기 사이를 Navigation할 수있는 기능을 추가합니다.
  • 사용자가 Hero 이름을 클릭하면 선택한 Hero의 상세보기로 이동합니다.
  • 사용자가 이메일에서 딥 링크를 클릭하면 특정 Hero의 상세 보기를 엽니다.

완료되면 사용자는 다음과 같이 앱을 Navigation할 수 있습니다.

요구 사항을 충족 시키기 위해 Angular의 Router를 앱에 추가합니다.

Router에 대한 자세한 내용은 Routing and Navigation 페이지를 참조하십시오.

이 페이지를 끝내면 앱은 이 라이브 예제/예제 다운로드 처럼 보일 것입니다.

어디까지 했었나?

Tour of Heroes를 계속하기 전에 다음과 같은 구조인지 확인하십시오.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
angular-tour-of-heroes
┣ src
┃ ┣ app
┃ ┃ ┣ app.component.ts
┃ ┃ ┣ app.module.ts
┃ ┃ ┣ hero.ts
┃ ┃ ┣ hero-detail.component.ts
┃ ┃ ┣ hero.service.ts
┃ ┃ ┗ mock-heroes.ts
┃ ┣ main.ts
┃ ┣ index.html
┃ ┣ styles.css
┃ ┣ systemjs.config.js
┃ ┗ tsconfig.json
┣ node_modules ...
┗ package.json

앱 실행과 트랜스파일을 유지하기

터미널 창에서 다음 명령을 입력하십시오.

1
npm start

이 명령은 TypeScript 컴파일러를 “watch mode”에서 실행하여 코드가 변경되면 자동으로 다시 컴파일 하도록합니다. 또한 이 명령은 앱이 브라우저에서 동시에 실행되고 코드가 변경되면 브라우저를 새로 고칩니다.

다시 컴파일하거나 새로 고치지 않고 브라우저를 일시 정지하지 않아 Tour of Heroes 앱을 계속 만들 수 있습니다.

Action plan

계획은 다음과 같습니다.

  • AppComponent를 Navigation만 처리하는 어플리케이션으로 변경합니다.
  • 현재 AppComponent 내의 Hero들 관심사를 별도의 HeroesComponent로 재배치합니다.
  • Router를 추가합니다.
  • 새로운 DashboardComponent를 생성합니다.
  • 대시보드를 Navigation 구조에 연결합니다.

Routing은 Navigation의 또 다른 이름입니다. Router는 View 사이의 Navigation을 위한 메커니즘입니다.

AppComponent 분할

현재 앱은 AppComponent를 로드하고 즉시 Hero들의 목록을 표시합니다.

수정된 앱은 뷰(Dashboard 및 Heroes)중 하나를 선택하여 표시하고 그중 하나를 기본값으로 설정합니다.

AppComponent는 Navigation만 처리 해야하므로, AppComponent에서 Hero들의 디스플레이를 자신의 HeroesComponent로 옮깁니다.

HeroesComponent

AppComponent는 이미 Hero들 전용입니다. AppComponent에서 코드를 이동하는 대신 HeroesComponent로 이름을 바꾸고 별도의 AppComponent을 만듭니다.

다음을 실행합니다.

  • app.component.ts 파일의 이름을 heroes.component.ts로 변경합니다.
  • AppComponent 클래스의 이름을 HeroesComponent로 변경합니다 (이 파일만).
  • Selector my-app의 이름을 my-heroes로 변경합니다.

src/app/heroes.component.ts (showing renamings only)

1
2
3
4
5
@Component({
selector: 'my-heroes',
})
export class HeroesComponent implements OnInit {
}

AppComponent 생성

새로운 AppComponent는 응용 프로그램 Shell입니다. 상단에는 Navigation 링크가 있고 아래에는 표시 영역이 있습니다.

아래 단계를 수행합니다.

  • src/app/app.component.ts 파일을 생성합니다.
  • exportAppComponent 클래스를 정의합니다.
  • my-app Selector를 사용하여 클래스 위에 @Component Decorator를 추가합니다.
  • HeroesComponent에서 AppComponent로 다음을 이동합니다.
    • title 클래스 프로퍼티
    • @Component 템플릿 <h1> 엘리먼트는 title의 바인딩을 포함합니다.
  • 제목 바로 아래의 앱 템플릿에 <my-heroes> 엘리먼트를 추가하면 Hero가 계속 표시됩니다.
  • HeroesComponentAppModuledeclarations 배열에 추가하여 Angular가 <my-heroes> 태그를 인식하도록합니다.
  • 다른 모든 View에서 HeroService가 필요하기 때문에 HeroServiceAppModuleproviders 배열에 추가합니다.
  • HeroesComponentproviders 배열에서 HeroService를 제거합니다.
  • AppComponent에 대한 import 문을 추가합니다.

첫 번째 초안은 다음과 같습니다.

src/app/app.component.ts (v1)

1
2
3
4
5
6
7
8
9
10
11
12
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<my-heroes></my-heroes>
`
})
export class AppComponent {
title = 'Tour of Heroes';
}

src/app/app.module.ts (v1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroesComponent } from './heroes.component';
import { HeroService } from './hero.service';
@NgModule({
imports: [
BrowserModule,
FormsModule
],
declarations: [
AppComponent,
HeroDetailComponent,
HeroesComponent
],
providers: [
HeroService
],
bootstrap: [ AppComponent ]
})
export class AppModule {
}

앱은 계속 실행되고 Hero를 표시할 것입니다.

라우팅 추가하기

자동으로 표시하는 대신 사용자가 버튼을 클릭하면 Hero가 표시됩니다. 즉, 사용자는 Hero들의 목록을 Navigate할 수 있어야합니다.

Angular Router를 사용하여 Navigation을 활성화합니다.

Angular RouterRouterModule이라고 하는 외부의 선택적 Angular NgModule입니다. Router는 여러개의 제공된 Service(RouterModule), 여러개의 Directive(RouterOutlet, RouterLink, RouterLinkActive) 및 Component(Routes)의 조합입니다. 먼저 라우트를 설정합니다.

<base href>

index.html을 열고 <head href = "... "> 엘리먼트 (또는 이 엘리먼트를 동적으로 설정하는 스크립트)가 <head>섹션 맨 위에 있는지 확인합니다.

src/index.html (base-href)

1
2
<head>
<base href="/">

base href는 필수입니다.
자세한 내용은 Routing and Navigation 페이지의 base href 설정 섹션을 참조하십시오.

라우트 설정

앱 라우트에 대한 설정 파일을 만듭니다.

Routes는 사용자가 링크를 클릭하거나, URL을 브라우저 주소 표시 줄에 붙여 넣을 때, 표시할 View를 라우터에 알립니다.

Hero들 Component에 대한 라우트를 첫 번째 라우트로 정의합니다.

src/app/app.module.ts (heroes route)

1
2
3
4
5
6
7
8
import { RouterModule } from '@angular/router';
RouterModule.forRoot([
{
path: 'heroes',
component: HeroesComponent
}
])

Routes는 라우트 정의 배열입니다.

이 라우트 정의에는 다음과 같은 부분이 포함됩니다.

  • Path : Router가 이 라우트의 경로를 브라우저 주소 표시 줄의 URL과 일치시킵니다.(heroes)
  • Component :이 라우트로 이동할 때 Router가 만들어야하는 Component입니다. (HeroesComponent)

Routing & Navigation 페이지에서 Routes를 사용하여 라우트를 정의하는 방법에 대해 자세히 알아보십시오.

사용가능한 라우터 만들기

RouterModuleimport해서 AppModuleimports 배열에 추가합니다.

src/app/app.module.ts (app routing)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroesComponent } from './heroes.component';
import { HeroService } from './hero.service';
@NgModule({
imports: [
BrowserModule,
FormsModule,
RouterModule.forRoot([
{
path: 'heroes',
component: HeroesComponent
}
])
],
declarations: [
AppComponent,
HeroDetailComponent,
HeroesComponent
],
providers: [
HeroService
],
bootstrap: [ AppComponent ]
})
export class AppModule {
}

설정된 Router가 앱의 루트에 제공되기 때문에 forRoot() 메서드를 호출합니다. forRoot() 메서드는 라우팅에 필요한 Router Service providersDirective를 제공하고 현재 브라우저 URL을 기반으로 초기 Navigation을 수행합니다.

라우터 아울렛

/heroes Path를 URL의 끝에있는 브라우저 주소창에 추가하면 Router는 그것을 heroes Path와 일치시키고 HeroesComponent를 표시합니다. 하지만 RouterComponent를 표시할 위치를 알려줘야합니다. 이렇게 하기 위해 템플릿 끝에 <router-outlet> 엘리먼트를 추가 합니다. RouterOutletRouterModule이 제공하는 Directive 중 하나입니다. Router는 사용자가 앱을 Navigate할 때 <router-outlet>바로 아래에 각 Component를 표시합니다.

라우터 링크들

사용자는 주소 표시 줄에 라우트 URL을 붙여 넣을 필요가 없습니다. 대신 템플릿에 앵커 태그를 추가하여 클릭하면 HeroesComponent에 대한 Navigation이 시작됩니다.

수정된 템플릿은 다음과 같습니다.

src/app/app.component.ts (template-v2)

1
2
3
4
5
template: `
<h1>{{title}}</h1>
<a routerLink="/heroes">Heroes</a>
<router-outlet></router-outlet>
`

앵커 태그의 routerLink 바인딩에 주목하세요. RouterLink Directive (RouterModule의 다른 Directive)는 사용자가 링크를 클릭할 때 Navigate할 위치를 Router에 알려주는 문자열이 바인딩됩니다.

링크가 동적이 아니기 때문에, 라우팅 명령은 라우트 Path에 대한 1회 바인딩으로 정의됩니다. 라우트 설정을 살펴보면 /heroesHeroesComponent에 대한 라우트 Path임을 확인할 수 있습니다.

Routing & Navigation 페이지의 Appendix: Link Parameters Array 섹션에서 동적 라우터 링크 및 링크 파라미터 배열에 대해 자세히 읽어보십시오.

브라우저를 새로 고칩니다. 브라우저에는 앱 제목과 Hero 링크가 표시되지만 Hero들 목록은 표시되지 않습니다.

브라우저의 주소 표시 줄에 /가 표시됩니다. HeroesComponent 라우트 Path는 /가 아닌 /heroes입니다. 곧 라우트 /와 일치하는 Path를 추가합니다.

Heroes Navigation 링크를 클릭하십시오. 주소 표시 줄이 /heroes로 업데이트되고 Heroes 목록이 표시됩니다.

AppComponent는 이제 다음과 같습니다.

src/app/app.component.ts (v2)

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<a routerLink="/heroes">Heroes</a>
<router-outlet></router-outlet>
`
})
export class AppComponent {
title = 'Tour of Heroes';
}

이제 AppComponentRouter에 연결되고 라우트된 View를 표시합니다. 이러한 이유로 다른 Component와 구별하기 위해 이 Component 타입을 Router component라고합니다.

Dashboard 추가

라우팅은 여러 뷰가 있을 때만 의미가 있습니다. 다른 View를 추가하기 위해 사용자가 Navigate할 수있는 Placeholder DashboardComponent를 만듭니다.

src/app/dashboard.component.ts (v1)

1
2
3
4
5
6
7
import { Component } from '@angular/core';
@Component({
selector: 'my-dashboard',
template: '<h3>My Dashboard</h3>'
})
export class DashboardComponent { }

나중에 이 Component를 보다 유용하게 만들것입니다.

Dashboard 라우트 설정

app.module.ts를 수정하여 대시 보드로 Navigate 하려면 대시 보드 Component를 가져 와서 다음을 Routes 배열의 정의에 추가합니다.

src/app/app.module.ts (Dashboard route)

1
2
3
4
{
path: 'dashboard',
component: DashboardComponent
},

또한 DashboardComponentimport하고 AppModuledeclarations에 추가합니다.

src/app/app.module.ts (dashboard)

1
2
3
4
5
6
declarations: [
AppComponent,
DashboardComponent,
HeroDetailComponent,
HeroesComponent
],

Redirect 라우트 추가

현재 브라우저는 주소 표시 줄에 /로 시작합니다. 앱이 시작되면 대시 보드가 표시되고 브라우저 주소 표시 줄에 /dashboard URL이 표시됩니다.

이를 가능하게 하기위해 redirect 라우트를 사용합니다. 라우트 정의 배열에 다음을 추가합니다.

src/app/app.module.ts (redirect)

1
2
3
4
5
{
path: '',
redirectTo: '/dashboard',
pathMatch: 'full'
},

Routing & Navigation 페이지의 Redirecting routes 섹션에서 redirect에 대해 자세히 알아보십시오.

Template으로 Navigation 추가

Heroes 링크 바로 위의 템플릿에 대시 보드 Navigation 링크를 추가합니다.

src/app/app.component.ts (template-v3)

1
2
3
4
5
6
7
8
template: `
<h1>{{title}}</h1>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
`

<nav> 태그는 아직 아무 것도하지 않지만 나중에 링크 Style을 지정하면 유용합니다.

브라우저에서 응용 프로그램 루트(/)로 가서 새로고침 합니다. 응용 프로그램은 대시 보드를 표시하고 Dashboard와 Heroes 사이를 Navigation할 수 있습니다.

Hero들을 Dashboard에 추가하기

대시 보드를 보다 재미있게 만들기 위해 상위 4명의 Heroes을 한눈에 볼 수 있게 수정합니다.

template 메타 데이터를 새로운 템플릿 파일을 가리키는 templateUrl 프로퍼티로 변경합니다.

src/app/dashboard.component.ts (metadata)

1
2
3
4
@Component({
selector: 'my-dashboard',
templateUrl: './dashboard.component.html',
})

다음 내용으로 파일을 만듭니다.

src/app/dashboard.component.html

1
2
3
4
5
6
7
8
<h3>Top Heroes</h3>
<div class="grid grid-pad">
<div *ngFor="let hero of heroes" class="col-1-4">
<div class="module hero">
<h4>{{hero.name}}</h4>
</div>
</div>
</div>

*ngFor는 Heroes 목록을 반복하고 이름을 표시하는데 다시 사용됩니다. 여분의 <div> 엘리먼트는 나중에 Style을 지정하는데 도움이 됩니다.

HeroService 공유하기

HeroService를 다시 사용하여 Componentheroes 배열을 채울수 있습니다.

이전에 HeroesComponentproviders 배열에서 HeroService를 삭제하고 AppModuleproviders 배열에 추가했습니다. 이 변경으로 앱의 모든 Component에서 사용할 수 있는 싱글톤 HeroService 인스턴스가 생성되었습니다. HeroService를 Angular에서 Injects하기 때문에 DashboardComponent에서 사용할 수 있습니다.

Get heroes

dashboard.component.ts에 다음 import 문을 추가합니다.

src/app/dashboard.component.ts (imports)

1
2
3
4
import { Component, OnInit } from '@angular/core';
import { Hero } from './hero';
import { HeroService } from './hero.service';

다음과 같이 DashboardComponent 클래스를 만듭니다.

src/app/dashboard.component.ts (class)

1
2
3
4
5
6
7
8
9
10
11
export class DashboardComponent implements OnInit {
heroes: Hero[] = [];
constructor(private heroService: HeroService) { }
ngOnInit(): void {
this.heroService.getHeroes()
.then(heroes => this.heroes = heroes.slice(1, 5));
}
}

이 로직은 HeroesComponent에서도 사용됩니다.

  • heroes 배열 프로퍼티를 정의합니다.
  • 생성자에서 HeroService를 주입받고 private heroService 필드에 할당합니다.
  • Angular ngOnInit() 라이프 사이클 후크에서 Service를 호출하여 Heroes을 얻습니다.

이 Dashboard에서는 Array.slice 메서드로 4명의 Hero들(두 번째, 세 번째, 네 번째 및 다섯 번째)를 지정합니다.

브라우저를 새로 고침하면 새로운 대시 보드에 4명의 Hero 이름이 표시됩니다.

Hero 상세보기로 Navigate 하기

선택된 Hero의 상세 보기가 HeroesComponent의 하단에 표시되지만, 사용자는 다음과 같은 추가 방법으로 HeroDetailComponent를 Navigate할 수 있어야합니다.

  • 대시 보드부터 선택한 Hero까지 Navigate.
  • Heroes 목록부터 선택한 Hero까지 Navigate.
  • 브라우저 주소창에 붙여 넣은 “딥 링크” URL부터 Navigate.

Hero 상세보기로 라우팅 하기

다른 라우트가 설정된 app.module.tsHeroDetailComponent에 라우트를 추가할 수 있습니다.

새로운 라우트는 어떤 Hero를 보여줘야 하는지 HeroDetailComponent에게 알려줘야 한다는 점에서 특이합니다. HeroesComponentDashboardComponent에는 아무것도 알려줄 필요가 없었습니다.

현재 부모 HeroesComponent는 다음과 같은 바인딩을 사용하여 Componenthero 프로퍼티를 Hero 객체로 설정합니다.

1
<hero-detail [hero]="selectedHero"></hero-detail>

그러나 이 바인딩은 어떠한 라우팅 시나리오에서도 작동하지 않습니다.

Parameterized route

Hero의 아이디를 URL에 추가할 수 있습니다. id가 11인 Hero에게 라우팅할 때 다음과 같은 URL을 사용할 수 있습니다.

1
/detail/11

URL의 /detail/ 부분은 상수입니다. 후행 숫자 id는 Hero에서 Hero로 바뀝니다. Hero의 id를 나타내는 파라미터 (또는 토큰)로 라우트의 변수 부분을 나타낼 필요가 있습니다.

파라미터를 이용한 라우트 설정

다음 라우트 정의를 사용합니다.

src/app/app.module.ts (hero detail)

1
2
3
4
{
path: 'detail/:id',
component: HeroDetailComponent
},

Path의 콜론(:)은 HeroDetailComponent로 Navigating할 때 :id가 특정 Hero id의 값임을 나타냅니다.

이 라우트를 생성하기 전에 HeroDetailComponent를 import 해야합니다.

앱 라우트가 완성되었습니다.

사용자가 특정 Hero을보기 위해 Navigation 링크를 클릭하지 않기 때문에 템플릿에 'Hero Detail'링크를 추가하지 않았습니다. 이름이 Dashboard에 표시되는지 또는 Hero들 목록에 표시되는지 여부와 관계없이 Hero 이름을 클릭합니다.

HeroDetailComponent가 수정되어 Navigate할 준비가될 때까지는 Hero 클릭을 추가할 필요가 없습니다.

HeroDetailComponent 변경

다음은 HeroDetailComponent의 현재 모습입니다.

src/app/hero-detail.component.ts (current)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Component, Input } from '@angular/core';
import { Hero } from './hero';
@Component({
selector: 'hero-detail',
template: `
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div>
<label>id: </label>{{hero.id}}
</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
</div>
`
})
export class HeroDetailComponent {
@Input() hero: Hero;
}

템플릿은 변경되지 않습니다. Hero 이름도 같은 방식으로 표시됩니다. 주요 변경 사항은 Hero 이름을 얻는 방법에 의해 결정됩니다.

부모 Component 프로퍼티 바인딩에서 Hero을 더이상 받지 않게됩니다. 새로운 HeroDetailComponentActivatedRoute Service의 Observable paramMap에서 id 파라미터를 가져오고 HeroService를 사용하여 해당 id의 Hero을 가져와야합니다.

다음 import를 추가하십시오.

src/app/hero-detail.component.ts

1
2
3
4
5
6
// 지금은 Input import를 유지하지만 나중에는 제거합니다.
import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Location } from '@angular/common';
import { HeroService } from './hero.service';

ActivatedRoute, HeroServiceLocation Service를 생성자에서 주입받아 private 필드에 할당합니다.

src/app/hero-detail.component.ts (constructor)

1
2
3
4
5
constructor(
private heroService: HeroService,
private route: ActivatedRoute,
private location: Location
) {}

switchMap 연산자를 가져 와서 나중에 Observable 매개 변수로 사용합니다.

src/app/hero-detail.component.ts (switchMap import)

1
import 'rxjs/add/operator/switchMap';

클래스에서 OnInit 인터페이스를 구현합니다.

src/app/hero-detail.component.ts

1
export class HeroDetailComponent implements OnInit {

ngOnInit() 라이프 사이클 훅 내에서 paramMap Observable을 사용하여 ActivatedRoute 서비스에서 id 파라미터 값을 추출하고 HeroService를 사용하여 그 id를 가진 영웅을 가져옵니다.

src/app/hero-detail.component.ts

1
2
3
4
5
ngOnInit(): void {
this.route.paramMap
.switchMap((params: ParamMap) => this.heroService.getHero(+params.get('id')))
.subscribe(hero => this.hero = hero);
}

switchMap 연산자는 Observable 라우트 파라미터의idHeroService.getHero()메소드의 결과인 새로운 Observable에 매핑합니다.

getHero 요청이 처리되는 동안 사용자가 이 Component를 다시 탐색하면 switchMap은 이전 요청을 취소한 다음 HeroService.getHero()를 다시 호출합니다.

Hero id는 숫자입니다. 라우트 파라미터는 항상 문자열입니다. 따라서 라우트 파라미터는 JavaScript(+) 연산자를 사용하여 숫자로 변환됩니다.

구독 취소(unsubscribe)가 필요합니까?
Routing & Navigation 페이지의 라우트 정보에 대한 one-stop-shop인 ActivatedRoute에서 설명한대로 Router는 제공하는 Observable 정보를 관리하고 구독을 Localizes합니다. 구독은 Component가 소멸될 때 정리되어 메모리 누수를 방지하므로 paramMap Observable 라우트에서 구독을 취소할 필요가 없습니다.

HeroService.getHero() 추가

이전 코드에서 HeroService에는 getHero() 메서드가 없습니다. 이 문제를 해결하려면 HeroService를 열고 getHeroes()에서 Heroes 목록을 id로 필터링하는 getHero() 메서드를 추가합니다.

src/app/hero.service.ts (getHero)

1
2
3
4
getHero(id: number): Promise<Hero> {
return this.getHeroes()
.then(heroes => heroes.find(hero => hero.id === id));
}

뒤로가기 방법 추가

사용자는 여러 가지 방법으로 HeroDetailComponent로 Navigate할 수 있습니다.

다른 곳을 Navigate하기 위해, 사용자는 AppComponent의 두 링크 중 하나를 클릭하거나 브라우저의 뒤로 버튼을 클릭할 수 있습니다. 이제 이전에 삽입 한 Location Service를 사용하여 브라우저 히스토리 스택의 한 단계 뒤로 탐색하는goBack() 메서드를 추가합니다.

src/app/hero-detail.component.ts (goBack)

1
2
3
goBack(): void {
this.location.back();
}

너무 멀리 되돌아 가면 사용자가 앱에서 빠져 나올 수 있습니다. 실제 앱에서는 CanDeactivate 가드로 이 문제를 방지할 수 있습니다. CanDeactivate 페이지에서 자세한 내용을 읽어보십시오.

이 메소드를 Component 템플릿에 추가할 Back 버튼에 대한 이벤트 바인딩으로 연결합니다.

1
<button (click)="goBack()">Back</button>

템플릿을 hero-detail.component.html 파일로 옮깁니다.

src/app/hero-detail.component.html

1
2
3
4
5
6
7
8
9
10
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div>
<label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name" />
</div>
<button (click)="goBack()">Back</button>
</div>

방금 만든 템플릿 파일을 가리키는 templateUrl을 사용하여 Component 메타 데이터를 업데이트합니다.

src/app/hero-detail.component.ts (metadata)

1
2
3
4
@Component({
selector: 'hero-detail',
templateUrl: './hero-detail.component.html',
})

브라우저를 새로 고치고 결과를 확인합니다.

Dashboard Hero 선택

사용자가 대시 보드에서 Hero을 선택하면 앱이 HeroDetailComponent로 이동하여 선택한 Hero을 보고 편집해야합니다.

대시 보드 Hero는 버튼과 같은 블록으로 표시되지만 앵커 태그처럼 행동해야합니다. Hero 블록 위로 마우스를 가져 가면 대상 URL이 브라우저 상태 표시 줄에 표시되고 사용자는 링크를 복사하거나 Hero 상세보기를 새 탭에 열 수 있습니다.

이 효과를 얻으려면 dashboard.component.html을 다시 열고 반복된 <div * ngFor ...>태그를 <a>태그로 수정합니다. 여는 <a>태그를 다음과 같이 변경합니다.

src/app/dashboard.component.html (repeated tag)

1
<a *ngFor="let hero of heroes" [routerLink]="['/detail', hero.id]" class="col-1-4">

[routerLink] 바인딩에 주목하십시오. 이 페이지의 Router 링크 섹션에서 설명했듯이, AppComponent 템플릿의 최상위 Navigation은 목적지 링크 "/dashboard""/heroes"의 고정 경로로 설정된 라우터 링크를 가지고 있습니다.

이번에는 링크 파라미터 배열을 포함하는 표현식에 바인딩됩니다. 배열은 대상 라우트의 Path와 현재 Hero의 id 값으로 설정된 라우트 파라미터라는 두가지 요소를 가지고 있습니다.

두 개의 배열 항목은 이전에 app.module.ts에 추가한 Hero 세부 라우트 정의의 Path와 :id 토큰에 매핑됩니다.

src/app/app.module.ts (hero detail)

1
2
3
4
{
path: 'detail/:id',
component: HeroDetailComponent
},

브라우저를 새로 고치고 대시 보드에서 Hero을 선택합니다. 그리고 Hero의 세부 사항을 Navigate합니다.

라우트를 라우팅모듈로 리팩토링

AppModule의 약 20개의 라인이 4개의 라우트 구성에 사용됩니다. 대부분의 응용 프로그램에는 더 많은 라우트가 있으며 원하지 않거나 권한이 없는 Navigation을 방지하기 위해 Guard Service가 추가됩니다. Routing & Navigation 페이지의 Route Guards 섹션에서 Guard Service에 대해 자세히 알아보십시오. 라우팅은 모듈을 복잡하게 하고, Angular 컴파일러가 전체 앱에 대한 주요 내용을 판단하는 주된 목적을 불분명하게합니다.

라우팅 구성을 자체 클래스로 리팩토링하는 것이 좋습니다. 현재 RouterModule.forRoot()는 Angular ModuleWithProviders를 생성합니다. 라우팅 전용 클래스는 routing module이어야합니다. 자세한 내용은 Routing & Navigation 페이지의 Milestone #2: Routing Module 섹션을 참조하십시오.

규칙에 따라 라우팅 모듈 이름에는 “Routing”이라는 단어가 포함되어 있으며 Navigate된 Component를 선언하는 모듈의 이름과 맞춰야합니다.

app-routing.module.ts 파일을 app.module.ts와 같은 레벨로 만듭니다. AppModule 클래스에서 추출한 다음 내용을 포함합니다.

src/app/app-routing.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from './hero-detail.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'detail/:id', component: HeroDetailComponent },
{ path: 'heroes', component: HeroesComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}

라우팅 모듈의 일반적인 특징은 다음과 같습니다.

  • 라우팅 모듈은 라우트(경로)를 변수로 가져옵니다. 변수는 나중에 모듈을 export할 경우 라우팅 모듈 패턴을 명확하게합니다.
  • 라우팅 모듈은 importRouterModule.forRoot (routes)를 추가합니다.
  • 라우팅 모듈은 RouterModuleexport에 추가하여 사용하는 모듈의 ComponentRouterLinkRouterOutlet과 같은 라우터 선언문에 액세스할 수 있도록합니다.
  • declarations이 없습니다. declarations은 사용하는 모듈의 책임입니다.
  • Guard Service가있는 경우 라우팅 모듈은 모듈 providers를 추가합니다. 이 예제에서는 아무 것도 없습니다.

Update AppModule

AppModule에서 라우팅 설정을 삭제하고 AppRoutingModule에 가져옵니다. ES2015 import 문을 사용하여 NgModule.imports 목록에 추가합니다.

아래는 리팩토링 전의 상태와 비교한, 수정된 AppModule이 있습니다.

src/app/app.module.ts (after)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard.component';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroesComponent } from './heroes.component';
import { HeroService } from './hero.service';
import { AppRoutingModule } from './app-routing.module';
@NgModule({
imports: [
BrowserModule,
FormsModule,
AppRoutingModule
],
declarations: [
AppComponent,
DashboardComponent,
HeroDetailComponent,
HeroesComponent
],
providers: [ HeroService ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

src/app/app.module.ts (before)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { HeroDetailComponent } from './hero-detail.component';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
import { HeroService } from './hero.service';
@NgModule({
imports: [
BrowserModule,
FormsModule,
RouterModule.forRoot([
{
path: '',
redirectTo: '/dashboard',
pathMatch: 'full'
},
{
path: 'dashboard',
component: DashboardComponent
},
{
path: 'detail/:id',
component: HeroDetailComponent
},
{
path: 'heroes',
component: HeroesComponent
}
])
],
declarations: [
AppComponent,
DashboardComponent,
HeroDetailComponent,
HeroesComponent
],
providers: [
HeroService
],
bootstrap: [ AppComponent ]
})
export class AppModule {
}

수정 및 단순화된 AppModule은 앱의 주요 부분을 식별하는데 중점을 둡니다.

HeroesComponent에서 Hero 선택

HeroesComponent에서 현재 템플릿은 “master/detail” 스타일로 위에 Heroes 목록을 보여주고 아래에 선택된 Hero의 세부 사항을 보여줍니다.

src/app/heroes.component.ts (current template)

1
2
3
4
5
6
7
8
9
10
11
12
template: `
<h1>{{title}}</h1>
<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>
<hero-detail [hero]="selectedHero"></hero-detail>
`,

맨 위에있는 <h1>을 삭제합니다.

템플릿의 마지막줄에 있는 <hero-detail> 태그를 삭제합니다.

더 이상 HeroDetailComponent 전체를 표시하지 않습니다. 대신 자신의 페이지에 Hero 상세를 표시하고 대시 보드에서 했던것처럼 Hero 상세를 전달합니다.

그러나 사용자가 목록에서 Hero을 선택하면 상세 보기 페이지로 이동하지 않습니다. 대신이 페이지에서 상세 보기를 볼 수 있으며 전체 상세보기 페이지로 이동하려면 버튼을 클릭해야합니다.

mini 상세보기 추가

<hero-detail>을 사용되었던 템플릿의 맨 아래에 다음 HTML을 추가합니다.

src/app/heroes.component.ts

1
2
3
4
5
6
<div *ngIf="selectedHero">
<h2>
{{selectedHero.name | uppercase}} is my hero
</h2>
<button (click)="gotoDetail()">View Details</button>
</div>

Hero를 클릭하면 사용자는 Hero들 목록 아래에 다음과 같은 내용을 보게됩니다.

uppercase Pipe를 이용한 서식

Hero의 이름은 Pipe 연산자 (|) 바로 뒤에있는 uppercase Pipe 때문에 대문자로 표시됩니다.

1
{{selectedHero.name | uppercase}} is my hero

Pipe는 문자열, 통화 금액, 날짜 및 기타 디스플레이 데이터의 서식을 지정하는 좋은 방법입니다. Angular에서 제공하는 여러가지 Pipe를 사용하거나 직접 작성할 수 있습니다.

Pipe에 대한 자세한 내용은 Pipe 페이지를 참조하십시오.

Content를 Component 파일 밖으로 이동

사용자가 View Details 버튼을 클릭할 때 HeroDetailComponent에 대한 Navigation을 지원하도록 Component 클래스를 업데이트 해야합니다.

Component 파일은 큽니다. HTML과 CSS 속에서 컴포넌트 로직을 찾는 것은 어렵습니다.

더 변경하기 전에 템플릿과 Style을 자체 파일로 마이그레이션합니다.

먼저, 템플릿 컨텐츠를 heroes.component.ts에서 새로운 heroes.component.html 파일로 이동합니다. 그러나 백틱은 복사하지 않습니다. heroes.component.ts에 관해서는 잠시 후에 살펴보겠습니다. 다음으로, Style 내용을 새로운 heroes.component.css 파일로 이동합니다.

새로운 두개의 파일은 다음과 같아야합니다.

src/app/heroes.component.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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}} is my hero
</h2>
<button (click)="gotoDetail()">View Details</button>
</div>

src/app/heroes.component.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
.selected {
background-color: #CFD8DC !important;
color: white;
}
.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: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes .text {
position: relative;
top: -3px;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
button {
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}

이제 heroes.component.ts에 대한 Component 메타 데이터로 돌아가 templatestyles을 삭제하고 각각을 templateUrlstyleUrls로 수정합니다. 그리고 새로운 파일을 참조하도록 해당 프로퍼티를 변경합니다.

src/app/heroes.component.ts (revised metadata)

1
2
3
4
5
@Component({
selector: 'my-heroes',
templateUrl: './heroes.component.html',
styleUrls: [ './heroes.component.css' ]
})

styleUrls 프로퍼티는 Style 파일 이름의 배열입니다 (경로 포함). 필요한 경우 다른 위치의 여러 Style 파일을 나열할 수 있습니다.

HeroesComponent 클래스 업데이트

HeroesComponent는 버튼 클릭에 대한 응답으로 HeroesDetailComponent로 Navigate합니다. 버튼의 클릭 이벤트는 라우터에게 어디로 가야 하는지를 알려줌으로써 명령형으로 Navigate하는 gotoDetail() 메서드에 묶여있습니다.

이 방법을 사용하려면 Component 클래스를 다음과 같이 변경해야합니다.

  1. Angular Router 라이브러리에서 Routerimport합니다.
  2. HeroService의 생성자에 Router를 주입합니다.
  3. Routernavigate() 메서드에서 호출할 goDetail() 메서드를 구현합니다.

src/app/heroes.component.ts (gotoDetail)

1
2
3
gotoDetail(): void {
this.router.navigate(['/detail', this.selectedHero.id]);
}

DashboardComponent[routerLink] 바인딩에서 했던 것처럼 두개의 엘리먼트로된 링크 파라미터 배열 (Path와 라우트 파라미터)을 라우터의 navigate() 메서드에 전달한다는 것에 주의합니다. 아래는 수정된 HeroesComponent 클래스입니다.

src/app/heroes.component.ts (class)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export class HeroesComponent implements OnInit {
heroes: Hero[];
selectedHero: Hero;
constructor(
private router: Router,
private heroService: HeroService) { }
getHeroes(): void {
this.heroService.getHeroes().then(heroes => this.heroes = heroes);
}
ngOnInit(): void {
this.getHeroes();
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
gotoDetail(): void {
this.router.navigate(['/detail', this.selectedHero.id]);
}
}

브라우저를 새로 고침하고 클릭합니다. 사용자는 Dashboard에서 Hero 상세보기, Heroes 목록에서 상세보기, Hero 상세보기, 다시 Heroes 목록으로 돌아갈 수 있습니다.

이 페이지에서 추가한 모든 Navigation 요구 사항을 충족했습니다.

앱 Style

앱은 기능적이지만 Style이 필요합니다. 대시 보드 Hero는 사각형 row로 표시되어야합니다. 반응형 디자인을 위한 간단한 미디어 쿼리를 포함하여 약 60 줄의 CSS를 이 용도로 사용했습니다.

알다시피, CSS를 styles 메타 데이터에 추가하면 Component 로직이 흐려집니다. 대신 CSS를 별도의 .css 파일로 저장합니다.

dashboard.component.css 파일을 app 폴더에 추가하고 Component 메타 데이터의 styleUrls 배열 프로퍼티에서 다음과 같이 이 파일을 참조합니다.

src/app/dashboard.component.ts (styleUrls)

1
styleUrls: [ './dashboard.component.css' ]

Hero 상세보기에 Style 추가

또한 HeroDetailComponent 용 CSS Style을 제공합니다.

hero-detail.component.cssapp 폴더에 추가하고 styleUrls 배열 안에 DashboardComponent에서 했던 것처럼 참조를 추가합니다. 또한 hero-detail.component.ts에서 hero 프로퍼티의 @Input Decoratorimport를 제거합니다.

다음은 Component의 CSS 파일 내용입니다.

src/app/hero-detail.component.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
label {
display: inline-block;
width: 3em;
margin: .5em 0;
color: #607D8B;
font-weight: bold;
}
input {
height: 2em;
font-size: 1em;
padding-left: .4em;
}
button {
margin-top: 20px;
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer; cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #ccc;
cursor: auto;
}

src/app/dashboard.component.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
[class*='col-'] {
float: left;
padding-right: 20px;
padding-bottom: 20px;
}
[class*='col-']:last-of-type {
padding-right: 0;
}
a {
text-decoration: none;
}
*, *:after, *:before {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
h3 {
text-align: center; margin-bottom: 0;
}
h4 {
position: relative;
}
.grid {
margin: 0;
}
.col-1-4 {
width: 25%;
}
.module {
padding: 20px;
text-align: center;
color: #eee;
max-height: 120px;
min-width: 120px;
background-color: #607D8B;
border-radius: 2px;
}
.module:hover {
background-color: #EEE;
cursor: pointer;
color: #607d8b;
}
.grid-pad {
padding: 10px 0;
}
.grid-pad > [class*='col-']:last-of-type {
padding-right: 20px;
}
@media (max-width: 600px) {
.module {
font-size: 10px;
max-height: 75px; }
}
@media (max-width: 1024px) {
.grid {
margin: 0;
}
.module {
min-width: 60px;
}
}

제공된 CSS는 AppComponent의 Navigation 링크를 선택 가능한 버튼과 비슷하게 만듭니다. 이러한 링크는 <nav>태그로 감싸도록 하겠습니다.

다음 내용으로 app.component.css 파일을 app 폴더에 추가합니다.

src/app/app.component.css (navigation styles)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
h1 {
font-size: 1.2em;
color: #999;
margin-bottom: 0;
}
h2 {
font-size: 2em;
margin-top: 0;
padding-top: 0;
}
nav a {
padding: 5px 10px;
text-decoration: none;
margin-top: 10px;
display: inline-block;
background-color: #eee;
border-radius: 4px;
}
nav a:visited, a:link {
color: #607D8B;
}
nav a:hover {
color: #039be5;
background-color: #CFD8DC;
}
nav a.active {
color: #039be5;
}

The routerLinkActive directive
Angular RouterrouterLinkActive Directive를 사용하여 라우트가 활성 라우트와 일치하는 HTML 탐색 요소에 클래스를 추가할 수 있습니다. 해야할 일은 Style을 정의하는 것뿐입니다.

src/app/app.component.ts (active router links)

1
2
3
4
5
6
7
8
template: `
<h1>{{title}}</h1>
<nav>
<a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
</nav>
<router-outlet></router-outlet>
`,

다음과 같이 이 CSS 파일을 참조하는 styleUrls 프로퍼티를 추가합니다.

src/app/app.component.ts

1
styleUrls: ['./app.component.css'],

전체 앱 Style

Component에 Style을 추가하면 Component가 필요로하는 것을 HTML, CSS 및 코드 한 곳에 편리하게 보관할 수 있습니다. 패키지로 만들면 Component를 다른 곳에서 재사용하기 쉽습니다.

Component 외부의 응용 프로그램 레벨에서 Style을 만들 수도 있습니다.

디자이너는 전체 앱의 엘리먼트에 적용할 수 있는 기본 Style을 제공합니다. 이는 setup 중 이전에 설치 한 전체 마스터 Style 세트에 해당합니다. 다음은 일부 발췌한 내용입니다.

src/styles.css (excerpt)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* Master Styles */
h1 {
color: #369;
font-family: Arial, Helvetica, sans-serif;
font-size: 250%;
}
h2, h3 {
color: #444;
font-family: Arial, Helvetica, sans-serif;
font-weight: lighter;
}
body {
margin: 2em;
}
body, input[text], button {
color: #888;
font-family: Cambria, Georgia;
}
/* everywhere else */
* {
font-family: Arial, Helvetica, sans-serif;
}

styles.css 파일을 만듭니다. 파일에 여기에 제공된 마스터 Style을 포함합니다. 이 Style 시트를 참조하려면 index.html도 편집합니다.

src/index.html (link ref)

1
<link rel="stylesheet" href="styles.css">

지금 앱을 보면 대시 보드, Hero 및 Navigation 링크 Style이 지정되어 있습니다.

Application 구조와 코드

이 예제 페이지의 라이브 예제/예제 다운로드에서 샘플 소스 코드를 확인하십시오. 다음과 같은 구조인지 확인하십시오.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
angular-tour-of-heroes
┣ src
┃ ┣ app
┃ ┃ ┣ app.component.ts
┃ ┃ ┣ app.module.ts
┃ ┃ ┣ app-routing.module.ts
┃ ┃ ┣ dashboard.component.css
┃ ┃ ┣ dashboard.component.html
┃ ┃ ┣ dashboard.component.ts
┃ ┃ ┣ hero.service.ts
┃ ┃ ┣ hero.ts
┃ ┃ ┣ hero-detail.component.css
┃ ┃ ┣ hero-detail.component.html
┃ ┃ ┣ hero-detail.component.ts
┃ ┃ ┣ heroes.component.css
┃ ┃ ┣ heroes.component.html
┃ ┃ ┣ heroes.component.ts
┃ ┃ ┗ mock-heroes.ts
┃ ┣ main.ts
┃ ┣ index.html
┃ ┣ styles.css
┃ ┣ systemjs.config.js
┃ ┗ tsconfig.json
┣ node_modules ...
┗ package.json

어디까지 했나?

이 페이지에서 얻은 결과는 다음과 같습니다.

  • 각기 다른 Component를 Navigate하기 위해 Angular Router를 추가했습니다.
  • 탐색 메뉴 항목을 나타내는 라우터 링크를 만드는 방법을 배웠습니다.
  • 라우터 링크 파라미터를 사용하여 사용자가 선택한 Hero의 상세보기를 탐색했습니다.
  • 여러 Component간에 HeroService를 공유했습니다.
  • HTML과 CSS를 컴포넌트 파일에서 자신의 파일로 옮겼습니다.
  • 데이터 서식을 지정하기 위해 uppercase Pipe를 추가했습니다.

앱은 이 라이브 예제/예제 다운로드와 비슷해야합니다.

앞으로의 여정

앱을 제작하는데 기초가 많이 필요합니다. 중요한 원격 데이터 액세스에 대한 부분이 빠져있습니다.

다음 튜토리얼 페이지에서 Mock 데이터를 http를 사용하여 서버에서 검색한 데이터로 바꿀것이다.


이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://angular.io/tutorial/toh-pt5]

참고

공유하기