[ Angular 2: Routes ] 네비게이션을 위한 라우트

네비게이션을 담당하는 라우트 기능을 구현해본다.


Angular 2 Routes Basic Setting

처음에는 할 때마다 Route 설정이 헤깔린다. 코더가 누구냐에 따라 코딩패턴이 조금씩 다르기 때문에 이것저것 참고하다보면 자꾸만 더 헤깔리게 된다. 일단, 한가지 패턴을 완전히 습득해서 외우고 나면, 약간 변형된 코딩들도 이해가 쉬워진다.

먼저, 새 프로젝트를 만든다.

$ ng new routes-study --prefix rt --directory routes-study

routes-study/src/app/components 폴더를 생성한다.

routes-study/src/app/components 폴더 위치에서 콘솔창에 다음과 같이 입력해서, user.component.ts 와 home.component.ts 를 생성한다. 테스트를 위한 Dummy Component 들이다.

$ ng generate component user --flat -it -is
$ ng generate component home --flat -it -is

//또는
$ ng g c user --flat -it -is
$ ng g c home --flat -it -is

routes-study/src/app/app.routes.ts 를 수동으로 생성하고, 다음과 같이 코딩한다.

import { Routes,RouterModule } from '@angular/router';
import { HomeComponent } from './components/home.component';
import { UserComponent } from './components/user.component';

const APP_ROUTES:Routes = [
    {path:'user',component:UserComponent},
    {path:'',component:HomeComponent}
];

export const routing = RouterModule.forRoot(APP_ROUTES);

app.component.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';
import { HomeComponent } from './components/home.component';
import { UserComponent } from './components/user.component';
import { routing } from './app.routes';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    UserComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    routing
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.component.html

<h1>
  {{title}}
</h1>
<a href="/">HomeComponent</a>
<a href="/user">UserComponent</a>
<router-outlet></router-outlet>

Angular 2 Routes 구현 시에, router-outlet tag 위치에는 주소에 따라 다른 컴포넌트가 표시된다. 라우트 기능을 구현할 때는 반드시 router-outlet tag가 어딘가에 위치해있어야 한다. 위 app.component.html 에서는 href 속성를 사용해서 링크를 걸었다. 하지만, 이렇게 하면, 페이지 전체가 다시 로딩되면서 부자연스럽게 다른 페이지로 넘어간다. 대신에 routerLink를 사용하면 부드러운 페이지 전환이 가능하다. app.component.html을 다음과 같이 수정해본다.

<h1>
  {{title}}
</h1>
<a [routerLink]="['']">HomeComponent</a>
<a [routerLink]="['user']">UserComponent</a>
<router-outlet></router-outlet>

브라우저에서 어떻게 작동하는지 확인해보자.

$ ng serve

//브라우저 주소창에 localhost:4200 을 입력한다.

Absolute vs Relative Path

[routerLink]에서는 절대 패스와 상대 패스를 사용할 수 있다.

//Relative Path
<a [routerLink]="['user']">User</a>
//Absolute Path
<a [routerLink]="['/user']">User</a>

UserComponent 는 AppComponent에 종속된 Nested Component이다. 그런데, 이 종속된 UserComponent의 Path가 ‘user’로 설정되어있다고 치자. UserComponent에서 다시 ‘user’라는  Path로 routerLink를 걸면, 어떻게 될까?

src/app/components/user.component.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'rt-user',
  template: `
    <p>
      user Works!
    </p>
    <a [routerLink]="user">User</a>
  `,
  styles: []
})
export class UserComponent implements OnInit {

  constructor() { }

  ngOnInit() {
  }

}

이와 같이 상대패스를 사용하면, 결국 localhost:4200/user/user 를 가르키는 주소가 된다. 브라우저 창에서 링크를 클릭해보면, 아무 변화가 없다. f12를 눌러 콘솔창을 열어보면 /user/user라는 주소가 없다는 오류가 뜬다.

절대 패스를 사용하면, 정상적으로 localhost:4200/user 를 가르키게 된다.

    <a [routerLink]="/user">User</a>

Nested Component 안에서 routerLink를 사용할 때는 상대/절대 패스를 잘 구분하여야 한다.

또 하나의 예로 UserComponent에서 다음과 같은 링크를 만들면, 한 단계 상위 주소인 localhost:4200 을 가르키게된다. 상대 패스인 셈이다.

    <a [routerLink]="../">User</a>

Imperitive Routing

종속 Component 안에서

user.component.ts

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'rt-user',
  template: `
    <p>
      user Works!
    </p>
    <a [routerLink]="user">User</a>
    <button (click)="onNavigate()">Go Home</button>
  `,
  styles: []
})
export class UserComponent implements OnInit {

  constructor(private router:Router) { }

  ngOnInit() {
  }
  onNavigate(){
    this.router.navigate(['/']);
  }

}

Routing Parameter

localhost:4200/user/10 과 같이 주소에 어떤 변수를 부여해야할 때가 있다.

app.routes.ts

import { Routes,RouterModule } from '@angular/router';
import { HomeComponent } from './components/home.component';
import { UserComponent } from './components/user.component';

const APP_ROUTES:Routes = [
    {path:'user/:id',component:UserComponent},
    {path:'',component:HomeComponent}
];

export const routing = RouterModule.forRoot(APP_ROUTES);

app.component.html

<h1>
  {{title}}
</h1>
<a [routerLink]="['']">HomeComponent</a> |
<input type="text" #id (input)="0">
<a [routerLink]="['user',id.value]">UserComponent</a>
<router-outlet></router-outlet>

5번째 줄의 input tag 를 #id라고 참조하고 6번째 줄의 routerLink에서 상대 주소인 user와 함께 id.value, 즉 input 값을 넣어주었다. 5번째 줄의 (input)=”0″이라는 이벤트를 붙여준 이유는 input이 있을 때마다 #id 값이 변하도록 하기 위해서이다. 다른 의미는 없다. 일종의 hack 이라고 생각하면 된다.

id값이 AppComponent에서 UserComponent로 넘어갔다. 그럼 UserComponent에서는 어떻게 받아서 풀어놓을까? UserComponent 에서 ActivatedRoute 을 import 하고 injection 한 후에 ActivatedRoute의 snapshot 에서 param을 뽑아내면 된다.

user.component.ts

import { Component } from '@angular/core';
import { Router,ActivatedRoute } from '@angular/router';

@Component({
  selector: 'rt-user',
  template: `
    <p>
      user Works!
    </p>
    {{id}}
    <button (click)="onNavigate()">Go Home</button>
  `,
  styles: []
})
export class UserComponent implements OnDestroy {
  id:string;

  constructor(private router:Router, private activatedRoute:ActivatedRoute) { 
    this.id = activatedRoute.snapshot.params['id'];
  }

  onNavigate(){
    this.router.navigate(['/']);
  }

}

input 필드에 숫자를 넣고 user routerLink를 클릭하면 그 숫자가 UserComponent에 표시되는 것을 볼 수 있다. 여기까지는 성공이다. 그런데, 다시 input 필드에 다른 숫자를 넣고 routerLink를 클릭하면 이번에는 UserComponent 에 id 값이 변하지 않고 그대로 있다. Angular 2는 필요한 Component만 Load하기 때문에 이미 Load 되어 있는 UserComponent를 다시 Load하지 않는다. snapshot을 사용했을 때는 그렇다. 그 상태를 유지하도록 하는 것이다. snapshot 의 그런 기능이 이로울 때도 있지만, 방해가 되는 경우도 있다.

id값 변화에 UserComponent가 반응하게 하려면 snapshot을 쓰지 말고, 다음과 같이 ActivatedRoute의 param을 직접 이용해야한다. activatedRoute.param은 Observable을 반환한다. 따라서, Subscription 과 OnDestroy를 사용해서 Observable 사용에 따르는 불필요한 메모리 leak을 방지해야한다.

user.component.ts

import { Component, OnDestroy } from '@angular/core';
import { Router,ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Rx';

@Component({
  selector: 'rt-user',
  template: `
    <p>
      user Works!
    </p>
    {{id}}
    <button (click)="onNavigate()">Go Home</button>
  `,
  styles: []
})
export class UserComponent implements OnDestroy {
  id:string;
  private subscription:Subscription;

  constructor(private router:Router, private activatedRoute:ActivatedRoute) { 
    //this.id = activatedRoute.snapshot.params['id'];
    this.subscription = activatedRoute.params.subscribe(
      (param:any) => this.id = param['id']
    );
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
  onNavigate(){
    this.router.navigate(['/']);
  }

}

Query Parameter

Query Parameter 는 예를 들어, http://localhost:4200/user/10?age=25 와 같은 주소가 있다면, 붉은 색으로 표시된 물음표 뒤의 Parameter를 의미한다. Angular 2 에서도 이 값을 넘겨주고 받을 수 있다.

import { Component, OnDestroy } from '@angular/core';
import { Router,ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Rx';

@Component({
  selector: 'rt-user',
  template: `
    <p>
      user Works!
    </p>
    {{id}}
    <button (click)="onNavigate()">Go Home</button>
  `,
  styles: []
})
export class UserComponent implements OnDestroy {
  id:string;
  private subscription:Subscription;

  constructor(private router:Router, private activatedRoute:ActivatedRoute) { 
    //this.id = activatedRoute.snapshot.params['id'];
    this.subscription = activatedRoute.params.subscribe(
      (param:any) => this.id = param['id']
    );
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
  onNavigate(){
    this.router.navigate(['/'],{queryParams:{'analytics':100}});
  }

}

button과 연결된 onNavigation 메서드에 Query Parameter를 적용한 것이다. Routing Parameter는 패스와 함께 같은 Array안에 나열되는데, Query Parameter는 별도의 argument 에서 Object 형식으로 표현되었다.

this.router.navigate( [‘/’] , {queryParams:{‘analytics’:100}} );

HomeComponent 에서 Query Parameter 값을 받아서 풀어놓으려면, 다음과 같이 activatedRoute.queryParams 를 이용한다. 이는 역시 Observable을 반환하기 때문에 메모리 leak을 막기위해 Subscription 과 onDestroy 을 함께 import 해서 사용해야한다.

home.componet.ts

import { Component, OnDestroy } from '@angular/core';
import { ActivatedRoute } from "@angular/router";
import { Subscription } from "rxjs/Rx";

@Component({
  selector: 'rt-home',
  template: `
    <p>
      home Works!
    </p>
    {{param}}
  `,
  styles: []
})
export class HomeComponent implements OnDestroy {
  private subscription: Subscription;

  param: string;

  constructor(private activatedRoute: ActivatedRoute) {
    this.subscription = activatedRoute.queryParams.subscribe(
      (queryParam: any) => this.param = queryParam['analytics']
    );
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}

위에서 router.navigate 로 Query Param을 넘길 수 있었다. 그런데, routerLink 를 사용해야할 때는 어떤가? routerLink는 한개의 Array만 받는다.

app.component.ts

<h1>
  {{title}}
</h1>
<a [routerLink]="['']" [queryParams]="{analytics:500}">HomeComponent</a> |
<input type="text" #id (input)="0">
<a [routerLink]="['user',id.value]">UserComponent</a>
<router-outlet></router-outlet>

위에서처럼 routerLink 옆에 queryParams 를 함께 적어주면된다.


Child Routes

다음 주소에 대한 Route 은 어떻게 구현할까?

localhost:4200/user/10/detail
localhost:4200/user/10/edit

이렇게 종속 컴포넌트 안에 Child Route 이 있는 경우를 가정하고 Route 를 만들어본다.

먼저 Child Route의 대상이 될 Component 를 생성해본다.

$ cd src/app/components

$ ng g c user-detail --flat -is -it
$ ng g c user-edit --flat -is -it

UserDetailComponent 와 UserEditComponent 가 만들어졌다. src/app/components/user.routes.ts 파일을 수동으로 생성한다.

user.routes.ts

import { Routes } from '@angular/router';
import { UserDetailComponent } from './user-detail.component';
import { UserEditComponent } from './user-edit.component';

export const USER_ROUTES:Routes = [
    {path:'detail', component:UserDetailComponent},
    {path:'edit',component:UserEditComponent}
];

app.routes.ts 파일에서 user Route 항목에 children 을 추가한다.

app.routes.ts

import { Routes,RouterModule } from '@angular/router';
import { HomeComponent } from './components/home.component';
import { UserComponent } from './components/user.component';
import { USER_ROUTES } from './components/user.routes';

const APP_ROUTES:Routes = [
    {path:'user/:id',component:UserComponent,children:USER_ROUTES},
    {path:'user/:id',component:UserComponent},
    {path:'',component:HomeComponent}
];

export const routing = RouterModule.forRoot(APP_ROUTES);

마지막으로 user.component.ts 에서 router-outlet 을 추가해준다.

user.component.ts

import { Component, OnDestroy } from '@angular/core';
import { Router,ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Rx';

@Component({
  selector: 'rt-user',
  template: `
    <p>
      user Works!
    </p>
    {{id}}
    <button (click)="onNavigate()">Go Home</button>
    <router-outlet></router-outlet>
  `,
  styles: []
})
export class UserComponent implements OnDestroy {
  id:string;
  private subscription:Subscription;

  constructor(private router:Router, private activatedRoute:ActivatedRoute) { 
    //this.id = activatedRoute.snapshot.params['id'];
    this.subscription = activatedRoute.params.subscribe(
      (param:any) => this.id = param['id']
    );
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
  onNavigate(){
    this.router.navigate(['/'],{queryParams:{'analytics':100}});
  }

}

Redirecting Request

 

import { Routes,RouterModule } from '@angular/router';
import { HomeComponent } from './components/home.component';
import { UserComponent } from './components/user.component';
import { USER_ROUTES } from './components/user.routes';

const APP_ROUTES:Routes = [    
    {path:'user/:id',component:UserComponent,children:USER_ROUTES},
    {path:'user/:id',component:UserComponent},
    {path:'user/',redirectTo:'user/1', pathMatch:'full'},
    {path:'user',redirectTo:'user/1', pathMatch:'full'},
    {path:'',component:HomeComponent},
    {path:'**',redirectTo:'user/1', pathMatch:'full'}
];

export const routing = RouterModule.forRoot(APP_ROUTES);

9,10번 줄은 localhost:4200/user 또는 localhost:4200/user/ 라는 주소가 요청되었을 때, localhost:4200/user/1 로 Redirect 하라는 의미이다. pathMatch:’full’ 은 주소가 완전히 같을 때를 작동하라는 의미이다.

12번 줄은 정의되지 않은 페이지로 요청이 들어왔을 때, user/1로 Redirect하라는 의미이다. Routes 리스트 중에 가장 마지막에 와 있는 것을 명심하자. Angular 2가 Routes Array를 순서대로 인식해서 처리하기 때문에, 순서가 중요하다.


Styling Active routerLink

선택된 링크의 스타일이 바뀌도록 설정할 수 있다. routerLinkActive, routerLinkActiveOptions 를 사용한다.

app.component.ts

<h1>
  {{title}}
</h1>
<a [routerLink]="['']" [queryParams]="{analytics:500}" routerLinkActive="active" [routerLinkActiveOptions]="{exact:true}">HomeComponent</a> |
<input type="text" #id (input)="0">
<a [routerLink]="['user',id.value]" routerLinkActive="active">UserComponent</a>
<router-outlet></router-outlet>

app.component.css

.active {
    color:red;
}

패턴을 조금 달리해서 routerLink가 적용된 a 태그를 감싸는 div 태그에 routerActive 와 routerLinkActiveOptions 속성을 부여할 수도 있다.

<div routerLinkActive="active" [routerLinkActiveOptions]="{exact:true}">
  <a [routerLink]="['']" [queryParams]="{analytics:500}">HomeComponent</a>
</div>

 

Guards – canActivate

사용자가 특정 페이지에 링크를 클릭하여 접근할 수 있게 하는 것이 Route 기능이다. 그런데, 여기에 어떤 제한이 있을 수 있다. 예를 들면, 로그인을 한 사용자만이 접근할 수 있게 한다든지 할 수가 있다. 이 기능을 담당하는 것이 Guards 이다. 단순한 예를 살펴보자.

아래 코드는 사용자가 링크를 클릭했을 때 확인하는 confirm 창을 연 후, 사용자가 ok를 클릭했을 때만 해당 링크 페이지를 연다.

user-detail.guard.ts

import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
import { Observable } from "rxjs/Rx";

export class UserDetailGuard implements CanActivate {
    canActivate(route:ActivatedRouteSnapshot,state:RouterStateSnapshot):Observable<boolean> | boolean {
        return confirm('Are you sure?');
    }
}

user.routes.ts

import { Routes } from '@angular/router';
import { UserDetailComponent } from './user-detail.component';
import { UserEditComponent } from './user-edit.component';
import { UserDetailGuard } from './user-detail.guard';

export const USER_ROUTES:Routes = [
    {path:'detail', component:UserDetailComponent,canActivate:[UserDetailGuard]},
    {path:'edit',component:UserEditComponent}
];

여기에 더해 UserDetailGuard를 app.module.ts의 providers에 추가해주어야한다.

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 { AppComponent } from './app.component';
import { HomeComponent } from './components/home.component';
import { UserComponent } from './components/user.component';
import { routing } from './app.routes';
import { UserDetailComponent } from './components/user-detail.component';
import { UserEditComponent } from './components/user-edit.component';
import { UserDetailGuard } from './components/user-detail.guard';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    UserComponent,
    UserDetailComponent,
    UserEditComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    routing
  ],
  providers: [UserDetailGuard],
  bootstrap: [AppComponent]
})
export class AppModule { }

localhost:4200/user/10/detail 을 브라우저 주소창에 입력하면, 확인을 받는 팝업이 뜬다.


Guards – CanDeactivate

작성 중인 글이나 폼이 있는 Component 에서 나가려할 때, 정말 나갈 것인가를 묻는 경고가 필요하다. 이때, CanDeactivate가 등장한다.

user-edit.guard.ts

import { CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs/Rx';

export interface ComponentCanDeactivate {
    canDeactivate:()=>boolean | Observable<boolean>;
}

export class UserEditGuard implements CanDeactivate<ComponentCanDeactivate> {
    canDeactivate(component:ComponentCanDeactivate):Observable<boolean>|boolean{
        return component.canDeactivate ? component.canDeactivate() : true;
    }
}

user-edit.component.ts

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { ComponentCanDeactivate } from './user-edit.guard';
import { Observable } from 'rxjs/Rx';

@Component({
  selector: 'rt-user-edit',
  template: `
    <p>
      user-edit works!
    </p>
    <button (click)="done=true">Done</button>
    <button (click)="onNavigate()">Go Home</button>
  `,
  styles: []
})
export class UserEditComponent implements ComponentCanDeactivate {
  done = false;

  constructor(private router:Router) { }

  canDeactivate():Observable<boolean>|boolean {
    if (!this.done){
      return confirm('Do you wnat to leave?');
    }
    return true;
  }

  onNavigate(){
    this.router.navigate(['/']);
  }

}

user.routes.ts

import { Routes } from '@angular/router';
import { UserDetailComponent } from './user-detail.component';
import { UserEditComponent } from './user-edit.component';
import { UserDetailGuard } from './user-detail.guard';
import { UserEditGuard } from './user-edit.guard';

export const USER_ROUTES:Routes = [
    {path:'detail', component:UserDetailComponent,canActivate:[UserDetailGuard]},
    {path:'edit',component:UserEditComponent,canDeactivate:[UserEditGuard]}
];

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 { AppComponent } from './app.component';
import { HomeComponent } from './components/home.component';
import { UserComponent } from './components/user.component';
import { routing } from './app.routes';
import { UserDetailComponent } from './components/user-detail.component';
import { UserEditComponent } from './components/user-edit.component';
import { UserDetailGuard } from './components/user-detail.guard';
import { UserEditGuard } from './components/user-edit.guard';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    UserComponent,
    UserDetailComponent,
    UserEditComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    routing
  ],
  providers: [UserDetailGuard,UserEditGuard],
  bootstrap: [AppComponent]
})
export class AppModule { }

localhost:4200/user/7/edit 을 브라우저 주소창에 넣어보자. 가장 아래에 있는 Go Home 버턴을 누르면, 나갈 것인지를 묻는 경고창이 뜬다.

 

 

참조: https://angular.io/docs/ts/latest/guide/router.html

“[ Angular 2: Routes ] 네비게이션을 위한 라우트”에 대한 1개의 생각

댓글 남기기