Tutorial: Tour of Heroes - Add Navigation

54774 단어 AngularAngular

Add naviagtion with routing

듀토리얼에서 만들고 있는 앱에 아래의 기능을 추가할 거임.

  • 대시보드 뷰
  • 대시보드 뷰랑 Heroes 사이를 navigate할 수있는 기능
  • 유저가 hero name을 어느 뷰에서든 클릭하면 selected hero에 대한
    detail이 있는 뷰로 이동하는 기능
  • 유저가 이메일의 deep link를 클릭하면 특정한 hero에 대해서
    detail view를 열어주는 기능

Add the AppRouting Module

라우팅만 하는 탑레벨 모듈을 만들어서 AppModule에 import 해줄거임.

일반적으로 모듈 클래스 이름은 AppRoutingModule로 하고 이 클래스는
src/app 안에 있는 app-routing-module.ts에 들어있음.

ng generate module app-routing --flat --module=app

위 명령어로 모듈 생성함.
--flat은 파일을 자신만의 폴더를 만들어서 생성하지않고 src/app에 추가해줌.
--modlue=app은 CLI가 AppModuleimports 배열에 모듈을 추가하라고 알려주는거임.

// src/app/app.module.ts
...
@NgModule({
  ...
  imports: [
    ...
    AppRoutingModule,
    ...
  ],
  ...
})

맨 처음에 CLI로 앱 생성할때 add routing = yes해서 이미 app-routing.module.ts가 있어서
위 명령어 했을떄 A merge conflicted on path "/src/app/app-routing.module.ts".라고 나오긴 하는데
이미 있어서 그런거고 똑같은 파일이라서 에러는 상관없음.

routing 모듈은 아래처럼 생겼음.

// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

위 코드를 아래처럼 수정해야함.

// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';

const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

위 코드를 보면 app-routing.module.ts 파일은 RouterModuleRoutes를 import해서
앱이 routing 기능을 가지게 해줌. 다음으로 하는 import는 HeroesComponent를 가져와서
라우터가 route를 configure했을때 갈 수있는 곳을 제공해줌.

Routes

app-routing.module.ts의 다음 부분은 route를 configure 하는 부분임. Routes는 Router한테
유저가 링크를 클릭하거나 URL을 브라우저의 주소창에 넣어서 들어갈때 어떤 뷰를 보여줄지 알려줌.

app-routing.module.tsHeroesCopmonent를 import했기때문에 모듈에서 routes array안에서
컴포넌트를 사용할 수 있음.

// src/app/app-routing.module.ts
const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];

일반적인 앵귤러 Route는 2개의 property를 가지고 있음.

  • path: 브라우저 주소 바에서 URL에 해당하는 string임.
  • component: 이 route로 왔을떄 라우터가 생성할 컴포넌트임.

따라서 위 코드는 라우터한테 URL로 localhost:4200/heroes가 왔을때 HeroesComponent를 display
하도록 알려줌.

RouterModule.forRoot()

@NgModule 메타데이터는 라우터를 초기화하고 라우터가 브라우저의 location 변화를 감지하도록함.

아래 코드는 AppRoutingModuleRouterModule을 imports 배열에 추가후에
RouterModule.forRoot()을 이용해 RouterModuleroutes로 configure 하도록함.
다음으로 AppRoutingModuleRouterModule을 export해서 RouterModule을 앱 전체에서 사용가능하게함.

// src/app/app-routing.module.ts
...
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
...

forRoot() 메소드를 부르는 이유는 라우터를 앱의 root level에 configure 했기 때문임.
forRoot() 메소드는 라우팅에 필요한 service provider와 directive를 제공하고
현재 브라우저 URL에 기반해서 초기 navigation을 수행해줌.

// RouterModule이 정확히 머임?? router 모듈임?

Add RouterOutlet

AppComponent 템플릿을 열어서 <app-heroes> element를 <router-outlet>
element로 대체해줌.

<!--
    src/app/app.component.html
-->
<h1>{{title}}</h1>
<router-outlet></router-outlet>
<app-messages></app-messages>

AppComponent 템플릿은 유저가 HeroesComponent로 navigate 할때만 HeroesComponent를 보여줄 것이기에
<app-heroes> component`가 더 이상 필요하지 않음.

<router-outlet>안 라우터한테 어디가 routed된 view를 보여줄지 알려줌.

RouterOutlet은 라우터 directive 중에 하나임. 이 directive는 아래에서 볼 수 있는 것처럼 AppRoutingModuleRouterModule를 export하는데 AppRoutingModuleAppModule이 import하기 때문에 AppComponent`에서 사용가능한 것임.

// src/app/app.module.ts
...
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

// src/app/app.module.ts
...
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
...
@NgModule({
  declarations: [
    AppComponent,
    ...
  ],
  imports: [
    ...
    AppRoutingModule,
    ...
  ],
  ...
})
export class AppModule { }

ng generate module app-routing --flate --module=app 명령어에서 --module=app플래그를
사용했기때문에 AppModule에서 import가 된거임.
app-routing.module.ts를 따로 만들었거나 CLI가 아닌 툴로 만들었으면
AppRoutingModuleapp.module.ts에 import하고 @NgModuleimports 배열안에 추가해줘야함.

Try it

이제 브라우저가 새로고침하면서 localhost:4200는 앱의 title은 보여주는데 hero의
list를 보여주지는 않음.

브라우저에서 주소창을 보면 http://localhost:4200//로 끝나는것을 볼 수 있음. HeroesComponent로 가는
route는 /heroes임.

http://localhost:4200/heroes로 들어가보면 이전과 똑같은 화면을 볼 수 있음.

Add a navigation link(routerLink)

보통 유저는 클릭을 통해서 사이트를 navigate하지 주소창에 URL을 직접 입력해서
navigate하지 않음.

<nav> element를 추가하고 그 안에 클릭하면 HeroesComponent로 navigate 해주는
anchor element(<a>)를 넣어줌.

<!--
    src/app/app.component.html
-->
<h1>{{title}}</h1>
<nav>
	<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

routerLink attribute는 "./heroes"로 set 됐는데 이 스트링은 라우터가 route를
HeroesComponment로 매칭하게 해주는 스트링임. routerLink는 유저의 클릭을 router navigation으로 바꿔주는 RouterLink 디렉티브의 selecotr임.

@angular/routerRouterLink directive는 템플릿의 element에 적용되면 해당 element를 route로 navigate해주는 링크로 만들어줌.

Add a dashboard view

뷰가 많으면 많을수록 routing의 장점이 돋보임. 지금까지는 heroes 뷰 밖에 없었는데 아래 명령어로 DashboardComponent를 추가해줄 거임.

  ng generate component dashboard

DashboardComponent 템플릿을 아래처럼 수정해줌.
템플릿은 hero name으로 된 링크들을 만들어줌.
아직은 링크눌러도 아무곳으로도 안감.

 <!--
	srp/app/dashboard.component.html
-->
  <h2>Top Heroes</h2>
  <div class="heroes-menu">
    <a *ngFor="let hero of heroes">
      {{hero.name}}
    </a>
  </div>

DashboardCompnent 클래스를 아래처럼 수정해줌.
클래스는 HeroesComponent 클래스랑 비슷함.

  • heroes 배열 property가 있고
  • constructor는 앵귤러가 HeroServiceheroService property로 inject하는것을 expect함.
  • ngOnInit() lifecycle hook은 getHeroes()를 호출함.
  // src/app/dashboard.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html',
  styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
  heroes: Hero[] = [];

  constructor(private heroService: HeroService) { }

  ngOnInit(): void {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes.slice(1, 5));
  }
}

getHeroes()는 hero 리스트에서 잘라내서 인덱스 1부터 4까지의 상위 4개의 hero만 리턴해줌.

  getHeroes(): void {
    this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes.slice(1, 5));
  }

Add the dashboard route

dashboard를 navigate 하기 위해서 router는 적절한 route가 필요함.

DashboardComponentapp-routing-module.ts에 import 해주고 routes 배열에 DashboardComponent에 path를 매치해주는 route를 추가해줌.

// src/app/app-routing.module.ts
import { DashboardComponent } from './dashboard/dashboard.component';

const routes: Routes = [
{ path: 'dashboard', component: DashboardComponent }
];

Add a default route

앱이 시작할때 브라우저의 주소창은 웹사이트의 root을 가리킴. 지금 현재는 root가 존재하는 route에 매칭되는게 없기 때문에 router가 아무곳으로도 navigate해주지 않음.
지금 보면 <router-outlet> 부터 아래는 아무것도 나오지 않고 있음.

앱이 dashboard로 자동적으로 navigate하게 하려면, 아래 route를 routes 배열에 추가 하면됨.

// src/app/app-routing.module.ts
const routes: Routes = [
...
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' }
];

이렇게하면 empty path에 fully match되는 URL을 path가 /dashboard인 곳으로 리다이렉트해줌.

이제 브라우저가 새로고침하면서 DashboardComponent를 로드하고 브라우저 주소창을 보면 localhost:4200/dashboard로 바뀐것을 볼 수 있음.

Add dashboard link to the shell

유저는 페이지 상단의 navigation area의 링크를 눌러서 DashboardComponentHeroesComponent를 왔다갔다 할 수 있어야함.

따라서 AppComponent shell 템플릿의 Heroes 링크 바로 위에 dashboard navigation을 추가해줌.

<!--
	src/app/app.component.html
-->
<h1>{{title}}</h1>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

브라우저가 새로고침 되면 링크를 클릭하는걸로 자유롭게 DashboardComponentHeroesComponent로 이동할 수 있음.

Navigating to hero details

HeroDetailComponent는 선택한 hero의 detail을 보여줌. 지금은 HeroDetailComponentHeroesComponent의 아래 부분에만 보이게 해놨음.

하지만 이제는 유저가 HeroDetailComponent에 아래 3가지 방법으로 접근하게 만들것임. 1. dashoboard에 있는 hero 클릭 2. 원래처럼 heroes list에 있는 hero 클릭 3. 원하는 hero에 대한 deep link` URL을 브라우저 주소창에 입력하기

이번 섹션에서는 위 3가지 방법으로 HeroDetailComponent로 navigate할 수 있게 만들면서 HeroesCopmonent에서 HeroDetailComponent를 떼어낼것임.

Delete hero details from HeroesComponent

유저가 HeroesComponent의 hero를 클릭하면 앱은 HeroDetailComponent로 이동하고 heros list 뷰를 hero detail 뷰로 바꿔주도록 할거임. heroes list 뷰는 hero detail을 이전처럼 보여주지 않을것임.

HeroesComponent 템플릿을 열어서 ` element을 삭제해 줌. 이러면 hero list에서 hero를 클릭해도 이전처럼 hero detail을 보여주지 않음.

Add a hero detail route

id가 11인 hero의 Hero Detail 뷰로 갈때 ~/detail/11같은 URL은 좋은 URL임.

app-routing.module.ts를 열어서 HeroDetailComponent를 import 해줌.
추가로 hero detail 뷰에 해당하는 path patern인 parameterized route를 routes array에 추가함.

// src/app/app-routing.module.ts
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
...

const routes: Routes = [
  { path: 'heroes', component: HeroesComponent },
  { path: 'dashboard', component: DashboardComponent },
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'detail/:id', component: HeroDetailComponent }
];
...

path의 콜론(:)은 :id가 특정한 hero의 id에 대한 placeholder라는 것을 나타내줌.

DashboardComponennt hero links

DashboardComponent의 상위 4개 hero에 대한 링크는 현재 아무 기능이 없음.

이제 라우터가 HeroDetailComponent에 대한 route를 가지게 됐으니 dashboard hero link가 parameterized dashboard route로 navigate 하도록 수정해야함.

<!--
	src/app/dashboard/dashboard.component.html
--->
<h2>Top Heroes</h2>
<div class="heroes-menu">
<a *ngFor="let hero of heroes"
	routerLink="/detail/{{hero.id}}">
	{{hero.name}}
</a>
</div>

위 코드에서 앵귤러의 interpolation binding를 *ngFor repeater 안에서 사용해서 현재 반복의 hero.idrouterLink에 넣어주고 있음.

HerosComponent hero links

HeroesComponent에 있는 <li> element의 hero item들은 클릭 이벤트가 HeroesCopmonentonSelect() 메소드에 bind 되어있는데 dashboard 템플릿처럼 버튼 말고 <a>로 바꿔줌.

<!--
	src/app/heroes/heroes.component.html
-->
<ul class="heroes">
<li *ngFor="let hero of heroes">
  <a routerLink="/detail/{{hero.id}}">
    <span class="badge">{{hero.id}}</span> {{hero.name}}
  </a>
</li>
</ul>

이제 컴포넌트의 private stylesheet를 수정해서 이전의 버튼처럼 보이도록 바꿔줘야함.

Remove dead code(optional)

HeroesComponent 클래스가 작동은 하지만, onSelect() 메소드랑 selectedHero property는 더 이상 사용을 안하니 삭제해주기.
// 이거 messageService도 그러면 안쓰게 되는데 삭제하는거 찾아보기

// src/app/heroes/hereos.component.ts
export class HeroesComponent implements OnInit {
  heroes: Hero[] = [];

  constructor(private heroService: HeroService) { }

  ngOnInit(): void {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes()
    .subscribe(heroes => this.heroes = heroes);
  }
}

Routable HeroDetailComponent

이전에 parent인 HeroesComponentHeroDetailComponent.hero property를 set해줘서 HeroDetailComponent가 hero를 보여줬었는데 다 수정해서 이제는 그렇게 보여주지 않음. 이제 라우터가 ~/detail/같은 URL이 들어오면 HeroDetailComponent를 생성해줌.

  <!--
	src/app/heroes/heroes.component.html
	parent-child 관계였던 이전 코드임
-->
  <app-hero-detail [hero]="selectedHero"></app-hero-detail>
// src/app/hero-detail/hero-detial.component.ts
  // parent-child 관계였던 이전 코드임
@Input() hero?: Hero;

그렇게 하려면 아직 더 수정할게 남았음. HeroDetailComponent는 위의 이전 코드에서 했던 방식 말고 다른 방법으로 보여줄 hero를 가지고 와야함.
이 섹션에서는 아래 방법들을 설명해줌.

  • 생성할 route를 가져오는 법
  • route에서 id 추출하기
  • 추출한 idHeroService를 이용해서 hero 가져오기 아래와 같은 import 해주기.
    // src/app/hero-detail/hero-detail.component.ts
    import { ActivatedRoute } from '@angular/router';
    import { Location } from '@angular/common'; 
    import { HeroService } from '../hero.service';
    ActivatedRoute, HeroService 그리고 Location 서비스를 constructor에 private field로 선언해줌.
    // src/app/hero-detail/hero-detail.component.ts
    constructor(
      private route: ActivatedRoute,
      private heroService: HeroService,
      private location: Location
    ) {}
    ActiveRouteHeroDetailComponent 인스턴스에 대한 route 정보를 가지고 있음. HeroDetailComponent는 URL에서 route의 parameter(id)를 추출하고 싶어함.
    "id" parameter는 보여줄 hero의 id를 말함. HeroService는 remote 서버에서 hero data를 가져오니 HeroDetailComponent 서비스를 이용해서 보여줄 hero를 가져옴. location은 앵귤러의 서비스로 브라우저와 상호작용하게 해줌. HeroDetailComponent 뷰로 왔을때 다시 뒤로 navigate 하기 위해 사용할 거임.

    Extract the id route parameter

    ngOnInit() lifecycle hook에서 getHeor()를 부르도록 아래처럼 해줌.
  // src/app/hero-detail/hero-detail.component.ts
ngOnInit(): void {
  this.getHero();
}

getHero(): void {
  const id = Number(this.route.snapshot.paramMap.get('id'));
  this.heroService.getHero(id)
    .subscribe(hero => this.hero = hero);
}

route.snapshot 컴포넌트가 생성된 직후의 route information의 static image임.

paramMap은 URL에서 추출한 route paramter value의 dictionary임. "id" 키는 가져올 here의 id를 리턴함.(https://angular.io/api/router/ParamMap)

  interface ParamMap {
    keys: string[]
    has(name: string): boolean
    get(name: string): string | null
    getAll(name: string): string[]
  }

route parameter는 언제나 스트링이어서 자바스크립트 Number 함수로 number로 바꿔줌.

브라우저를 새로고침하면 아직까지는 HeroServicegetHero()메소드가 없기때문에 컴파일 에러가 나옴.

Add HeroService.getHero()

HeroService를 열어서 getHero() 메소드를 추가해줌.

// src/app/hero.service.ts
getHero(id: number): Observable<Hero> {
  // For now, assume that a hero with the specified `id` always exists.
  // Error handling will be added in the next step of the tutorial.
// 화살표 함수는 iterable 한거 다 돌아서 아래되는거임.
  const hero = HEROES.find(h => h.id === id)!;
  this.messageService.add(`HeroService: fetched hero id=${id}`);
  return of(hero);
}

getHeroes()와 비슷하게 getHero()는 asynchronous signature를 가지고 있음. 이 메소드는 RxJS의 of()함수를 이용해서 mock hero를 Observable로 리턴함.
나중에 getHero()Http request를 사용하도록 바꿀거임. 서비스로 데이터를 가져오는게 분리되어있기때문에 바꿀떄 HeroDetailComponent를 수정할 필요없이 HeroService만 바꿔주면됨.

Try it

브라우저가 새로고침되면서 앱이 다시 제대로 작동할 거임. dashboard의 hero나 herolist의 hero를 클릭해서 hero detail 뷰로 이동할 수 있음.

localhost:4200/detail/11로 가게되면 라우터가 id가 11인 hero의 detail 뷰로 navigate 해줌.

Find the way back

브라우저의 뒤로가기 버튼을 누르면 hero list나 dashboard 뷰로 돌아갈 수 있지만 HeroDetail뷰에 뒤로가기 버튼을 만들어서 뒤로 갈 수 있게 만드는 것도 좋은 생각임.

HeroDetailComponent 템플릿의 맨 아래에 뒤로 가기 버튼을 만들고 컴포넌트의 goback() 메소드를 bind 해줌.

 <!--
	src/app/hero-detail/hero-detail.component.html
-->
 <button type="button" (click)="goBack()">go back</button>

goback() 메소드도 추가해줌.

// src/app/hero-detail/hero-detail.component.ts
goBack(): void {
  this.location.back();
}

location.back()은 Navigates back in the platform's history 하게 해줌.

Summary

  • 앵귤러 router를 추가해서 컴포넌트들을 navigate 할 수 있게 했음
  • AppComponent<a> 링크들과 <router-outlet>를 이용해서 navigation shell로 변경했음
  • `AppRoutingModule에 router를 configure했음
  • route랑 redirect rout, parameterized route를 define 했음
  • routerLink directive를 <a>안에 사용했음
  • master/detail(HeroesComponent랑 HeroeDetailCopmonent)뷰를 routed detail view로 바꿨음.
  • router link parameter를 사용해서 선택한 hero의 detail view로 이동할 수 있게 했음
  • HeroService를 여러 컴포넌트에서 사용하게 했음

출처

좋은 웹페이지 즐겨찾기