라우터

lee jae hwan·2022년 9월 21일

앵귤러

목록 보기
65/83

라우터(Router)는 컴포넌트 네비게이션을 담당하는 내장서비스객체로 소스컴포넌트는 비활성화되고 대상컴포넌트를 활성화시켜 컴포넌트가 전환된다.

앵커태그에의해 url이 변경되면 라우터는 url을 해석하여 라우트(Routes)규칙에 지정된 컴포넌트를 활성화시킨다.

라우트규칙은 url과 매핑되는 컴포넌트의 쌍으로 구성되며 Route인터페이스에 정의되어있다.
type Routes = Route[];

interface Route {
  path?: string
  pathMatch?: string
  matcher?: UrlMatcher
  component?: Type<any>
  redirectTo?: string
  outlet?: string
  canActivate?: any[]
  canActivateChild?: any[]
  canDeactivate?: any[]
  canLoad?: any[]
  data?: Data
  resolve?: ResolveData
  children?: Routes
  loadChildren?: LoadChildren
  runGuardsAndResolvers?: RunGuardsAndResolvers
}

path프로퍼티는 활성화될 컴포넌트에 매핑되는 고유 url을 의미한다.

component프로퍼티는 path값에 매핑되는 컴포넌트이름이다.


컴포넌트는 2가지 방법으로 전환이 가능하다.

  1. 브라우저 URL 변경될때
  2. 어플리케이션 코드에의해 url 변경될때

라우터는 브라우저 url이나 라우터의 주소가 변경되는 것을 감지하여 작동한다.


라우터객체를 사용하려면 RouterModule을 app.module에 등록하면 된다. ( 직접 라우터객체를 생성하지 않는다. 앵귤러는 프레임워크에 필수적인 객체는 사용자가 생성하지 않는 방식을 사용한다. )

라우터의 forRoot()메소드로 routes규칙을 어플리케이션에 등록할 수 있다.

const routes:Route[] = [
  {path:'crisis', component:CrisisListComponent},
  {path:'hero', component:HeroListComponent}
];

활성화될 컴포넌트는 url에 매핑되는 라우트규칙에따라 정해지는데 소스컴포넌트는 어떤 컴포넌트일까?

앵귤러는 라우팅영역이라는 엘리먼트를 만들어놓고 활성화될 컴포넌트를 라우팅영역 바로아래에 추가하는 방식을 사용한다.

<router-outlet></router-outlet>

그리고나서 다른 컴포넌트를 활성화해야하면 라우팅영역에있는 컴포넌트를 제거하고 새로운 컴포넌트를 활성화하여 추가한다.


< a> , < button>등 HTML엘리먼트로 필요한 컴포넌트를 활성화시켜보자.

routerLink 디렉티브는 할당된 값으로 브라우저 url을 변경하는 역활을 한다.

현재 url이 localhost:4200이고 < a routerLink="hero">라면 브라우저 url은 localhost:4200/hero로 변경된다.

라우터는 url변경을 감지하고 hero가 route규칙에 있는지 확인후 매핑된 컴포넌트를 라우팅영역에 활성화시킨다.


와일드카드 라우트규칙

route규칙에 없는 url값일 경우 라우터는 에러객체를 던지며 스크립트는 중단된다.

{ path: '**', component: PageNotFoundComponent }

path값이 별표2개면 매핑되지 않는 url은 모두 PageNotFoundComponent를 활성화시킨다.


어플리케이션이 커질수록 route규칙은 많아지게되며 일반적으로 컴포넌트들은 계층구조를 가지므로 route규칙들간의 우선순위가 필요하다.

라우터는 위에서 아래의 순서로 매핑되는 규칙이 있다면 우선적으로 일치하는 라우트규칙에따라 컴포넌트를 활성화한다.

따라서 하단에 와일드카드 규칙을 위치시키는 것이 좋다.


라우팅영역 계층

AppComponent에 있는 라우팅영역은 가장 최상위에 있는 1단계 라우팅영역이다.

이 라우팅영역에 활성화된 컴포넌트의 템플릿에는 2단계 라우팅영역을 가질 수 있다. 즉 라우팅영역안에 라우팅영역이 있는 것이다.

2단계 라우팅영역에 매핑되려면 라우트규칙도 2단계 규칙이어야 한다.

Hero컴포넌트내에 있는 라우팅영역에 컴포넌트를 활성화하려면 hero라우트규칙의 하위로 라우트규칙을 만들어야 한다.

1단계 라우트규칙은 1단계 라우팅영역에만 매핑될 수 있다.


템플릿의 routerLink에는 절대주소, 상대주소를 사용할 수 있고 라우터는 변경된 주소를 해석하여 처리한다.

<a routerLink="/hero">

절대경로를 지정하면 도메인을 제외한 최상위경로를 의미한다.

http://localhost:4200/crisis일때

<a routerLink="hero">abcd</a>
<a routerLink="./hero">abcd</a>

2가지는 같은 표현으로 http://localhost:4200/crisis/hero가 된다.

라우터는 1단계 라우터규칙으로 crisis/hero 규칙이 있거나 crisis규칙 하위에 hero규칙이 있다면 해당 컴포넌트를 활성화시킬 것이다.

http://localhost:4200/crisis일때

<a routerLink="../">

http://localhost:4200으로 변경된다.
http://localhost:4200/crisis일때

<a routerLink="../hero">

http://localhost:4200/hero로 변경된다.

공백와일드 라우트 규칙

어플리케이션이 실행되면 브라우저url은 localhost:4200이 된다.

라우트규칙이 없다면 일치하는 규칙이 없으므로 활성되는 컴포넌트는 없다.

{path:'', component:MatchComponent},
{path:'**', component:PageNotComponent},

'**' 라우트규칙은 매칭되는 규칙이 없을때를 의미하기때문에 어플리케이션수준에서 에러를 방지하기위해 사용한다.

'' 라우트규칙은 어떤 규칙이든 일치함을 의미하고 모듈단위에서 세부적인 규칙을 만들고 세부규칙들이 일치하지 않을때 활성화할 컴포넌트를 설정할때 사용한다.

localhost:4200일때 /hero로 url을 리다이렉트해야 할 경우가 있다.

{ path: '',   redirectTo: '/hero', pathMatch: 'full' },

모듈화방식

앵귤러는 여러개의 유사한 기능의 컴포넌트들을 ngModule에 묶어 관리하는 방식을 사용한다.

컴포넌트들을 관리할 ngModule 하나와 라우트규칙들을 관리할 ngModule 하나, 이렇게 2개의 ngModule파일이 컴포넌트의 작동을 관리한다.


라우트인자

{ path: 'hero/:id', component: HeroDetailComponent }

:id토큰은 라우트인자(route parameter)를 선언하는 역활을 한다.

localhost:4200/hero/15

url로 컴포넌트에 인자를 전달하는 방법은 여러가지가 있지만 컴포넌트에 반드시 인자를 전달해야 하는 경우에는 :?? 토큰을 사용하는 것이 좋다.

라우트인자는 전달되는 인자가 url path의 구성부분이 된다.


템플릿에서 라우트인자 설정하기

localhost:4200/hero/15처럼 url이 되도록 하면 되며 15가 라우트인자다.

<a routerLink="/hero/{{hero.id}}">

문자열바인딩을 사용해도 된다.

<a [routerLink]="['/hero', hero.id]">

routerLink프로퍼티 바인딩하면 타입스크립트를 사용할 수 있어 배열표현식을 사용 할 수 있다. 화면갱신의 최적화를 위해 복잡한 표현식은 사용하지 않는 것이 좋다.

링크인자배열(link parameters array)는 첫번째는 일반적인 routerLink프로퍼티 값이고 두번째는 라우트인자값이다.



전달된 라우트인자에 접근하는 방법

  • 옵저버블객체를 이용하는 방법
class ActivatedRoute {
  snapshot: ActivatedRouteSnapshot
  url: Observable<UrlSegment[]>
  params: Observable<Params>
  queryParams: Observable<Params>
  fragment: Observable<string | null>
  data: Observable<Data>
  outlet: string
  component: Type<any> | string | null
  routeConfig: Route | null
  root: ActivatedRoute
  parent: ActivatedRoute | null
  firstChild: ActivatedRoute | null
  children: ActivatedRoute[]
  pathFromRoot: ActivatedRoute[]
  paramMap: Observable<ParamMap>
  queryParamMap: Observable<ParamMap>
  toString(): string
}

ActivatedRoute객체는 현재 url에대한 정보들을 가지고 있다.

constructor(private route: ActivatedRoute) {}

ngOnInit() {
  this.hero$ = this.route.paramMap.pipe(
    switchMap((params: ParamMap) =>
      this.service.getHero(params.get('id')!))
  );
}

ActivatedRoute는 서비스가 아니고 클래스인데 의존성주입이 되네...?
사용자정의 클래스는 서비스로 만들어야 의존성주입이 된다.

ActivatedRoute의 paramMap은 전달되는 인자들을 옵저버블객체로 반환하기 때문에 구독하면 정보들을 얻을 수 있다.


params.get메소드로 id 라우트인자를 얻고 서비스로 hero 옵저버블 객체를 반환받는다.

switchMap, mergeMap 2가지 다 사용가능하지만 switchMap을 사용하면 응답지연시 다음 이벤트발생했을때 이전구독을 취소하고 방출하므로 장점이 있다.

service.getHero메소드가 옵저버블객체를 반환하여 중첩되므로 highorder의 switchMap을 사용해야 한다.

할당된 this.hero$는 템플릿에서 async파이프로 구독된다.


paramMap 옵저버블과 컴포넌트 재사용

라우트인자는 컴포넌트 라이프사이클에따라 변경될 수 있다.

기본적으로 라우터는 컴포넌트가 다른 컴포넌트로부터 전환되지 않고 같은 컴포넌트로 전환되는 경우도 있을 수 있다. 이런 경우 컴포넌트는 재사용되며 새롭게 초기화되지 않기때문에 ngOnInit라이프사이클이 다시 실행되지 않는다.

그러나 이때 라우트인자는 달라 질 수 있다.

이런 경우 ngOnInit에서 route.snapshot으로 라우트인자를 받는다면 변경된 라우트인자를 받을 수 없게된다.

이런 상황에서 라우트인자를 받으려면 paramMap옵저버블객체를 구독해야 한다.


주의사항

일반적으로 컴퍼넌트내에서 옵저버블을 구독하면 컴포넌트가 종료되기전에 구독을 반드시 해제해야 메모리누수가 없다.

그러나 ActivatedRoute옵저버블은 예외인데 ActivatedRoute객체가 반환하는 옵저버블객체들은 컴포넌트 종료시 라우터가 옵저버블 구독도 자동으로 해지한다.

그런데 Router가 ActivatedRoute옵저버블 구독해지할때는 complete신호를 방출하지 않기때문에 구독시 complete콜백을 정의했다면 ngOnDestroy함수에서 직접 구현해야 한다.


  • snapshot: 옵저버블을 사용하지 않는 방법

ActivatedRoute객체에는 snapshot프로퍼티가 있는데 옵저버블객체가 아니고 컴포넌트가 초기화될때 라우트인자들의 정보를 가지고 있으며 라이프사이클시 변경되지 않는다.

옵저버블이 아니므로 보다 간편히 라우트인자를 얻을 수 있지만 컴포넌트재사용등 라우트인자가 변경될 수 있다면 옵저버블객체를 사용해야 한다.


필수 라우트인자 / 옵션 라우트인자

필수 라우트인자

localhost:4200/hero/15

필수 라우트인자를 전달받으려면 :id와 같이 토큰을 사용해며 라우트규칙에 :id 토큰을 명시해야 한다.

토큰을 사용한 라우트인자는 url path의 일부분이 된다.

<a [routerLink]="['/hero', hero.id]">

{ path: 'hero/:id', component: HeroDetailComponent }

link parameter array로 필수인자를 전달할 수 있다.


옵션 라우트인자

name='wind*' after='12/31/2015' & before='1/1/2017'

옵션 라우트인자는 url path와 매칭되지 않기 때문에 복잡한 정보라도 url과 함께 자유롭게 전달할 수 있다.

this.router.navigate(['/heroes', { id: heroId, foo: 'foo' }]);

두번째인자에 옵션라우트인자를 객체형태로 전달하면 된다.

localhost:4200/heroes;id=15;foo=foo

옵션라우트인자는 url쿼리스트링처럼 ?, &으로 구분되지않고 ;로 구분하며 이 방식은 메트릭스url표기법이라 한다.

메트릭스표기법으로 전달된 옵션 라우트인자도 필수라우트인자와 같이 paramMap옵저버블의 get메소드를 사용하면 된다.


자식라우팅

이전 예제에서는 하나의 라우팅영역으로 hero목록 컴포넌트와 hero내용 컴포넌트가 서로 컴포넌트전환을 했다.

이러한 방식은 두 컴포넌트의 라우트규칙이 같은 계층에 있기 때문에 같은 라우팅영역을 사용해서 컴포넌트전환된다.

어떤 라우팅영역에 컴포넌트가 활성화되고 활성화된 컴포넌트 내에 라우팅영역이 있으면 라우팅영역은 부모-자식관계를 형성하기때문에 라우트규칙도 부모-자식관계를 형성해야 한다.

따라서 자식라우팅영역에는 자식라우트규칙의 컴포넌트만 활성화가 가능하다.


2차 라우팅영역

템플릿에 라우팅영역이 1개있다면 앵귤러가 라우트규칙에따르는 컴포넌트를 라우팅영역 바로아래에 활성화하여 추가시킨다.

템플릿에 2개이상의 라우팅영역이 필요할때는 name프로퍼티를 지정해서 구분해야 한다.

<router-outlet name="popup"></router-outlet>

라우트규칙에 popup라우팅영역으로 활성화시킬 컴포넌트를 지정하면 된다.

{path:'compose',component:ComposeMessageComponent, outlet:'popup'},
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>

routeLink에는 위와같이 정해진 규칙을 따라야 한다.

this.router.navigate([{ outlets: { popup: null } }]);

활성화된 컴포넌트를 비활성화하려면 위와같이 해야 한다.


라우터가드

앵귤러는 url변경을 감지하여 컴포넌트활성화를 제어하므로 컴포넌트 활성화전 필요한 동작을 수행할 수 있도록 라우터가드를 제공한다.

라우터가드를 만든후 라우터규칙에 추가하면 자동으로 동작한다.

라우터가드가 true를 반환하면 라우터규칙대로 컴포넌트는 활성화된다.

라우터가드가 false를 반환하면 컴포넌트가 활성화되지 않고 진행을 멈춘다.

라우터가드가 UrlTree를 반환하면 UrlTree에 해당하는 url로 변경된다.

라우터가드의 결과값은 Observable< boolean>이나 Promise< boolean>으로 라우터가드는 결과값을 반환하기전까지 전환을 멈추게된다.

라우터가드 종류

CanActivate, CanActivateChild : 활성화전 작동
CanDeactivate : 비활성화전 작동
Resolve : 활성화전 데이터로드
CanLoad : 비동기 기능모듈 로드전 작동

라우터가드도 서비스의 한종류이며 로그인여부로 특정 컴포넌트에 접속권한을 확인하는 기능이기때문에 별도의 모듈로 관리한다.

export class AuthGuard implements CanActivate {
  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): boolean | UrlTree {
    return true;
  }
}

AuthGuard서비스는 CanActivate를 구현하여 CanActivate함수는 옵저버블등 여러타입 값을 반환할 수 있다.

 {path:'',component:AdminComponent, canActivate:[AuthGuard]},

AdminComponent컴포넌트가 활성화되기전에 AuthGuard서비스의 반환값에따라 동작이 달라진다.

라우트규칙에 필요한 라우터가드를 등록하고 가드서비스에 함수를 구현하여 여러 값을 반환함으로써 흐름을 제어한다.

하나의 컴포넌트에 여러개의 라우터가드가 사용이 가능하며 라우터가드는 다른 컴포넌트에도 재사용이 가능하기때문에 라우터가드는 해당 함수를 구현하고 자세한 기능구현은 서비스에서 구현하는 것이 좋다.


AuthGuard는 로그인여부를 확인하여 로그인안되었을때는 login컴포넌트로 이동시켜 로그인후 AdminComponent컴포넌트를 활성화시킨다.

const navigationExtras:NavigationExtras = {
          queryParamsHandling: 'preserve',
          preserveFragment:true
        }
this.router.navigate([redirectUrl, navigationExtras]);  

컴포넌트를 전환시킬때 라우트인자들을 보존하기위해 NavigationExtras객체를 사용한다.


CanDeactivate가드

화면의 데이터가 변경된후 사용자가 화면을 벗어나면 저장하지 않은 변경사항을 어떻게 처리할지 결정되어야 한다.

변경사항을 저장하는 경우에는 저장이 완료될때까지 화면전환동작이 잠시 멈추어야할 수도 있다.

그래서 화면전환을 잠시 기다렸다가 저장이 완료된후 비동기로 화면을 전환하는 것이 안전하다.

저장하지 않은 변경사항을 어떻게 처리할지 사용자에게 확인하려면 CanDeactivate가드를 사용하면 된다.

변경사항이 있는데 사용자가 취소버튼을 누르거나 뒤로가기 버튼을 눌렸을때 사용자에게 확인하여 결정될때까지 비동기로 기다리는 방식을 활용할 수 있다.

동기식으로 사용자결정을 기다릴수도 있지만 이방식은 코드실행을 블록킹하기때문에 백그라운드에서 다른 작업을 계속 할 수 있는 비동기방식이 좋다.

CrisisDetailComponent에서 변경사항이 있는데 취소버튼을 클릭한경우 confirm함수로 사용자에게 결정을 받아서 처리하는 것은 Deactivate가드를 사용하지 않아도 되며 confirm결과를 옵저버블로 받지 않아도 된다.

하지만 Deactivate가드를 사용해 구현해보자

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

@Injectable({
  providedIn: 'root',
})
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate) {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}

CanDeactivateGuard가드를 위와 같이 정의하면 가드가 필요한 컴포넌트에 canDeactivate()함수만 구현하여 재사용이 가능하다.

{path:':id',component:CrisisDetailComponent,canDeactivate:[CanDeactivateGuard]

라우트규칙에 canDeactivate가드를 등록해야 한다.

  canDeactivate():Observable<boolean> | boolean{
    if(this.crisis.name == this.editName){
      return true;    }

    return this.dialogService.confirm('변경사항을 버리시겠습니까?');
  }

CrisisDetailComponent컴포넌트에 canDeactivate함수를 구현하면 컴포넌트가 비활성화되기전에 호출되어 필요한 동작을 수행할 수 있다.


Resolve가드: 컴포넌트 데이터 미리 받아오기

Heroes, Crisis컴포넌트는 url변경되고 라우트규칙으로 해당 컴포넌트의 라이프사이클함수에서 필요한 데이터를 받아온후 랜더링된다.

실제 운영환경에서는 컴포넌트가 활성화된후 서버로부터 데이터를 받아서 랜더링되기까지 시간이 소요될것이며 빈화면이 표시되는데 이 동작을 개선할 수 있다.

Resolve가드를 사용하면 서버로부터 데이터를 미리 받으면 라우트규칙이 적용되는 시점에 바로 랜더링될 수 있다.

만약 데이터를 받을때 에러가 발생한다면 랜더링하기전에 에러에 대응할 수 있는 장점도 있다.

ng generate service crisis-center/crisis-detail-resolver

resolve가 라우팅가드에 속해있다고 했는데 서비스를 생성한다????

resolve서비스 클래스에서는 데이터를 불러오는 작업을 하며 옵저버블객체를 반환하면된다.

불러올 데이터가 없다면

this.router.navigate(['/crisis-center']);
          return EMPTY;

컴포넌트를 전환하고 EMPTY를 반환한다.

{path:':id',component:CrisisDetailComponent,
            canDeactivate:[CanDeactivateGuard],
            resolve:{crisis:CrisisDetailResolverService}
          }

해당컴포넌트 라우트규칙에 resolve를 등록하고 받는 데이터를 객체형태로 지정한다.

이렇게 하면 컴포넌트에서는 crisis 데이터를 불러오는 작업이 필요없으며

    this.route.data.subscribe(
      data=>{
        this.crisis = data['crisis'];
        this.editName = this.crisis.name;
      }
    );

Router.data 옵저버블을 구독하여 데이터를 받으면 된다.


쿼리인자와 프레그먼트

라우트인자는 라우트규칙에서 토큰형식으로 인자를 사용하였으며 url의 path 일부분으로 구성되었다.

하지만 쿼리인자와 프레그먼트를 사용해도 라우트규칙에서 인자를 얻을 수 있지만 이것은 url path의 일부분이 되지 않는다.

const navigationExtras:NavigationExtras={
  queryParams:{session_id:sessionId},
  fragment:'anchor'
}

this.router.navigate(['/login'],navigationExtras);

navigate메소드 2번째인자에 NavigationExtras형식의 객체를 전달하면 쿼리인자와 프레그먼트를 라우트규칙에 전달할 수 있다.

기본적으로 전달받은 퀄리인자와 프레그먼트는 일회성이므로 다른 컴포넌트로 전환된다면 소실된다.

const navigationExtras:NavigationExtras = {
   queryParamsHandling: 'preserve',
   preserveFragment:true
}

전달받은 것을 그대로 다시 전달하려면 위와 같이 NavigationExtras객체를 navigate 2번째 인자로 사용하면 된다.

    this.session_id = this.route.snapshot.queryParamMap.get('session_id')!;
    this.token = this.route.snapshot.fragment!;

쿼리인자와 프레그먼트를 얻을 수 있다.

<a [routerLink]="['crisis-center', {foo: 'bar'}, 3, {bar: 'baz'}]">
      link to user component
    </a>

리다이렉션으로 기존 url 마이그레이션하기

/heroes주소를 유지하고 /superheroes로 변경하려면 라우트규칙에 리다이렉션을 추가하면 된다.

라우터는 컴포넌트를 전환하기전에 일반 라우터규칙보다 리다이렉션규칙을 먼저 검사한다.

const routes: Routes = [
  {path:'heroes', redirectTo:'superheroes'},
  {path:'hero/:id', redirectTo:'superhero/:id'},
  {path:'superheroes',component:HeroListComponent},
  {path:'superhero/:id',component:HeroDetailComponent},
  {path:'', redirectTo:'heroes', pathMatch:'full'}
];

위와같이 마이그레이션을 위한 리다이렉션 규칙을 추가하면 된다.

{path:'', redirectTo:'heroes', pathMatch:'full'}
는 리다이렉션이 안되므로 superheroes로 변경해줘야 한다.

routerLink와 navigate는 라우트규칙에 직접연결된것이 아니고 url을 변경하면 라우트규칙에 따르는 것이므로 고쳐주는것이 좋다.

0개의 댓글