이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.
이 게시물의 소스코드는 Tour of Heroes / Services(@Injectable, OnInit, ngOnInit)에서 이어집니다.
tour-of-heroes.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.
tour-of-heroes.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 tour-of-heroes.git 을 clone 합니다.
- Github에서 소스코드 보기: https://github.com/a-mean-blogger/tour-of-heroes/tree/548f57d8538150dc687167ebe255e923f76c46e1
Angular사이트의 공식 tutorial인 Tour of Heroes의 다섯번째 강의, Routing입니다.
공식 tutorial link : https://angular.io/docs/ts/latest/tutorial/toh-pt5.html
온라인 예제 : https://angular.io/generated/live-examples/toh-pt5/eplnkr.html
우선 강의에 들어가기 앞서 이번 포스트에서 뭘 만들건지 페이지들의 관계도를 보며 설명하겠습니다.
이미지 출처(https://angular.io/docs/ts/latest/tutorial/toh-pt5.html)
우리가 만들 route은 다음과 같습니다.
:
)은 parameter를 뜻합니다. :id자리에 들어오는 값으로 보여줄 hero를 선택합니다.Route과는 별개로 이번 강의부터 ts파일에서 html과 css를 분리합니다.
이번강의에서 처음 등장하게 될 Observable에 대해 알아봅시다.
Observable은 Angular에서 사용되는 객체로(자바스크립트 객체가 아닙니다) 일반적으로 Observable을_리턴하는_함수.subscribe(Observable을_처리하는_함수)
의 형태로 사용됩니다.
Observable을_리턴하는_함수에 의해 정의된 특정조건이 갖춰지면 Observable 생성되고 .subscribe
를 통해 Observable을_처리하는_함수로 전달되어 함수가 실행됩니다.
이벤트.subscribe(이벤트의_처리)
로 생각하시면 이해가 쉬울텐데, Promise를 더 발전시킨 개념으로 Promise와 유사한 점이 많습니다.
실제 코드에서 다시 설명하겠습니다.
이쯤에서 한번 짚고 넘어가는 각 파일들의 역할
CSS 코드는 설명하지 않습니다. 소스코드에서 복사하여 붙여넣기 해주세요.
먼저 app-routing.module.ts부터 살펴봅시다.
// src/app/app-routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { DashboardComponent } from './dashboard.component'; import { HeroesComponent } from './heroes.component'; import { HeroDetailComponent } from './hero-detail.component'; const routes: Routes = [ //2-1 { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, //2-2 { path: 'dashboard', component: DashboardComponent }, //2-3 { path: 'detail/:id', component: HeroDetailComponent }, { path: 'heroes', component: HeroesComponent } ]; @NgModule({ imports: [ RouterModule.forRoot(routes) ], exports: [ RouterModule ] }) export class AppRoutingModule {} //1
1. AppRoutingModule class를 export
하는데 NgModule decorator(@NgModule
)가 붙어있는 것으로 봐서 이 class가 NgModule임을 알 수 있습니다.
2-1. Routes
타입인 const routes에 route을 정의해 줍니다.
2-2.path
에는 경로를 입력하고, redirect인 경우에는redirectTo
에 redirect할 경로를 입력합니다.pathMatch
는 redirect인 경우에 필요한 항목입니다. full을 입력해 줍니다.
2-3.path
에는 경로를 입력하고component
에 보여줄 page의 component를 입력하면 해당 경로에서 해당 component를 보여주게 됩니다.
Angular에서 route은 현재 component의 template에서 <router-outlet></router-outlet>
를 설정된 component로 교체하는 방식입니다. 즉 현재 component는 AppComponent이므로 여기에 <router-outlet></router-outlet>
를 넣어주어야 합니다.
app.component.ts를 살펴봅시다.
// src/app/app.component.ts import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: ` //1 <h1>{{title}}</h1> <nav> <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <router-outlet></router-outlet> `, styleUrls: ['./app.component.css'], //2 }) export class AppComponent { title = 'Tour of Heroes'; }
1. 사이트 제목, 상단메뉴와 <router-outlet></router-outlet>
가 있는 것을 볼 수 있습니다. a
tag에 href가 아니라routerLink
에 이동할 route를 적어줍니다.routerLinkActive
는 해당 route로 이동한 경우 해당 a
의 class에 값을 추가하는 역할을 합니다. 현재 "active"가 들어 있으므로 해당 route로 이동하면 a
의 class에 active가 추가됩니다.
2. css 코드를 분리하기 위해 styles
대신에 styleUrls
가 사용되었습니다. css파일의 위치를 적어주면 해당 css파일이 호출됩니다.
// src/app/app.module.ts 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.component'; import { HeroDetailComponent } from './hero-detail.component'; import { HeroesComponent } from './heroes.component'; import { HeroService } from './hero.service'; import { AppRoutingModule } from './app-routing.module'; @NgModule({ imports: [ BrowserModule, FormsModule, AppRoutingModule //1 ], declarations: [ AppComponent, DashboardComponent, //2 HeroDetailComponent, //2 HeroesComponent //2 ], providers: [ HeroService ], //3 bootstrap: [ AppComponent ] }) export class AppModule { }
1. 사이트에서 사용할 module들은 imports
에 넣습니다.
2. 사이트에서 사용할 component들은 declarations
에 넣습니다.
3. 이전 포스트에서 AppComponent providers
로 들어갔던 HeroServie가 AppModule의 providers
로 옮겨졌습니다. Module의 providers
에 service를 넣으면 해당 service는 각각 class의 providers
에 넣지 않아도 됩니다.
// src/app/hero.service.ts import { Hero } from './hero'; import { HEROES } from './mock-heroes'; import { Injectable } from '@angular/core'; @Injectable() export class HeroService { getHeroes(): Promise<Hero[]> { return Promise.resolve(HEROES); } getHeroesSlowly(): Promise<Hero[]> { return new Promise(resolve => { // Simulate server latency with 2 second delay setTimeout(() => resolve(this.getHeroes()), 2000); }); } getHero(id: number): Promise<Hero> { //1 return this.getHeroes() .then(heroes => heroes.find(hero => hero.id === id)); } }
1. 하나의 hero를 return하는 getHero 함수가 service에 추가되었습니다. 나중에는 모든 CRUD기능이 추가됩니다.
// src/app/dashboard.component.ts import { Component, OnInit } from '@angular/core'; import { Hero } from './hero'; import { HeroService } from './hero.service'; @Component({ selector: 'my-dashboard', templateUrl: './dashboard.component.html', styleUrls: [ './dashboard.component.css' ] }) export class DashboardComponent implements OnInit { heroes: Hero[] = []; constructor(private heroService: HeroService) { } ngOnInit(): void { this.heroService.getHeroes() .then(heroes => this.heroes = heroes.slice(1, 5)); } }
이전 포스트에서 익혔던 OnInit
을 사용해서 component가 준비되면 hero 리스트를 받은 다음 1번부터 5번까지 4개의 hero를 this.heroes에 넣습니다.
<!-- src/app/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를 사용해서 4개의 hero를 보여줍니다. routerLink
에 들어갈 항목이 하나 이상이라면 배열을 사용할 수도 있습니다. 배열 항목 사이에는 자동으로 '/'가 추가됩니다.
// src/app/heroes.component.ts import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; //1 import { Hero } from './hero'; import { HeroService } from './hero.service'; @Component({ selector: 'my-heroes', templateUrl: './heroes.component.html', styleUrls: [ './heroes.component.css' ] }) export class HeroesComponent implements OnInit { heroes: Hero[]; selectedHero: Hero; constructor( private router: Router, //1 private heroService: HeroService) { } getHeroes(): void { this.heroService.getHeroes().then(heroes => this.heroes = heroes); } ngOnInit(): void { this.getHeroes(); } onSelect(hero: Hero): void { this.selectedHero = hero; } gotoDetail(): void { //1 this.router.navigate(['/detail', this.selectedHero.id]); } }
지금까지의 AppComponent에 해당하는 부분이 HeroesComponent로 옮겨왔습니다.
1. 다만 Router
가 추가되었고, gotoDetail 함수에서 router.navigate를 사용해서 route를 변경하는 기능이 추가되었습니다.
<!-- src/app/heroes.component.html --> <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 //1 </h2> <button (click)="gotoDetail()">View Details</button> </div>
hero 리스트를 보여주고, hero를 click하면 하나의 hero를 보여줍니다. View Detail버튼을 click하면 gotoDetail 함수가 호출됩니다.
1. uppercase filter를 사용해서 | 앞의 문자열을 대문자로 변경하고 있습니다. 이처럼 Angular에서는 template에서 연산을 할 때 javascript를 사용하지 않습니다.
//src/app/hero-detail.component.ts import 'rxjs/add/operator/switchMap'; //2 import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; import { Location } from '@angular/common'; import { Hero } from './hero'; import { HeroService } from './hero.service'; @Component({ selector: 'hero-detail', templateUrl: './hero-detail.component.html', styleUrls: [ './hero-detail.component.css' ] }) export class HeroDetailComponent implements OnInit { hero: Hero; constructor( private heroService: HeroService, private route: ActivatedRoute, private location: Location ) {} ngOnInit(): void { //1 this.route.params .switchMap((params: Params) => this.heroService.getHero(+params['id'])) //3 .subscribe(hero => this.hero = hero); } goBack(): void { // 4 this.location.back(); } }
1. 이부분이 Observable을 사용하고 있는 부분입니다. this.route.params은 route의 parameter의 변화를 감지하여 Observable을 생성합니다.
HeroDetailComponent의 route path가 'detail/:id' 이므로 :id에 해당하는 문자열이 parameter가 되고 이 값이 변경되는 경우(처음 생성되는 경우 포함) this.route.params함수에 의해 Params 타입을 가지는 Observable이 생성됩니다.
강의 초반에 Observable은 주로 Observable_리턴.subscribe(함수)
의 형태로 사용된다고 했던 것 기억나시나요? 근데 위의 코드를 보면 형태가 this.route.params.subscribe(...)가 아니라 this.route.params.swithMaps(...).subscribe(...) 입니다. 이 중간에 끼인 .switchMap
은Observable을 advance하게 사용하는 방법으로 만약 해당 이벤트의 처리가 덜 끝난 상태에서 Observable
이 새로운 이벤트를 감지하는 경우(route.params의 경우는 route가 변경되어 parameter가 변경된 경우) 현재 진행중인 이벤트 처리를 중단하고 새로운 이벤트를 처리하게 합니다. .switchMap
역시 Observable
를 리턴하므로 다음에 .subscribe
로 할일을 추가한 형태입니다. .switchMap
은 그냥 사용할 수 있는 함수가 아니라 //2 처럼 따로 RxJS의 switchMaps package를 써야만 사용할 수 있습니다.
.switchMap
을 빼고 그냥 Observable-subscribe으로 작성하면 아래와 같이 작성할 수 있습니다. .switchMap
은 일단 제쳐두고 .subscribe
을 먼저 이해하도록 합시다.
ngOnInit(): void { this.route.params.subscribe((params: Params) => { this.heroService.getHero(+params['id']) //3 .then(hero => this.hero = hero); }); }
route에서 paramater가 변경되면, heroservice의 getHero함수에 'id' route parameter를 전달하여 호출합니다.
3. params['id']의 'id'는 물론 'detail/:id' 의 :id에서 콜론(:)을 뺀 것입니다. 만약 이 명칭을 바꾸고 싶다면 양쪽 다 동일한 것으로 바꾸어 줘야 합니다. route parameter는 항상 문자열로 전달되기 때문에 params['id'] 앞에 + 기호를 써서 문자열을 숫자로 바꾸었습니다.
4. goBack 함수는 Location 객체를 사용해서 브라우저의 이전페이지로 이동시키는 함수입니다.
<!-- src/app/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>
이전 포스트의 HeroDetailComponent에 있던 detail 부분이 여기로 옮겨왔습니다. Back버튼만 추가되었습니다.
내용 중에 이해가 잘 안되는 부분이 있으면 일단은 형태를 익힌 후 넘어가시기 바랍니다. 물론 꼭 이해를 하고 싶으신 분은 답글로 질문을 남겨 주시면 답변해 드립니다.
지금까지는 hero data를 파일에서 읽어왔는데, 다음 포스트에서 hero data를 REST API에서 불러오는 방법에 대해 알아보겠습니다.
댓글
이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기