Tour of Heroes - Routing (routerLink)

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

이 게시물의 소스코드는 Tour of Heroes / Tour of Heroes - Service (@injectable, observable)에서 이어집니다.

tour-of-heroes.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.

git reset --hard
git pull
git reset --hard 16242b1
git reset --soft e1a7ce6
npm install
atom .

tour-of-heroes.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 tour-of-heroes.git 을 clone 합니다.

git clone https://github.com/a-mean-blogger/tour-of-heroes.git
cd tour-of-heroes
git reset --hard 16242b1
git reset --soft e1a7ce6
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/tour-of-heroes/tree/16242b1e8bea682110b1717a00a38a8e4d26540c


이번 강의에서는 route를 만들어 사이트의 페이지를 나누고 dashbaord를 추가합니다. 우선 이번 강의에서 뭘 만들건지 페이지들의 관계도를 봅시다.


이미지 출처(https://angular.io/tutorial/toh-pt5)

  • 페이지 종류는 총 3개로, 위 그림에서 왼쪽 위의 dashboard 페이지, 왼쪽 아래의 hero detail 페이지 그리고 오른쪽의 heroes 페이지입니다.
  • 모든 페이지에는 상단에 메뉴가 있어서 dashboardheroes 페이지로 이동할 수 있습니다.
  • dashboard 페이지는 4명의 hero를 보여주며, hero를 클릭하면 hero detail 페이지로 이동합니다.
  • heroes 페이지는 모든 hero를 보여주며, hero를 클릭하면 아래에 view details 버튼이 생기고 이 버튼을 클릭하면 hero detail 페이지로 이동합니다.
  • hero detail에서는 hero 이름을 수정할 수 있고, back버튼을 클릭하면 이전 페이지로 돌아갑니다.

우리가 만들 route은 다음과 같습니다.

  • '/' - /dashboard로 redirect합니다.
  • '/dashboard' - dashboard 페이지로 이동합니다.
  • '/heroes' - heroes 페이지로 이동합니다.
  • '/detail/:id' - hero detail 페이지로 이동합니다. route에서 콜론(:)은 parameter를 뜻합니다. :id자리에는 어떠한 값이 오더라도 /detail/:id route로 간주되며, :id자리에 들어온 값은 /detail/:id route의 parameter로 따로 저장이 됩니다. 우리는 이 자리에 hero의 id를 받을 예정이며, 이 값에 해당하는 id를 가진 hero를 이 페이지에서 보여줍니다.

이번 강의에서 한가지 짚고 넘어가야 할 부분이 있는데, Angular 사이트 내에서 url 전환은 실제 페이지 전환이 아닙니다. Angular는 SPA인데, 페이지가 전환되면 SPA가 아니죠. 메뉴버튼을 누르면 메뉴버튼 아래의 부분이 해당 메뉴에 해당하는 내용으로 바뀌게 됩니다. 이때 브라우저의 url도 함께 바뀌게 되어 페이지를 이동한 것 처럼 보이지만, 실제로 페이지 이동을 한 것이 아닙니다.

반대로 url을 입력받으면 Angular 사이트가 해당 url에 해당하는 html코드를 구성해서 보여주게 됩니다. 실제 Angular 사이트는 페이지 하나로만 구성이 됩니다.(src\index.html)

이 부분에 대해 설명이 더 필요하신 분들은 댓글 남겨 주세요.

폴더 구조

Dashboard component를 추가하기 위해 아래 명령어를 입력합니다.

$ ng g c dashboard --skipTests

app-routing.module.ts 파일을 추가하기 위해 아래 명령어를 입력합니다.

$ ng g m app-routing --flat --module=app

ng g mng generate module의 축약형으로 module 파일을 생성합니다. --flat은 폴더를 생성하지 않고 src/app에 파일을 생성하게 하는 옵션입니다. --module=모듈명은 현재 모듈을 어느 모듈의 하위모듈로 설정할지를 정하는 옵션입니다. 우리는 app-routing 모듈을 app 모듈에 등록하고 있습니다.

코드 - Hero Service

// src/app/hero.service.ts

  ...

  getHeroes(): Observable<Hero[]> {
    ...
  }

  getHero(id: number): Observable<Hero> { // 1
    this.messageService.add(`HeroService: fetched hero id=${id}`);
    return of(HEROES.find(hero => hero.id === id));
  }
}

1. getHero함수가 추가되었습니다. 이전에 만들었던 getHeroes함수는 hero mock data의 전체 배열을 리턴합니다. 반면에 getHero함수는 id를 인자로 받고 HEROES배열에서 id가 일치하는 hero를 찾아 해당 hero만 리턴합니다.

코드 - Hero Detail Component

// src/app/hero-detail/hero-detail.component.ts

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';

import { Hero } from '../hero';
import { HeroService }  from '../hero.service';

@Component({
  selector: 'app-hero-detail',
  templateUrl: './hero-detail.component.html',
  styleUrls: ['./hero-detail.component.css']
})
export class HeroDetailComponent implements OnInit {
  hero: Hero; // 1

  constructor(
    private route: ActivatedRoute,
    private heroService: HeroService,
    private location: Location,
  ) {}

  ngOnInit() {
    this.getHero(); // 2
  }

  getHero(): void {
    const id = +this.route.snapshot.paramMap.get('id'); // 3-1
    this.heroService.getHero(id)            // 3-2
      .subscribe(hero => this.hero = hero); // 3-2
  }

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

1. 마스터-디테일 강의에서 추가했던 @input이 빠졌습니다. hero-detail component는 다른 페이지에서 하위 component로 사용되는 것이 아니라, hero detail 페이지를 담당하는 component로 사용되기 때문입니다.

2. 페이지가 요청되면 HeroDetailComponent class의 getHero함수를 호출합니다.

3-1. this.route.snapshot.paramMap.get함수로 detail/:id route에서 :id에 위치하는 텍스트값을 가져올 수 있습니다. hero-detail component를 detail/:id에 연결하는 코드는 app-routing.module.ts파일에 있습니다. 이부분은 뒤에서 설명합니다. 이 줄에서 +가 사용된 이유는, url parameter로 받은 값은 문자열 타입이기 때문에 숫자로 바꿔 주기 위한 것입니다. 이건 자바스크립트에서 사용하는 일종의 꼼수같은 건데요, 문자열인 숫자, 예를 들어 "1"을 숫자 1로 바꿔주려면 +"1"을 해주면 됩니다.

3-2. Hero service의 getHero함수를 사용해서 해당 id의 hero 데이터를 가져와서 this.hero에 대입합니다.

4. goBack함수는 브라우저의 '뒤로가기' 기능을 하는 코드를 담고 있습니다.

<!-- src/app/hero-detail/hero-detail.component.html -->

<div *ngIf="hero">

  <h2>{{hero.name | uppercase}} Details</h2>
  <div><span>id: </span>{{hero.id}}</div>
  <div>
    <label>name:
      <input [(ngModel)]="hero.name" placeholder="name"/>
    </label>
  </div>

  <button (click)="goBack()">go back</button> <!-- 1 -->
  
</div>

1. '뒤로가기' 버튼이 추가되었습니다.

코드 - Heroes Component

// src/app/heroes/heroes.component.ts

...
export class HeroesComponent implements OnInit {
  heroes: Hero[];

  constructor(private heroService: HeroService) { }

  ngOnInit() {
    this.getHeroes();
  }

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

기존의 코드에서 selectedHero항목과 onSelect함수가 지워졌습니다. 해당 기능은 이제 페이지 전환에 의해 hero-detail component에서 다루어 집니다.

<!-- src/app/heroes/heroes.component.html -->

<h2>My Heroes</h2>
<ul class="heroes">
  <li *ngFor="let hero of heroes"> <!-- 1 -->
    <a routerLink="/detail/{{hero.id}}"> <!-- 2 -->
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </a>
  </li>
</ul>

1. li 태그에서 CSS class을 변경하는 코드와, click event를 처리하는 코드가 지워졌습니다.

2. a 태그는 보통 href 항목과 함께 사용하는데, href 항목 대신 routerLink 항목을 사용하고 있습니다. routerLink 항목은 angular에서 지원하는 항목으로, Angular 코드에서 페이지를 이동할 때 쓰입니다. 왜 그냥 href안쓰고 굳이 routerLink 항목을 써야하는지 궁금하신 분들은, 위 코드에서 routeLink를 href로 고친후 테스트해보세요. 차이점이 보일겁니다.

코드 - Dashboard Component

// src/app/dashboard/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() {
    this.getHeroes();
  }

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

DashboardComponent class는 HeroesComponent class와 굉장히 유사하죠. 차이점이라면, getHeroes함수에서 hero들을 전부 가져오는 것이 아니라, 2번째부터 5번째까지, 4개의 hero 데이터만 가져오는 것이 차이점입니다.

<!-- src/app/dashboard/dashboard.component.html -->

<h3>Top Heroes</h3>
<div class="grid grid-pad">
  <a *ngFor="let hero of heroes" class="col-1-4"
      routerLink="/detail/{{hero.id}}">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </a>
</div>

HeroesComponent class와 유사하게 *ngFor를 사용해서 heroes의 리스트를 보여줍니다. 다만 html 코드를 달리해서 다른 모양으로 보여주는 것 뿐입니다.

/* src/app/dashboard/dashboard.component.css */

[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: #3f525c;
  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;
  }
}

코드 - App Component

<!-- src/app/app.component.html -->

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

1. <app-heroes></app-heroes>가 지워지고 <router-outlet></router-outlet>가 추가되었습니다. 즉 heroes component를 빼버린 것이죠. 대신에 route-oulet은 router에 의해 선택된 component를 표시해 줍니다.

코드 - App Routing Module

// src/app/app-routing.module.ts 

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { DashboardComponent } from './dashboard/dashboard.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';

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

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

이번 강의의 핵심인 route를 설정법을 알아봅시다. 간단히 요약하면Routes타입인routes상수를 생성하고RouterModule.forRoot 함수에 넣은 후 import에 추가하고,RouterModule를 exports에 추가합니다. 이렇게 생성된 AppRoutingModule class는 app module에 imports에 추가되어 설정한 route이 적용됩니다. 

@NgModule의 imports 항목에는 다른 module들이 추가됩니다. imports에 추가된 항목이 exports에도 추가되면,  현재 module(위 코드에서는AppRoutingModule)이 다른 module에 imports되는 경우 exports의 module들도 함께 imports됩니다.

app module을 제외한 다른 module은 app module을 분리하고 정리하기 위해서 사용됩니다. RouterModule.forRoot가 설정된RouterModule를 app module의 imports에 바로 넣어도 routing이 작동합니다. 다만 route 설정이라는 특별한 기능을 파일로 분리해서 관리하기 위해 app-routing module을 만든 것입니다.

1. 실제 route이 설정되는 부분입니다.

1-1. 웹사이트의 루트 route을 설정합니다. dashboard로 redirect하며, url이 완전히 같아야만 이 설정이 적용됩니다. (pathMatch의 기본값은 'prefix'로, route의 앞부분이 동일하면 해당 설정이 적용됩니다. 예를 들어 path가 'test'이고 pathMatch항목이 없다면 'test/abc'로 접속해도 redirect가 됩니다.)

1-2 path와 component가 쓰인 경우에는 해당 path에 접속하면 해당 component를 <router-outlet></router-outlet>에 render합니다.

코드 - App Module

// src/app/app.module.ts

...

import { AppRoutingModule } from './app-routing.module'; // 1
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard/dashboard.component'; // 1
...

@NgModule({
  declarations: [
    AppComponent,
    DashboardComponent, // 1
    HeroesComponent,
    HeroDetailComponent,
    MessagesComponent,
  ],
  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule, // 1
  ],
  ...

1. 이번 강의에서 새로 생성된 component, module가 declarations, imports에 들어갔습니다. 

실행 결과

'Tour of Heroes' 문구 밑에 Dashboard, Heroes 메뉴 버튼들이 추가되었습니다. 각 버튼을 눌러서 페이지가 이동되고 url이 바뀌는 것을 확인합시다.

또한 hero service의 함수가 호출될 때마다 Messages에 그 내용이 기록되는 것을 볼 수 있습니다.

마치며..

이번 강의에서는

  • ng generate module 명령어
  • routerLink로 Angular에서 url을 변경하는 방법
  • <router-outlet></router-outlet> 태그
  • RouterModule 설정방법

을 알아봤습니다. 코드의 변화는 많았지만 어려운 부분은 없어요. 질문있으시면 댓글 남겨주세요.

댓글

댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP