7. 네비게이션 추가하기
히어로들의 여행 앱에 새로운 요구사항이 생겼다:
- 대시보드 화면을 추가해야 한다.
- 히어로 목록 화면과 대시보드 화면을 전환하는 기능이 필요하다.
- 사용자가 화면에서 히어로 이름을 클릭하면 선택된 히어로의 상세정보 화면으로 이동해야 한다.
- 사용자가 이메일로 받은 딥 링크(deep link)를 클릭하면 해당 히어로의 상세정보 화면을 바로 표시해야 한다.
그래서 최종 결과물은 다음과 같이 3개의 화면을 이동하면서 동작해야 한다:
1. AppRoutingModule
생성하기
Angular에서는 최상위 모듈과 같은 계층에 별개의 모듈을 두고 이 모듈에 애플리케이션 최상위 라우팅 모듈을 정의하는 방법을 권장한다. AppModule
은 이렇게 정의한 라우팅 설정을 로드해서 사용하기만 하면 된다.
일반적으로 애플리케이션 최상위 라우팅을 담당하는 모듈의 클래스 이름은 AppRoutingModule
이라고 정의하며 src/app
폴더에 app-routing.module.ts
파일로 생성한다.
Angular CLI로 다음 명령을 실행해서 라우팅 모듈을 만들어 보자.
Note
--flat
옵션을 사용하면 새로운 폴더를 만들지 않고src/app
폴더에 파일을 생성한다.--module=app
옵션을 사용하면 Angular CLI가 이 라우팅 모듈을AppModule
의imports
배열에 자동으로 추가한다.
이 명령을 실행해서 만든 파일의 내용은 다음과 같다:
import { NgModule } from "@angular/core";
import { CommonModule } from "@angular/common";
@NgModule({
imports: [CommonModule],
declarations: [],
})
export class AppRoutingModule {}
이 내용을 다음과 같이 수정한다.
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
파일에 RouterModule
과 Routes
심볼을 로드한다. 그리고 라우팅 규칙에 따라 이동할 HeroesComponent
를 로드한다.
CommonModule
을 로드했던 부분이나 declarations
배열은 필요없기 때문에 AppRoutingModule
에서 제거했다. 변경된 나머지 부분에 대해서는 다음 섹션부터 자세하게 알아보자.
1) 라우팅 규칙(Routes
)
새롭게 만든 파일에서는 라우팅 규칙을 정의하는 부분도 변경되었다. 라우팅 규칙은 사용자가 링크를 클릭하거나 브라우저 주소표시줄에 URL을 직접 입력했을 때 라우터가 어떤 화면을 표시할지 정의한 것이다.
이전 섹션에서 로드했던 HeroesComponent
를 라우팅 규칙으로 등록하려면 routes
배열을 다음과 같이 작성하면 된다:
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
인자를 넣어서 실행한 결과를 지정한다.
Note
- 애플리케이션 최상위 계층에 존재하는 라우터를 설정할 때는
forRoot()
메서드를 사용한다. forRoot()
메서드를 사용하면 라우팅과 관련된 서비스 프로바이더와 디렉티브를 애플리케이션에 제공할 수 있으며, 브라우저에서 변경되는 URL을 감지할 수 있다.
그리고 앱에서도 RouterModule
을 사용할 수 있도록 AppRoutingModule
의 exports
배열을 다음과 같이 지정한다.
2. 라우팅 영역(RouterOutlet
) 추가하기
AppComponent
템플릿을 열어서 <app-heroes>
엘리먼트를 <router-outlet>
엘리먼트로 변경한다.
<h1>{{title}}</h1>
<router-outlet></router-outlet>
<app-messages></app-messages>
AppComponent
템플릿에는 더이상 <app-heroes>
컴포넌트가 존재하지 않는다. 이 컴포넌트는 이제 사용자의 네비게이션의 의해 화면에 표시된다.
<router-outlet>
은 라우팅된 화면이 표시될 위치를 지정하는 엘리먼트이다.
Note
RouterOutlet
디렉티브는AppComponent
에서도 사용할 수 있다.- 왜냐하면
AppModule
이 로드하고 있는AppRoutingModule
이RouterModule
을 외부로 공개하고 있기 때문이다. - 이 동작은 프로젝트를 생성하는
ng generate
명령을 실행하면서--module=app
플래그를 지정했기 때문에 자동으로 추가된 것이다. app-routing.module.ts
파일을 직접 생성했거나 Angular CLI 외의 툴을 사용했다면app.module.ts
파일에서AppRoutingModule
을 로드하고NgModule
의imports
배열에 이 라우팅 모듈을 추가하면 된다.
동작 확인하기
Angular CLI로 다음 명령을 실행해서 애플리케이션을 실행한다.
브라우저가 갱신되면 애플리케이션 제목은 표시되지만 히어로의 목록은 표시되지 않는다.
이 때 브라우저의 주소표시줄을 확인해 보자. URL은 /
로 끝난다. HeroesComponent
는 라우팅 경로 /heroes
에 연결되어 있기 때문에 이 주소에서 히어로 목록이 표시되지 않는 것이다.
브라우저 주소표시줄의 URL을 /heroes
로 변경해 보자. 그러면 이전과 같이 히어로의 목록이 표시될 것이다.
3. 네비게이션 링크(routerLink
) 추가하기
사용자가 브라우저 주소표시줄에 원하는 URL을 입력해야만 하는 것은 좋은 방법이 아니다. 이 방법보다는 네비게이션을 실행하는 링크를 클릭하는 방법이 더 편할 것이다.
이번에는 앵커(<a>
) 엘리먼트를 추가하고, 사용자가 이 엘리먼트를 클릭했을 때 HeroesComponent
로 이동하도록 해보자. AppComponent
의 템플릿을 다음과 같이 수정한다.
<h1>{{title}}</h1>
<nav>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>
routerLink
는 RouterModule
이 제공하는 RouterLink
디렉티브이며, 사용자가 이 디렉티브가 적용된 엘리먼트를 클릭하면 네비게이션을 실행한다. 그리고 routerLink
어트리뷰트의 값은 "/heroes"
로 할당되었는데, 이 문자열은 HeroesComponent
에 해당하는 라우팅 경로를 의미한다.
이제 브라우저가 갱신되면 애플리케이션 제목과 히어로 목록으로 가는 링크가 표시되지만 히어로의 목록은 여전히 표시되지 않는다.
링크를 클릭해 보자. 주소표시줄의 URL이 /heroes
로 바뀌면서 히어로 목록이 표시될 것이다.
app.component.css
파일에 CSS 스타일을 작성하면 네비게이션 링크를 더 보기 좋게 표시할 수 있다. 이 내용은 최종코드 리뷰에서 확인할 수 있다.
4. 대시보드 화면 추가하기
라우터를 사용하면 여러 화면을 전환하기도 쉽다. 아직까지는 히어로 목록을 표시하는 화면만 있지만 이제 다른 화면을 추가해 보자.
Angular CLI로 다음 명령을 실행해서 DashboardComponent
를 생성한다.
그러면 DashboardComponent
를 구성하는 파일이 생성되면서 이 컴포넌트가 AppModule
에 자동으로 등록된다.
이 컴포넌트의 내용을 다음과 같이 수정한다:
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
프로퍼티에 할당한다. HeroService
의getHeroes
함수는ngOnInit()
라이프싸이클 후킹 함수에서 호출한다.
이 때 대시보드 화면의 컴포넌트 클래스에서는 HeroService
의 getHeroes()
로 받은 배열 데이터 중 4개만 추출해서 heroes
프로퍼티에 할당한다.
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
}
1) 대시보드 라우팅 규칙 추가하기
대시보드로 화면을 전환하려면 이 컴포넌트를 연결하는 라우팅 규칙이 필요하다.
먼저, app-routing.module.ts
파일에 DashboardComponent
를 로드한다.
import { DashboardComponent } from "./dashboard/dashboard.component";
그리고 routes
배열에 DashboardComponent
에 해당하는 라우팅 규칙을 추가한다.
2) 기본 라우팅 규칙 추가하기
애플리케이션이 시작되면 브라우저의 주소표시줄은 웹 사이트의 최상위 주소를 가리킨다. 하지만 이 주소에 매칭되는 라우팅 규칙이 없기 때문에 라우터는 페이지를 이동하지 않는다. 그래서 <router-outlet>
아래쪽은 빈 공간으로 남게 된다.
애플리케이션이 실행되면서 대시보드 화면을 자동으로 표시하려면 routes
배열에 다음과 같이 기본 라우팅 규칙을 추가하면 된다.
이 라우팅 규칙은 브라우저의 URL이 빈 문자열일 때 '/dashboard'
주소로 이동하도록 설정한 것이다.
이제 브라우저가 갱신되고 나면 라우터는 브라우저 주소를 /dashboard
로 변경하면서 DashboardComponent
를 바로 표시한다.
3) 애플리케이션 셸에 대시보드 링크 추가하기
사용자는 페이지 위쪽 네비게이션 영역에 있는 링크를 클릭해서 DashboardComponent
나 HeroesComponent
로 이동할 수 있어야 한다.
이 기능을 위해 AppComponent
셸의 템플릿에 대시보드로 이동할 수 있는 링크를 추가하자.
<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
는 사용자가 선택한 히어로의 상세정보를 표시하는 컴포넌트이다. 그리고 지금까지 작성한 코드에서 HeroDetailComponent
는 HeroesComponent
아래쪽에 표시된다.
사용자는 이 컴포넌트를 세 가지 방법으로 사용할 수 있어야 한다.
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은 히어로 상세정보 화면에서 id
가 11
에 해당하는 히어로의 상세정보를 표시한다는 것으로 이해할 수 있다.
이렇게 구현하기 위해 app-routing.module.ts
파일을 열어서 HeroDetailComponent
를 로드한다.
import { HeroDetailComponent } from "./hero-detail/hero-detail.component";
그리고 routes
배열에 히어로 상세정보 화면과 매칭되는 패턴을 라우팅 변수를 사용해서 정의한다.
이렇게 정의하면 히어로의 id
에 해당하는 라우팅 변수를 :id
로 받겠다는 것을 의미한다.
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
로 이동하는 라우팅 규칙을 연결해 보자.
<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()
메서드를 실행할 수 있었다.
<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
어트리뷰트를 추가한다.
<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
프로퍼티는 더이상 사용되지 않는다.
그래서 클래스 코드를 깔끔하게 유지하려면 사용하지 않는 코드를 제거하는 것이 좋다.
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
가 자식 컴포넌트 HeroDetailComponent
의 hero
프로퍼티를 바인딩하면 자식 컴포넌트가 이 히어로에 대한 상세정보를 표시했다.
하지만 이제 HeroesComponent
는 이런 동작을 하지 않는다. 이제부터는 라우터가 HeroDetailComponent
를 생성하는데 이 때 URL에 있는 ~/detail/11
과 같은 URL을 활용하도록 수정해 보자.
이제 HeroDetailComponent
가 화면에 표시할 히어로의 id
는 다음과 같이 가져온다.
- 컴포넌트를 생성할 때 사용한 라우팅 규칙을 참조한다.
- 라우팅 규칙에서
id
에 해당하는 라우팅 변수를 추출한다. id
에 해당되는 히어로 정보는HeroService
를 활용해서 서버에서 가져온다.
다음 코드를 추가한다:
import { ActivatedRoute } from "@angular/router";
import { Location } from "@angular/common";
import { HeroService } from "../hero.service";
먼저, 컴포넌트 생성자로 ActivatedRoute
, HeroService
, Location
서비스를 의존성으로 주입하고 private
프로퍼티로 선언한다:
constructor(
private route: ActivatedRoute,
private heroService: HeroService,
private location: Location
) {}
ActivatedRoute
는 HeroDetailComponent
의 인스턴스를 생성하면서 적용한 라우팅 규칙에 대한 정보를 담고 있다. 그래서 이 라우팅 규칙을 참조하면 URL을 통해 컴포넌트로 전달되는 변수를 추출할 수 있다. 화면에 표시할 히어로를 구분할 때도 URL에 포함된 라우팅 변수 id
를 사용한다.
컴포넌트에 사용할 히어로 데이터는 HeroService
를 사용해서 리모트 서버에서 가져온다.
Location
은 브라우저를 제어하기 위해 Angular가 제공하는 서비스이다. 이 서비스는 이전 페이지로 전환하는 예제를 다룰 때 다시 살펴본다.
1) 라우팅 변수 id
추출하기
지금까지 작성한 예제에서는 ngOnInit()
라이프싸이클 후킹 함수에서 HeroService
의 getHero()
메서드를 호출한다.
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()
메서드를 추가한다.
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()
메서드와 바인딩한다.
<button (click)="goBack()">go back</button>
그리고 컴포넌트 클래스에 goBack()
메서드를 추가하는데, 브라우저의 히스토리 스택을 활용할 수 있도록 이전에 주입받은 Location
서비스를 사용한다.
브라우저가 다시 시작되면 이것 저것 클릭해 보자. 사용자는 화면에 있는 버튼으로 히어로 목록이나 대시보드 화면을 이동할 수 있으며, 이전 화면으로 돌아갈 수도 있다.
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
/* 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
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
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
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
로 옮겨서 재사용할 수 있도록 변경했다.