Skip to content

7. 네비게이션 추가하기


히어로들의 여행 앱에 새로운 요구사항이 생겼다:


  • 대시보드 화면을 추가해야 한다.
  • 히어로 목록 화면과 대시보드 화면을 전환하는 기능이 필요하다.
  • 사용자가 화면에서 히어로 이름을 클릭하면 선택된 히어로의 상세정보 화면으로 이동해야 한다.
  • 사용자가 이메일로 받은 딥 링크(deep link)를 클릭하면 해당 히어로의 상세정보 화면을 바로 표시해야 한다.


Note


그래서 최종 결과물은 다음과 같이 3개의 화면을 이동하면서 동작해야 한다:


001


1. AppRoutingModule 생성하기

Angular에서는 최상위 모듈과 같은 계층에 별개의 모듈을 두고 이 모듈에 애플리케이션 최상위 라우팅 모듈을 정의하는 방법을 권장한다. AppModule은 이렇게 정의한 라우팅 설정을 로드해서 사용하기만 하면 된다.


일반적으로 애플리케이션 최상위 라우팅을 담당하는 모듈의 클래스 이름은 AppRoutingModule이라고 정의하며 src/app 폴더에 app-routing.module.ts 파일로 생성한다.


Angular CLI로 다음 명령을 실행해서 라우팅 모듈을 만들어 보자.


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


Note

  • --flat 옵션을 사용하면 새로운 폴더를 만들지 않고 src/app 폴더에 파일을 생성한다.
  • --module=app 옵션을 사용하면 Angular CLI가 이 라우팅 모듈을 AppModuleimports 배열에 자동으로 추가한다.


이 명령을 실행해서 만든 파일의 내용은 다음과 같다:


src/app/app-routing.module.ts (기본 생성 코드)
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";

@NgModule({
  imports: [CommonModule],
  declarations: [],
})
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 심볼을 로드한다. 그리고 라우팅 규칙에 따라 이동할 HeroesComponent를 로드한다.


CommonModule을 로드했던 부분이나 declarations 배열은 필요없기 때문에 AppRoutingModule에서 제거했다. 변경된 나머지 부분에 대해서는 다음 섹션부터 자세하게 알아보자.


1) 라우팅 규칙(Routes)

새롭게 만든 파일에서는 라우팅 규칙을 정의하는 부분도 변경되었다. 라우팅 규칙은 사용자가 링크를 클릭하거나 브라우저 주소표시줄에 URL을 직접 입력했을 때 라우터가 어떤 화면을 표시할지 정의한 것이다.


이전 섹션에서 로드했던 HeroesComponent를 라우팅 규칙으로 등록하려면 routes 배열을 다음과 같이 작성하면 된다:


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


Routes는 보통 두 개의 프로퍼티를 사용한다:


  • path: 브라우저 주소표시줄에 있는 URL과 매칭될 문자열을 지정한다.
  • component: 라우터가 생성하고 화면에 표시할 컴포넌트를 지정한다.


이제 라우터가 path: 'heroes'에 해당하는 URL을 만나면 localhost:4200/heroes와 같은 URL로 이동하면서 HeroesComponent를 표시할 수 있다.


2) RouterModule.forRoot()

@NgModule에 메타데이터를 지정하면 모듈이 생성될 때 라우터를 초기화하면서 브라우저의 주소가 변화되는 것을 감지한다.


그래서 AppRoutingModule에도 라우터를 초기화하기 위해 imports 배열에 RouterModule을 등록해야 하는데, 이 때 RouterModule.forRoot() 메서드에 routes 인자를 넣어서 실행한 결과를 지정한다.


src/app/app-routing.module.ts
imports: [ RouterModule.forRoot(routes) ],


Note

  • 애플리케이션 최상위 계층에 존재하는 라우터를 설정할 때는 forRoot() 메서드를 사용한다.
  • forRoot() 메서드를 사용하면 라우팅과 관련된 서비스 프로바이더와 디렉티브를 애플리케이션에 제공할 수 있으며, 브라우저에서 변경되는 URL을 감지할 수 있다.


그리고 앱에서도 RouterModule을 사용할 수 있도록 AppRoutingModuleexports 배열을 다음과 같이 지정한다.


src/app/app-routing.module.ts (exports 배열)
exports: [RouterModule];


2. 라우팅 영역(RouterOutlet) 추가하기

AppComponent 템플릿을 열어서 <app-heroes> 엘리먼트를 <router-outlet> 엘리먼트로 변경한다.


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


AppComponent 템플릿에는 더이상 <app-heroes> 컴포넌트가 존재하지 않는다. 이 컴포넌트는 이제 사용자의 네비게이션의 의해 화면에 표시된다.


<router-outlet>은 라우팅된 화면이 표시될 위치를 지정하는 엘리먼트이다.


Note

  • RouterOutlet 디렉티브는 AppComponent에서도 사용할 수 있다.
  • 왜냐하면 AppModule이 로드하고 있는 AppRoutingModuleRouterModule을 외부로 공개하고 있기 때문이다.
  • 이 동작은 프로젝트를 생성하는 ng generate 명령을 실행하면서 --module=app 플래그를 지정했기 때문에 자동으로 추가된 것이다.
  • app-routing.module.ts 파일을 직접 생성했거나 Angular CLI 외의 툴을 사용했다면 app.module.ts 파일에서 AppRoutingModule을 로드하고 NgModuleimports 배열에 이 라우팅 모듈을 추가하면 된다.


동작 확인하기

Angular CLI로 다음 명령을 실행해서 애플리케이션을 실행한다.


ng serve


브라우저가 갱신되면 애플리케이션 제목은 표시되지만 히어로의 목록은 표시되지 않는다.


이 때 브라우저의 주소표시줄을 확인해 보자. URL은 /로 끝난다. HeroesComponent는 라우팅 경로 /heroes에 연결되어 있기 때문에 이 주소에서 히어로 목록이 표시되지 않는 것이다.


브라우저 주소표시줄의 URL을 /heroes로 변경해 보자. 그러면 이전과 같이 히어로의 목록이 표시될 것이다.


사용자가 브라우저 주소표시줄에 원하는 URL을 입력해야만 하는 것은 좋은 방법이 아니다. 이 방법보다는 네비게이션을 실행하는 링크를 클릭하는 방법이 더 편할 것이다.


이번에는 앵커(<a>) 엘리먼트를 추가하고, 사용자가 이 엘리먼트를 클릭했을 때 HeroesComponent로 이동하도록 해보자. AppComponent의 템플릿을 다음과 같이 수정한다.


src/app/app.component.html (히어로 목록으로 이동하는 RouterLink)
<h1>{{title}}</h1>
<nav>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>


routerLinkRouterModule이 제공하는 RouterLink 디렉티브이며, 사용자가 이 디렉티브가 적용된 엘리먼트를 클릭하면 네비게이션을 실행한다. 그리고 routerLink 어트리뷰트의 값은 "/heroes"로 할당되었는데, 이 문자열은 HeroesComponent에 해당하는 라우팅 경로를 의미한다.


이제 브라우저가 갱신되면 애플리케이션 제목과 히어로 목록으로 가는 링크가 표시되지만 히어로의 목록은 여전히 표시되지 않는다.


링크를 클릭해 보자. 주소표시줄의 URL이 /heroes로 바뀌면서 히어로 목록이 표시될 것이다.


app.component.css 파일에 CSS 스타일을 작성하면 네비게이션 링크를 더 보기 좋게 표시할 수 있다. 이 내용은 최종코드 리뷰에서 확인할 수 있다.


4. 대시보드 화면 추가하기

라우터를 사용하면 여러 화면을 전환하기도 쉽다. 아직까지는 히어로 목록을 표시하는 화면만 있지만 이제 다른 화면을 추가해 보자.


Angular CLI로 다음 명령을 실행해서 DashboardComponent를 생성한다.


ng generate component dashboard


그러면 DashboardComponent를 구성하는 파일이 생성되면서 이 컴포넌트가 AppModule에 자동으로 등록된다.


이 컴포넌트의 내용을 다음과 같이 수정한다:


<h2>Top Heroes</h2>
<div class="heroes-menu">
  <a *ngFor="let hero of heroes"> {{hero.name}} </a>
</div>
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에 적용되는 CSS styles */

h2 {
  text-align: center;
}

.heroes-menu {
  padding: 0;
  margin: auto;
  max-width: 1000px;

  /* flexbox */
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: space-around;
  align-content: flex-start;
  align-items: flex-start;
}

a {
  background-color: #3f525c;
  border-radius: 2px;
  padding: 1rem;
  font-size: 1.2rem;
  text-decoration: none;
  display: inline-block;
  color: #fff;
  text-align: center;
  width: 100%;
  min-width: 70px;
  margin: 0.5rem auto;
  box-sizing: border-box;

  /* flexbox */
  order: 0;
  flex: 0 1 auto;
  align-self: auto;
}

@media (min-width: 600px) {
  a {
    width: 18%;
    box-sizing: content-box;
  }
}

a:hover {
  background-color: #000;
}


이 템플릿에는 네비게이션 링크로 구성된 히어로의 이름이 그리드 형태로 배열되어 있다.


  • 컴포넌트의 heroes 배열에 있는 항목을 모두 링크로 만들기 위해 *ngFor를 사용했다.
  • 링크 항목의 스타일은 dashboard.component.css에 작성한다.
  • 아직 링크 항목들은 화면을 전환하지 않는다. 이 내용은 곧 작성할 예정이다.


대시보드 화면의 클래스는 HeroesComponent 클래스와 비슷하다.


  • heroes 프로퍼티를 배열로 선언한다.
  • 생성자를 통해 HeroService를 의존성으로 주입받고 이 객체를 private heroService 프로퍼티에 할당한다.
  • HeroServicegetHeroes 함수는 ngOnInit() 라이프싸이클 후킹 함수에서 호출한다.


이 때 대시보드 화면의 컴포넌트 클래스에서는 HeroServicegetHeroes()로 받은 배열 데이터 중 4개만 추출해서 heroes 프로퍼티에 할당한다.


src/app/dashboard/dashboard.component.ts
getHeroes(): void {
  this.heroService.getHeroes()
    .subscribe(heroes => this.heroes = heroes.slice(1, 5));
}


1) 대시보드 라우팅 규칙 추가하기

대시보드로 화면을 전환하려면 이 컴포넌트를 연결하는 라우팅 규칙이 필요하다.


먼저, app-routing.module.ts 파일에 DashboardComponent를 로드한다.


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


그리고 routes 배열에 DashboardComponent에 해당하는 라우팅 규칙을 추가한다.


src/app/app-routing.module.ts
{ path: 'dashboard', component: DashboardComponent },


2) 기본 라우팅 규칙 추가하기

애플리케이션이 시작되면 브라우저의 주소표시줄은 웹 사이트의 최상위 주소를 가리킨다. 하지만 이 주소에 매칭되는 라우팅 규칙이 없기 때문에 라우터는 페이지를 이동하지 않는다. 그래서 <router-outlet> 아래쪽은 빈 공간으로 남게 된다.


애플리케이션이 실행되면서 대시보드 화면을 자동으로 표시하려면 routes 배열에 다음과 같이 기본 라우팅 규칙을 추가하면 된다.


src/app/app-routing.module.ts
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },


이 라우팅 규칙은 브라우저의 URL이 빈 문자열일 때 '/dashboard' 주소로 이동하도록 설정한 것이다.


이제 브라우저가 갱신되고 나면 라우터는 브라우저 주소를 /dashboard로 변경하면서 DashboardComponent를 바로 표시한다.


3) 애플리케이션 셸에 대시보드 링크 추가하기

사용자는 페이지 위쪽 네비게이션 영역에 있는 링크를 클릭해서 DashboardComponentHeroesComponent로 이동할 수 있어야 한다.


이 기능을 위해 AppComponent 셸의 템플릿에 대시보드로 이동할 수 있는 링크를 추가하자.


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>


브라우저가 갱신되고 나면 링크를 클릭해서 대시보드 화면과 히어로 목록 화면으로 자유롭게 이동할 수 있다.


5. 히어로 상세정보 화면으로 전환하기

HeroDetailComponent는 사용자가 선택한 히어로의 상세정보를 표시하는 컴포넌트이다. 그리고 지금까지 작성한 코드에서 HeroDetailComponentHeroesComponent 아래쪽에 표시된다.


사용자는 이 컴포넌트를 세 가지 방법으로 사용할 수 있어야 한다.


1] 대시보드에서 히어로를 클릭했을 때

2] 히어로 목록에서 히어로를 클릭했을 때

3] 특정 히어로에 해당하는 "딥 링크(deep link)" URL을 브라우저 주소표시줄에 입력했을 때


이번 섹션에서는 HeroesComponent와 별개로 HeroDetailComponent로 직접 네비게이션할 수 있는 방법에 대해 알아보자.


1) HeroesComponent에 포함된 히어로 상세정보 제거하기

사용자가 HeroesComponent에 있는 히어로 아이템을 클릭하면 HeroDetailComponent에 해당하는 주소로 이동하면서 화면에 표시된 컴포넌트를 변경해야 한다. 그리고 이 경우에 히어로 목록은 더이상 화면에 표시되지 않아야 한다.


HeroesComponent 템플릿 파일(heroes/heroes.component.html)을 열고 템플릿 아래쪽에 사용된 <app-hero-detail> 엘리먼트를 제거한다.


그리고나서 히어로 항목을 선택하면 아무일도 일어나지 않는다. 이 에러는 HeroDetailComponent로 라우팅하는 시나리오를 처리한 후에 수정할 것이다.


2) 히어로 상세정보 화면에 대한 라우팅 규칙 추가하기

URL이 ~/detail/11라면 이 URL은 히어로 상세정보 화면에서 id11에 해당하는 히어로의 상세정보를 표시한다는 것으로 이해할 수 있다.


이렇게 구현하기 위해 app-routing.module.ts 파일을 열어서 HeroDetailComponent를 로드한다.


src/app/app-routing.module.ts (HeroDetailComponent 로드하기)
import { HeroDetailComponent } from "./hero-detail/hero-detail.component";


그리고 routes 배열에 히어로 상세정보 화면과 매칭되는 패턴을 라우팅 변수를 사용해서 정의한다.


src/app/app-routing.module.ts
{ path: 'detail/:id', component: HeroDetailComponent },


이렇게 정의하면 히어로의 id에 해당하는 라우팅 변수를 :id로 받겠다는 것을 의미한다.


src/app/app-routing.module.ts (전체 라우팅 규칙)
const routes: Routes = [
  { path: "", redirectTo: "/dashboard", pathMatch: "full" },
  { path: "dashboard", component: DashboardComponent },
  { path: "detail/:id", component: HeroDetailComponent },
  { path: "heroes", component: HeroesComponent },
];


3) DashboardComponent에서 상세정보로 가는 링크

DashboardComponent에 추가한 링크는 아직 동작하지 않는다.


*ngFor로 배열을 순회할 때 할당되는 hero 객체의 id를 활용해서 HeroDetailComponent로 이동하는 라우팅 규칙을 연결해 보자.


src/app/dashboard/dashboard.component.html (hero links)
<a *ngFor="let hero of heroes" routerLink="/detail/{{hero.id}}">
  {{hero.name}}
</a>


이 때 *ngFor로 순회하는 각 링크의 routerLink의 값으로 hero.id를 지정하기 위해 Angular가 제공하는 문자열 바인딩(interpolation binding) 문법을 사용했다.


4) HeroesComponent에서 상세정보로 가는 링크

HeroesComponent에 있는 히어로 아이템은 <li> 엘리먼트로 구성되었기 때문에, 이 엘리먼트에 클릭 이벤트를 바인딩하면 컴포넌트의 onSelect() 메서드를 실행할 수 있었다.


src/app/heroes/heroes.component.html (onSelect가 적용된 리스트)
<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>


이 코드에서 <li>에 적용된 어트리뷰트를 모두 제거하고 *ngFor만 남겨둔다. 그리고 히어로의 id 뱃지와 이름을 표시하는 앵커 엘리먼트에 routerLink 어트리뷰트를 추가한다.


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>


화면에 표시되는 모습을 변경하려면 컴포넌트의 스타일시트 파일(heroes.component.css)을 수정하면 된다. 이 문서 마지막에 있는 최종코드 리뷰를 확인해 보자.


필요없는 코드 제거하기 (생략 가능)

HeroesComponent는 지금도 제대로 동작하지만 이 컴포넌트 클래스에 있는 onSelect() 메서드와 selectedHero 프로퍼티는 더이상 사용되지 않는다.


그래서 클래스 코드를 깔끔하게 유지하려면 사용하지 않는 코드를 제거하는 것이 좋다.


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));
  }
}


6. HeroDetailComponent 라우팅

이전 예제에서는 부모 컴포넌트 HeroesComponent가 자식 컴포넌트 HeroDetailComponenthero 프로퍼티를 바인딩하면 자식 컴포넌트가 이 히어로에 대한 상세정보를 표시했다.


하지만 이제 HeroesComponent는 이런 동작을 하지 않는다. 이제부터는 라우터가 HeroDetailComponent를 생성하는데 이 때 URL에 있는 ~/detail/11과 같은 URL을 활용하도록 수정해 보자.


이제 HeroDetailComponent가 화면에 표시할 히어로의 id는 다음과 같이 가져온다.


  • 컴포넌트를 생성할 때 사용한 라우팅 규칙을 참조한다.
  • 라우팅 규칙에서 id에 해당하는 라우팅 변수를 추출한다.
  • id에 해당되는 히어로 정보는 HeroService를 활용해서 서버에서 가져온다.


다음 코드를 추가한다:


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 서비스를 의존성으로 주입하고 private 프로퍼티로 선언한다:


src/app/hero-detail/hero-detail.component.ts
constructor(
  private route: ActivatedRoute,
  private heroService: HeroService,
  private location: Location
) {}


ActivatedRouteHeroDetailComponent의 인스턴스를 생성하면서 적용한 라우팅 규칙에 대한 정보를 담고 있다. 그래서 이 라우팅 규칙을 참조하면 URL을 통해 컴포넌트로 전달되는 변수를 추출할 수 있다. 화면에 표시할 히어로를 구분할 때도 URL에 포함된 라우팅 변수 id를 사용한다.


컴포넌트에 사용할 히어로 데이터는 HeroService를 사용해서 리모트 서버에서 가져온다.


Location은 브라우저를 제어하기 위해 Angular가 제공하는 서비스이다. 이 서비스는 이전 페이지로 전환하는 예제를 다룰 때 다시 살펴본다.


1) 라우팅 변수 id 추출하기

지금까지 작성한 예제에서는 ngOnInit() 라이프싸이클 후킹 함수에서 HeroServicegetHero() 메서드를 호출한다.


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은 컴포넌트가 생성된 직후에 존재하는 라우팅 규칙에 대한 정보를 담고 있는 객체이다.


그래서 이 객체가 제공하는 paramMap을 사용하면 URL에 존재하는 라우팅 변수를 참조할 수 있다. 지금 작성하고 있는 예제에서는 서버로부터 받아올 히어로의 id에 해당하는 값을 URL에 있는 "id" 키로 참조한다.


라우팅 변수는 언제나 문자열 타입이다. 그래서 라우팅 변수로 전달된 값이 원래 숫자였다면 문자열로 받아온 라우팅 변수에 Number 함수를 사용해서 숫자로 변환할 수 있다.


하지만 브라우저가 갱신되고 난 후에 이 코드는 동작하지 않는다. 왜냐하면 HeroService에 아직 getHero() 메서드가 없기 때문이다. 이 메서드를 추가해 보자.


2) HeroService.getHero() 추가하기

HeroService를 열고 getHeroes() 메서드 뒤에 다음과 같이 getHero() 메서드를 추가한다.


src/app/hero.service.ts (getHero)
getHero(id: number): Observable<Hero> {
  // 지금은 히어로의 `id` 프로퍼티가 항상 존재한다고 간주합니다.
  // 에러를 처리하는 방법은 다음 튜토리얼에 대해 알아봅니다.
  const hero = HEROES.find(h => h.id === id)!;
  this.messageService.add(`HeroService: fetched hero id=${id}`);
  return of(hero);
}


Note

  • id에 사용된 역따옴표(```)는 템플릿 리터럴(template literal)을 표현하는 JavaScript 문법이다.


getHero() 함수도 getHeroes() 함수와 비슷하게 비동기로 동작한다. 그리고 히어로의 목 데이터 하나를 Observable로 반환하기 위해 RxJs가 제공하는 of() 함수를 사용했다.


이렇게 구현하면 나중에 getHero()가 실제 Http 요청을 보내도록 수정하더라도 이 함수를 호출하는 HeroDetailComponent는 영향을 받지 않는다.


3) 동작 확인하기

브라우저가 갱신되고 나면 앱이 다시 동작한다. 그리고 대시보드나 히어로 목록 화면에서 히어로를 한 명 선택하면 이 히어로의 상세정보를 표시하는 화면으로 이동한다.


브라우저 주소표시줄에 localhost:4200/detail/11라는 값을 붙여넣으면 이 때도 마찬가지로 id: 11에 해당하는 "Dr Nice" 히어로의 정보를 표시하는 화면으로 이동할 것이다.


4) 이전 화면으로 돌아가기

히어로 목록이나 대시보드 화면에서 히어로를 선택해서 히어로 상세정보 화면으로 이동했다면, 브라우저의 뒤로 가기 버튼을 눌렀을 때 이전 화면으로 돌아갈 수 있다.


이 기능을 실행하는 버튼이 HeroDetail 화면에 있다면 사용자가 애플리케이션을 사용하기 더 편할 것이다.


컴포넌트 템플릿 맨 아래에 뒤로 가기 버튼을 추가하고 이 버튼을 컴포넌트의 goBack() 메서드와 바인딩한다.


src/app/hero-detail/hero-detail.component.html (뒤로 가기 버튼)
<button (click)="goBack()">go back</button>


그리고 컴포넌트 클래스에 goBack() 메서드를 추가하는데, 브라우저의 히스토리 스택을 활용할 수 있도록 이전에 주입받은 Location 서비스를 사용한다.


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


브라우저가 다시 시작되면 이것 저것 클릭해 보자. 사용자는 화면에 있는 버튼으로 히어로 목록이나 대시보드 화면을 이동할 수 있으며, 이전 화면으로 돌아갈 수도 있다.


7. 최종코드 리뷰

이 문서에서 다룬 코드를 확인해 보자.


1) AppRoutingModule, AppModule, HeroService

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 = [
  { 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 {}
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/dashboard.component";
import { HeroDetailComponent } from "./hero-detail/hero-detail.component";
import { HeroesComponent } from "./heroes/heroes.component";
import { MessagesComponent } from "./messages/messages.component";

import { AppRoutingModule } from "./app-routing.module";

@NgModule({
  imports: [BrowserModule, FormsModule, AppRoutingModule],
  declarations: [
    AppComponent,
    DashboardComponent,
    HeroesComponent,
    HeroDetailComponent,
    MessagesComponent,
  ],
  bootstrap: [AppComponent],
})
export class AppModule {}
import { Injectable } from "@angular/core";

import { Observable, of } from "rxjs";

import { Hero } from "./hero";
import { HEROES } from "./mock-heroes";
import { MessageService } from "./message.service";

@Injectable({ providedIn: "root" })
export class HeroService {
  constructor(private messageService: MessageService) {}

  getHeroes(): Observable<Hero[]> {
    const heroes = of(HEROES);
    this.messageService.add("HeroService: fetched heroes");
    return heroes;
  }

  getHero(id: number): Observable<Hero> {
    // 지금은 히어로의 `id` 프로퍼티가 항상 존재한다고 간주합니다.
    // 에러를 처리하는 방법은 다음 튜토리얼에 대해 알아봅니다.
    const hero = HEROES.find((h) => h.id === id)!;
    this.messageService.add(`HeroService: fetched hero id=${id}`);
    return of(hero);
  }
}


2) AppComponent

<h1>{{title}}</h1>
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>
/* AppComponent에 적용되는 CSS 스타일 */
h1 {
  margin-bottom: 0;
}
nav a {
  padding: 1rem;
  text-decoration: none;
  margin-top: 10px;
  display: inline-block;
  background-color: #e8e8e8;
  color: #3d3d3d;
  border-radius: 4px;
}

nav a:hover {
  color: white;
  background-color: #42545c;
}
nav a.active {
  background-color: black;
}


3) DashboardComponent

<h2>Top Heroes</h2>
<div class="heroes-menu">
  <a *ngFor="let hero of heroes" routerLink="/detail/{{hero.id}}">
    {{hero.name}}
  </a>
</div>
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에 적용되는 CSS styles */

h2 {
  text-align: center;
}

.heroes-menu {
  padding: 0;
  margin: auto;
  max-width: 1000px;

  /* flexbox */
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: space-around;
  align-content: flex-start;
  align-items: flex-start;
}

a {
  background-color: #3f525c;
  border-radius: 2px;
  padding: 1rem;
  font-size: 1.2rem;
  text-decoration: none;
  display: inline-block;
  color: #fff;
  text-align: center;
  width: 100%;
  min-width: 70px;
  margin: 0.5rem auto;
  box-sizing: border-box;

  /* flexbox */
  order: 0;
  flex: 0 1 auto;
  align-self: auto;
}

@media (min-width: 600px) {
  a {
    width: 18%;
    box-sizing: content-box;
  }
}

a:hover {
  background-color: #000;
}


4) HeroesComponent

<h2>My Heroes</h2>
<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>
import { Component, OnInit } from "@angular/core";

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

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

  constructor(private heroService: HeroService) {}

  ngOnInit() {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes().subscribe((heroes) => (this.heroes = heroes));
  }
}
/* HeroesComponent에 적용되는 CSS 스타일 */
.heroes {
  margin: 0 0 2em 0;
  list-style-type: none;
  padding: 0;
  width: 15em;
}
.heroes li {
  position: relative;
  cursor: pointer;
}

.heroes li:hover {
  left: 0.1em;
}

.heroes a {
  color: #333;
  text-decoration: none;
  background-color: #eee;
  margin: 0.5em;
  padding: 0.3em 0;
  height: 1.6em;
  border-radius: 4px;
  display: block;
  width: 100%;
}

.heroes a:hover {
  color: #2c3a41;
  background-color: #e6e6e6;
}

.heroes a:active {
  background-color: #525252;
  color: #fafafa;
}

.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;
  min-width: 16px;
  text-align: right;
  margin-right: 0.8em;
  border-radius: 4px 0 0 4px;
}


5) HeroDetailComponent

<div *ngIf="hero">
  <h2>{{hero.name | uppercase}} Details</h2>
  <div><span>id: </span>{{hero.id}}</div>
  <div>
    <label for="hero-name">Hero name: </label>
    <input id="hero-name" [(ngModel)]="hero.name" placeholder="Hero name" />
  </div>
  <button (click)="goBack()">go back</button>
</div>
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 | undefined;

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

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

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

  goBack(): void {
    this.location.back();
  }
}
/* HeroDetailComponent에 적용되는 CSS 스타일 */
label {
  color: #435960;
  font-weight: bold;
}
input {
  font-size: 1em;
  padding: 0.5rem;
}
button {
  margin-top: 20px;
  background-color: #eee;
  padding: 1rem;
  border-radius: 4px;
  font-size: 1rem;
}
button:hover {
  background-color: #cfd8dc;
}
button:disabled {
  background-color: #eee;
  color: #ccc;
  cursor: auto;
}


8. 정리

이번 튜토리얼을 진행하면서 다음과 같은 내용에 대해 알아봤다.


  • 화면에 표시하는 컴포넌트를 전환하기 위해 Angular 라우터를 추가했다.
  • AppComponent<a> 링크와 <router-outlet>을 추가하면 네비게이션 동작을 실행할 수 있다.
  • 라우터 설정은 AppRoutingModule에 정의한다.
  • 간단한 라우팅 규칙부터 리다이렉트 라우팅 규칙, 라우팅 변수가 있는 라우팅 규칙을 정의해 봤다.
  • 앵커 엘리먼트에 routerLink 디렉티브를 적용했다.
  • 히어로 목록/상세정보 화면은 원래 결합도가 높았지만 라우터를 활용해서 결합도를 낮추도록 리팩토링했다.
  • 히어로 목록 화면에서 사용자가 선택한 히어로의 정보를 히어로 상세정보 화면으로 전달하기 위해 라우터 링크 배열을 활용했다.
  • 여러 컴포넌트에 사용하는 로직을 중복해서 구현하지 않고 HeroService로 옮겨서 재사용할 수 있도록 변경했다.

References