-
9장) Angular 2 -HeroEditor- (5-2)Angular 2017. 2. 4. 01:20
오랜만에 글을 씁니다.
그동안 이것저것 준비하고 바빠서 이제야 시간이 생겼네요 ㅠ
늦은만큼 이제부터 점점 속도를 내서 쓰겠습니다.
혹시나 기다리신 분이 있으셨다면 죄송합니다 ㅜ
저번 시간에 이어서 Routing 부분을 마무리 짓겠습니다.
8장에서 dashboard 를 추가하고 메인화면에서 dashboard 와 Heroes 목록을 이동하는 링크를
추가 하였습니다.
이번엔 여기서 HeroesDetail 컴포넌트를 분리하려 합니다.
Dashboard 에서 영웅을 클릭해서 영웅상세화면 으로 가고 싶을땐 어떻게 해야 할까요?
영웅목록 컴포넌트의 영웅 데이터를 가지고와서 비교해야 할까요?
그럼 URL은 어떻게 구성되야 할까요?
추가로 현재 HeroesDetail 컴포넌트는 Heroes 목록 컴포넌트 밑에 붙어 있습니다.
그럼 영웅 상세화면을 볼려면 무조건 영웅 목록을 같이 봐야 할까요?
이런문제를 해결하기 위해 HeroesDetail 컴포넌트는 독립적인 화면으로 구성하고
영웅의 id 에 따른 URL 로 이동하도록 만들려 합니다.
그럼 먼저 경로를 추가 해 봅시다.
app/app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; import { HeroService } from './hero.service'; import { HeroesComponent } from './heroes/heroes.component'; import { DashboardComponent } from './dashboard/dashboard.component'; @NgModule({ declarations: [ AppComponent, HeroDetailComponent, HeroesComponent, DashboardComponent ], imports: [ BrowserModule, FormsModule, HttpModule, RouterModule.forRoot([ { path : 'heroes', component : HeroesComponent }, { path : 'dashboard', component : DashboardComponent }, { path:'', redirectTo : '/dashboard', pathMatch : 'full' }, { path: 'detail/:id', component : HeroDetailComponent } ]) ], providers: [ HeroService ], bootstrap: [AppComponent] }) export class AppModule { }
path 로 detail/:id 가 추가되었습니다.
/:id 가 어떤건지 예상이 갈 겁니다. 당연히 영웅의 id 값이 들어오고
결국 주소는 http://localhost:4200/detail/11 이런식으로 들어가게 됩니다.
자 이젠 HeroDetail 컴포넌트가 변경되어야 합니다.
기존의 부모 컴포넌트로 부터 영웅 데이터를 받는 것이 아닌 URL 의 param 으로 영웅 id 를 받아서
해당 id 의 영웅을 보여줘야 할 겁니다.
변경 해 봅시다.
app/hero-detail/hero-detail.component.ts
import { Component, OnInit, Input } from '@angular/core'; import { ActivatedRoute , Params } from '@angular/router'; import { Location } from '@angular/common'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; import 'rxjs/add/operator/switchMap'; @Component({ selector: 'app-hero-detail', templateUrl: './hero-detail.component.html', styleUrls: ['./hero-detail.component.css'] }) export class HeroDetailComponent implements OnInit { @Input() hero :Hero ; constructor( private _heroService : HeroService, private _route : ActivatedRoute, private _location : Location ) { } ngOnInit() : void { this._route.params .switchMap((params: Params) => this._heroService.getHero(+params['id'])) .subscribe(hero => this.hero = hero); } }
(현재 heroService 에 getHero 를 만들지 않았기 때문에 에러가 나는 건 당연합니다.)
뭔가 많이 추가되었습니다.
차근차근 살펴보면
Import 부분에서는
ActivatedRoute 는 현재 경로에 대한 정보를 얻을 수 있는 모듈 입니다.
Location 은 경로와 관련된 다양한 함수를 사용할 수 있는 모듈입니다.
그리고 HeroServcie 가 추가 되었습니다.
그리고 rxjs 의 SwitchMap 을 사용하기위해 import 해둔 부분이 보입니다.
로직부분을 보면 생성자에 heroService 와 ActivatedRoute, 그리고 location 에 관한 서비스를
사용하도록 명시 해두고, ngOnInit 부분에서는 Rxjs 를 사용하여 heroService 의 getHero 메소드를
사용하여 얻어온 데이터를 현재 Hero 변수에 삽입하고 있습니다.
여기서 중요하게 볼 부분은 ngOnInit 부분입니다.
설명을 하자면 this._route.params 이 부분은 Angular 에서 만든 ActivatedRoute 모듈의
params 기능을 사용하여 현재 경로의 매개변수를 탐색하여 Params 형식의 Observable
을 리턴하는 부분입니다.
Observable 이 뭐나구요? 이 부분에 관해서는 Rxjs 를 알아야 합니다.
Angular 2 는 Rxjs 를 적극 도입하고 있는데, Rxjs 는 기존의 절차형 프로그래밍 방식에서
반응형 프로그래밍 방식으로 바꿔주는 라이브러리 입니다.
간단하게 설명해서
이 부분에 대해 인터넷에서 가장많은 예제로 나와있는 건 바로 Excel 입니다.
엑셀에서 C 셀 에 A셀+B셀 이라고 공식을 적어두면 C 에서는 A와 B를 합친 값이 나오게 됩니다.
거기에 A 나 B의 값을 바꾸면 C는 자동으로 합계를 계속 출력하게 되구요.
반응형 프로그래밍은 엑셀의 예처럼 A+B=C 라는 데이터 흐름을 만들고
A , B 를 관찰해서 해당 값이 바뀌면 C에게 알려주게 되고, 변경된 A , B 의 값은
데이터 흐름을 통해 다시 C의 결과로 나타나게 됩니다.
내부에서 이벤트를 발생시켜 외부로 방출하는 것이 아닌 , 미리 데이터 스트림을 만들어두고
외부에서 반응이 생기면 해당 데이터 스트림을 따라서 결과가 나오게 되는 겁니다.
Rxjs 에 관련된 부분은 따로 정리해서 올려두겠습니다.
다시 코드로 돌아가서 이 데이터 흐름을 만들기위해 필요한건 관찰대상과 관찰자 입니다.
현재 코드에서 this._route.params 에서 ActivatedRoute 의 params 는 Observable 을 리턴하다고
했습니다. 바로 이 Observable 이 관찰대상이 됩니다.
그럼 Observable<Params> 가 리턴되기 때문에 Params 형식 , 즉 매개변수 형식의 관찰대상이
리턴되었고 , 이는 현재 경로로 들어오는 매개변수를 관찰대상으로 보는 겁니다.
이어서 .switchMap 은 현재 관찰대상의 값을 가지고, 새로운 공식에 적용시키는 Rxjs 의
함수입니다. 위 코드를 살펴보면 관찰대상이 된 매개변수를 가지고 heroService 의 getHero() 에
적용시키는 것을 볼 수 있습니다.
+params['id'] 는 매개변수 중 id 매개변수를 뜻 하는 것 입니다.
마지막으로 subscribe() 는 관찰자를 뜻 합니다.
해당 데이터 흐름을 관찰해서 결과를 도출해내는 함수가 되며 데이터흐름을 따라 마지막으로 나온
데이터 즉 , 여기서는 getHero 의 결과가 되겠네요, 해당 Hero 데이터를 현재 컴포넌트의 Hero 변수에
넣게 됩니다. Observable => 데이터 흐름 => Subscribe() 이런 식 입니다.
이어서 HeroService 에 getHero 를 만들러 갑시다.
app/hero.service.ts
import { Injectable } from '@angular/core'; import { Hero } from './hero'; import { HEROES } from './mock-heroes'; @Injectable() export class HeroService { constructor() { } getHero(id: number) : Promise
{ return this.getHeroes() .then(heroes => heroes.find(hero => hero.id === id)); } getHeroes() : Promise { return Promise.resolve(HEROES); } } getHero 를 만들었습니다.
id 를 매개변수로 받고 getHeroes() 를 이용하여 받은 영웅목록 중 id 가 일치하는 영웅을 리턴하도록 하였습니다.
그런데 구글에서 또 하나의 버튼을 추가 하려 합니다.
바로 뒤로가기 버튼입니다. heroDetail 컴포넌트에 추가하여 사용자의 편의성을 높이려 한다는데
이럴꺼면 아까 추가하지 , 왜이리 왔다갔다 하게 만드는지..ㅠ
먼저 heroDetail 컴포넌트에 뒤로가기 버튼을 추가합니다.
app/hero-detail/hero-detail.component.html
<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>
그리고 goBack() 함수를 추가합니다.
app/hero-detail/hero-detail.component.ts
import { Component, OnInit, Input } from '@angular/core'; import { ActivatedRoute , Params } from '@angular/router'; import { Location } from '@angular/common'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; import 'rxjs/add/operator/switchMap'; @Component({ selector: 'app-hero-detail', templateUrl: './hero-detail.component.html', styleUrls: ['./hero-detail.component.css'] }) export class HeroDetailComponent implements OnInit { @Input() hero :Hero ; constructor( private _heroService : HeroService, private _route : ActivatedRoute, private _location : Location ) { } goBack() : void { this._location.back(); } ngOnInit() : void { this._route.params .switchMap((params: Params) => this._heroService.getHero(+params['id'])) .subscribe(hero => this.hero = hero); } }
자 이제 이어서 dashBoard 에서 영웅을 클릭하면 바로 상세보기 페이지로 가도록 만들어야 합니다.
그러기 위해서는 현재 div 로 되어있는 부분을 a 태그로 바꿔야 합니다.
바꿔봅시다 !
app/dashboard/dashboard.component.html
<h3>Top Heroes</h3> <div class="grid grid-pad"> <a *ngFor="let hero of heroes" [routerLink]="['/detail', hero.id]" class="col-1-4"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </a> </div>
*ngFor 가 들어가는 부분을 a 태그로 변경하였습니다.
여기서 routerLink 부분을 보면 경로와 매개변수가 배열로 들어간 것을 확인 할 수 있습니다.
자 이제 저장하고 결과를 확인 해 봅시다.
a 태그로 인해 영웅이름의 링크를 클릭 할 수 있고
클릭하면 상세화면으로 잘 이동 되는군요.
이어서 해야 할 작업은 현재 app.module.ts 에서 지정한 Route 를 분리 해내는 겁니다.
현재로서는 경로가 4개 밖에 없지만, 실제 서비스를 만들때는 엄청난 수의 경로를 만들 것이고,
더불어 권한관리를 하며 경로에 대한 접근을 제한 할 수도 있습니다.
이를 효율적으로 관리하기 위해 라우팅을 위한 새로운 모듈을 만들 것 입니다.
app 폴더에 app.routing.module.ts 파일을 만듭시다.
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 = [ { 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 {}
어떤 방식인지 살펴봅시다.
일단 route 에 관련된 부분을 변수로 처리 했습니다.
이렇게 하면 나중에 export 를 할 수 있으며 , 새로운 라우팅 모듈이 추가 되었을때도 모듈패턴을
명확히 할 수 있습니다.
app.module.ts 에서 사용했던 RouterModule.forRoot(routes) 를 import 했습니다.
그리고 후에 경로접근을 제어 할 providers 를 추가 할 수 있습니다.
라우팅 파일을 만들었으니 이젠
app.module.ts 파일을 손 봅시다.
app/app.module.ts
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpModule } from '@angular/http'; import { RouterModule } from '@angular/router'; import { AppComponent } from './app.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; import { HeroService } from './hero.service'; import { HeroesComponent } from './heroes/heroes.component'; import { DashboardComponent } from './dashboard/dashboard.component'; import { AppRoutingModule } from './app.routing.module'; @NgModule({ declarations: [ AppComponent, HeroDetailComponent, HeroesComponent, DashboardComponent ], imports: [ BrowserModule, FormsModule, HttpModule, AppRoutingModule ], providers: [ HeroService ], bootstrap: [AppComponent] }) export class AppModule { }
app.module 파일이 한결 날씬해 졌습니다.
아까 만든 라우팅 파일을 import 하고 app.module 에서는 해당 라우팅 파일을 imports 에 추가만
해주었습니다.
자 이제 구글에서 또 어떤 숙제를 줬을까요?
네 이번에는 영웅목록 페이지 에서 미니 상세보기를 만들고 싶답니다.
영웅목록에서 영웅을 클릭하면 미니 상세보기 화면이 나오고 해당 미니 상세보기 화면에서
버튼을 클릭하여 상세 화면으로 가는 방식을 원합니다.
자 그럼 얼른 추가 해 봅시다.
app/heroes/heroes.component.ts
<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>
기존에 있던 app-hero-detail 태그를 삭제하고 새로운 미니 상세보기 화면을 넣었습니다.
여기서 새롭게 사용된 구문이 있는데 영웅의 이름을 대문자로 표시하기 위해서
pipe 기능을 사용하였습니다.
{{ selectdHero.name | uppercase }} 가 그 부분입니다.
자 이제 마지막으로 버튼을 클릭하면 영웅 상세보기로 가도록 gotoDetail() 함수를 작성합시다.
app/heroes/heroes.component.ts
import { Component , OnInit} from '@angular/core'; import { Router } from '@angular/router'; 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{ constructor( private heroservice : HeroService, private _router : Router ){} title = 'Tour of Heroes'; heroes : Hero[]; selectedHero: Hero; onSelect(hero:Hero) : void { this.selectedHero = hero; } gotoDetail(): void { this._router.navigate(['/detail', this.selectedHero.id]); } ngOnInit() : void { this.heroservice.getHeroes() .then(result => this.heroes = result); } }
Router 를 import 하고 gotoDetail() 을 추가 했습니다.
Router 의 navigate 를 이용하여 이전 [routerLink] 에서 했던 것 처럼 경로와 매개 변수를
넘겼습니다.
자 이제 잘 작동하는지 확인 해 봅시다.
잘 작동합니다.
자 이제 라우팅 부분은 끝입니다 ? 라고 하고 싶지만 구글은 그냥 넘어가지 않습니다.
스타일링을 해야 됩니다.
구글에서 만들어 준 css 를 사용합시다.
app/hero-detail/hero-detail.component.css
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; }
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: #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; } }
app/app.component.css
.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.selected:hover { background-color: #BBD8DC !important; color: white; } .heroes li:hover { color: #607D8B; background-color: #DDD; left: .1em; } .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; } 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; }
그리고 angular 는 해당 링크를 클릭했을때 클릭한 링크태그에 클래스를 추가 해주는routerLinkActive 지시자가 존재합니다.app.component 의 heroes 와 dashboard 링크에 추가 해줍시다.app/app.component.html<h1>{{title}}</h1> <nav> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a> </nav> <router-outlet></router-outlet>
routerLinkActive 를 확인하기 위해 해당 링크를 클릭하고 active 라는 class 가 추가
되었는지 봅시다.
링크를 클릭하면 해당 링크에 class="active" 가 추가 된 것을 확인 할 수 있습니다.
그리고 당연히 한쪽 링크를 클릭하면 반대쪽 링크의 active 는 사라집니다.
이제 스타일링도 끝났으니 최종적으로 확인 해 봅시다.
이쁘게 잘 나오는 군요.
드디어 라우팅 부분도 끝났습니다.
이제 http 부분만 남았네요.
이거 이러다 다 적기 전에 angular 4 가 먼저 나올 수도 있겠다 생각됩니다....
(참고로 이번에 빠진 부분이 있습니다. 공식홈페이지를 보시며 하던 분은 아실수도...)
'Angular' 카테고리의 다른 글
Angular 2 ~ 4 개발 스타일 가이드 (1) 2017.10.17 Angular 4 업데이트 변경점 정리 (1) 2017.03.26 8장) Angular 2 -HeroEditor- (5-1) (2) 2017.01.14 7장) Angular 2 -HeroEditor- (4) (5) 2017.01.12 6장) Angular 2 -HeroEditor- (3) (1) 2017.01.10