이 글은 Thoughtram 블로그를 참고하여 작성한 것입니다.

Angular의 router를 이용하면 쉽게 페이지 전환을 할 수 있습니다. 하지만 좀 더 완벽히 구동되는 application을 만들기 위해서 router는 해결해야 할 문제점이 하나 있습니다. 바로 바인딩되는 데이터 로딩이 라우터가 실행보다 빠르게 완료되는 것이 보장되지 않는다는 것입니다.

예를들어 http통신을 통해 api로 특정 데이터를 가져오고, 이를 화면에 뿌린다고 했을때, http통신은 비동기이기 때문에 라우터가 먼저 실행되고 나서 얼마 후에 데이터 도착하면 view가 rendering 됩니다. 이 때문에 사용자들은 가끔 데이터가 듬성듬성 빠져있는 화면을 잠깐동안 보게 됩니다.

이를 해결하는 방법은 여러가지가 있습니다(데이터가 들어오기 전까지 host경로에 ngIffalse로 한다는 등의…). 여기서는 Route의 resolver를 통해 문제를 해결해 보려고 합니다.

무엇이 문제일까요?

자, contact 애플리케이션을 만들어 봅시다. 우리는 contacts listcontacts detail을 위한 라우터를 가지고 있습니다.

1
2
3
4
5
6
7
8
import { Routes } from '@angular/router';
import { ContactsListComponent } from './contacts-list';
import { ContactsDetailComponent } from './contacts-detail';
export const AppRoutes: Routes = [
{ path: '', component: ContactsListComponent },
{ path: 'contact/:id', component: ContactsDetailComponent }
];

그리고 당연히 우리는 해당 라우터를 탑재한 루트모듈이 필요합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouterModule } from '@angular/router';
import { AppRoutes } from './app.routes';
@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot(AppRoutes)
],
...
})
export class AppModule {}

여기까지 특별할 것은 없습니다. 만일 이 소스가 낯설다면 라우팅에 관한 다른 글을 먼저 읽으시는걸 권장합니다.

ContactsDetailComponent을 살펴 봅시다. 이 컴포넌트는 contact data를 보여주는 역할을 가지고 있습니다. 따라서 route URL에서 제공되는 id값(route에서 :id로 포현되는 파라미터)을 가지고 contact object에 접근해야 합니다. ActivatedRoute를 통해서 쉽게 route parameter에 접근 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ContactsService } from '../contacts.service';
import { Contact } from '../interfaces/contact';
@Component({
selector: 'contacts-detail',
template: '...'
})
export class ContactsDetailComponent implements OnInit {
contact: Contact;
constructor(
private contactsService: ContactsService,
private route: ActivatedRoute
) {}
ngOnInit() {
let id = this.route.snapshot.params['id'];
this.contactsService.getContact(id)
.subscribe(contact => this.contact = contact);
}
}

좋습니다. ContactsDetailComponent는 받은 id를 가지고 contact 객체를 가져오고, 로컬 contact property에 제공합니다. 그리고 `{{contact.name}}`같은 표현을 통해 컴포넌트의 템플릿에 값을 삽입합니다.
컴포넌트의 템플릿을 살펴봅시다!

undefined

contact객체 뒤에 물음표(?)를 붙였습니다. 이를 Safe Navigation Operators(SNO)라고 부릅니다. 이는 만일 비동기로 contact 데이터를 바인딩한다면, 컴포넌트가 초기화될때 contactundefined이기 때문에, 프로퍼티를 가질 수 없어 에러를 내는 것을 방지하기 위한 표현입니다.
이 이슈를 구현하기 위해서, ContactsService#getContact()에 3초 딜레이를 주고 contact 오브젝트를 내보내겠습니다. RxJSdelay 오퍼레이터를 쓰면 쉽게 구현할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Injectable } from '@angular/core';
@Injectable()
export class ContactsService {
getContact(id) {
return Observable.of({
id: id,
name: 'Pascal Precht',
website: 'http://thoughtram.io',
}).delay(3000);
}
}

템플릿 마다 SNO를 모든 곳에 추가하는 것도 상당히 힘든 일이 될 수 있습니다. 그 외에도 NgModelRouterLink Directive와 같은 일부 연산자는 SNO를 지원하지 않습니다. 이제 route resolver를 사용하여 어떻게 해결할 수 있는지 살펴 보겠습니다.

resolver의 정의

route resolvers는 route가 활성화 되기전에, route에게 필요한 데이터를 제공하는 것을 도와줍니다. resolver를 생성하는 것은 여러 방법이 있습니다. 우리는 가장 쉬운것 부터 시작할 것입니다. resolverObservable<any>, Promise<any> 또는 단지 데이터를 반환하는 함수입니다.

Resolver는 Angular Module의 providers에 등록되어야 합니다.

여기에 static한 contact object를 반환하는 resolver 함수가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@NgModule({
...
providers: [
ContactsService,
{
provide: 'contact',
useValue: () => {
return {
id: 1,
name: 'Some Contact',
website: 'http://some.website.com'
};
}
]}
})
export class AppModule {}

우리는 resolver가 사용될때 항상 같은 contact object가 반환되는걸 바라지 않습니다. 우리는 Angular의 dependency injection(DI, 의존성 주입)를 사용해서 간단한 resolver 함수를 등록할 수 있습니다. 어떻게 이 resolverroute에 연결하면 될까요? resolve 프로퍼티를 resolver를 사용할 route 구성안에 추가하면 됩니다.

아래는 어떻게 우리의 resolver 함수를 route 구성에 추가하는지 보여줍니다.

1
2
3
4
5
6
7
8
9
10
export const AppRoutes: Routes = [
...
{
path: 'contact/:id',
component: ContactsDetailComponent,
resolve: {
contact: 'contact'
}
}
];

이게 다인가요? 네 맞습니다! 'contact'resolver를 route 구성에 추가할때에 참고하는 provider 토큰입니다.

이제 우리가해야 할 일은 ContactsDetailComponentcontact 객체를 유지하는 방법을 변경하는 것입니다. route resolvers를 통해 전달되는 모든 것은 ActivatedRoute의 데이터 속성에 노출됩니다. 즉, 이제 우리는 다음과 같이 ContactsService 종속성(dependency)을 제거 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
@Component()
export class ContactsDetailComponent implements OnInit {
contact;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.contact = this.route.snapshot.data['contact'];
}
}

사실, resolver함수를 정의하면, 우리는 RouterStateSnapshot뿐 아니라 ActivatedRouteSnapshot에도 접근할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
@NgModule({
...
providers: [
ContactsService,
{
provide: 'contact',
useValue: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
...
}
]}
})
export class AppModule {}

이것은 라우터 파라미터 같이 우리가 접근이 필요한 곳에 접근할 수 있어, 유용하게 쓰일 수 있습니다.
하지만, 우리는 ContactsService 인스턴스도 필요합니다. 하지만 우리는 여기에 서비스를 주입할 수 없습니다. 그러면 dependency injection이 필요한 resolver는 어떻게 만들어야 할까요?

Resolvers with dependencies

이미 알다시피, dependency injectionclass 구조에서 작동합니다. 따라서 우린 class가 필요합니다. 다행히 우리는 class를 써서 resolver를 만들 수 있습니다. 우리가 해야하는 유일한 것은, resolver 클래스를 Resolve 인터페이스를 구현(implement)하고, resolve() 메소드를 추가하는 것입니다. 이 resolve() 메소드는 위에서 DI를 통해 등록한 것과 거의 같은 함수입니다.

아래는 클래스로 contact resolver를 구현한 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot } from '@angular/router';
import { ContactsService } from './contacts.service';
@Injectable()
export class ContactResolve implements Resolve<Contact> {
constructor(private contactsService: ContactsService) {}
resolve(route: ActivatedRouteSnapshot) {
return this.contactsService.getContact(route.params['id']);
}
}

resolver가 클래스가 되면, 클래스는 provider token으로 사용될 수 있기 때문에 provider 구성은 매우 심플해집니다.

1
2
3
4
5
6
7
8
@NgModule({
...
providers: [
ContactsService,
ContactResolve
]
})
export class AppModule {}

그리고 동일한 토큰을 route에 resolver로 사용하면 됩니다.

1
2
3
4
5
6
7
8
9
10
export const AppRoutes: Routes = [
...
{
path: 'contact/:id',
component: ContactsDetailComponent,
resolve: {
contact: ContactResolve
}
}
];

Angular는 resolver가 함수인지, 클래스인지, resolve()를 호출하는 클래스인지 탐지하기에 충분히 똑똑합니다.
어떻게 Angular가 데이터가 도착할때까지 컴포넌트를 인스턴스화 시키는 것을 지연시키는지(3초), 아래의 데모에서 확인해보세요.