Angular2 Resolve
이 글은 Thoughtram 블로그를 참고하여 작성한 것입니다.
Angular의 router를 이용하면 쉽게 페이지 전환을 할 수 있습니다. 하지만 좀 더 완벽히 구동되는 application을 만들기 위해서 router는 해결해야 할 문제점이 하나 있습니다. 바로 바인딩되는 데이터 로딩이 라우터가 실행보다 빠르게 완료되는 것이 보장되지 않는다는 것입니다.
예를들어 http통신을 통해 api로 특정 데이터를 가져오고, 이를 화면에 뿌린다고 했을때, http통신은 비동기이기 때문에 라우터가 먼저 실행되고 나서 얼마 후에 데이터 도착하면 view가 rendering 됩니다. 이 때문에 사용자들은 가끔 데이터가 듬성듬성 빠져있는 화면을 잠깐동안 보게 됩니다.
이를 해결하는 방법은 여러가지가 있습니다(데이터가 들어오기 전까지 host경로에 ngIf
를 false
로 한다는 등의…). 여기서는 Route의 resolver를 통해 문제를 해결해 보려고 합니다.
무엇이 문제일까요?
자, contact 애플리케이션을 만들어 봅시다. 우리는 contacts list
와 contacts detail
을 위한 라우터를 가지고 있습니다.
1 | import { Routes } from '@angular/router'; |
그리고 당연히 우리는 해당 라우터를 탑재한 루트모듈이 필요합니다.
1 | import { NgModule } from '@angular/core'; |
여기까지 특별할 것은 없습니다. 만일 이 소스가 낯설다면 라우팅에 관한 다른 글을 먼저 읽으시는걸 권장합니다.
ContactsDetailComponent
을 살펴 봅시다. 이 컴포넌트는 contact data를 보여주는 역할을 가지고 있습니다. 따라서 route URL에서 제공되는 id
값(route에서 :id
로 포현되는 파라미터)을 가지고 contact object
에 접근해야 합니다. ActivatedRoute
를 통해서 쉽게 route parameter에 접근 할 수 있습니다.
1 | import { Component, OnInit } from '@angular/core'; |
좋습니다. ContactsDetailComponent
는 받은 id
를 가지고 contact
객체를 가져오고, 로컬 contact
property에 제공합니다. 그리고 `{{contact.name}}`같은 표현을 통해 컴포넌트의 템플릿에 값을 삽입합니다.
컴포넌트의 템플릿을 살펴봅시다!
contact
객체 뒤에 물음표(?)를 붙였습니다. 이를 Safe Navigation Operators(SNO)라고 부릅니다. 이는 만일 비동기로 contact
데이터를 바인딩한다면, 컴포넌트가 초기화될때 contact
는 undefined
이기 때문에, 프로퍼티를 가질 수 없어 에러를 내는 것을 방지하기 위한 표현입니다.
이 이슈를 구현하기 위해서, ContactsService#getContact()
에 3초 딜레이를 주고 contact
오브젝트를 내보내겠습니다. RxJS
의 delay
오퍼레이터를 쓰면 쉽게 구현할 수 있습니다.
1 | import { Injectable } from '@angular/core'; |
템플릿 마다 SNO를 모든 곳에 추가하는 것도 상당히 힘든 일이 될 수 있습니다. 그 외에도 NgModel
및 RouterLink
Directive와 같은 일부 연산자는 SNO를 지원하지 않습니다. 이제 route resolver
를 사용하여 어떻게 해결할 수 있는지 살펴 보겠습니다.
resolver의 정의
route resolvers
는 route가 활성화 되기전에, route에게 필요한 데이터를 제공하는 것을 도와줍니다. resolver
를 생성하는 것은 여러 방법이 있습니다. 우리는 가장 쉬운것 부터 시작할 것입니다. resolver
는 Observable<any>
, Promise<any>
또는 단지 데이터를 반환하는 함수입니다.
Resolver는 Angular Module의 providers
에 등록되어야 합니다.
여기에 static한 contact
object를 반환하는 resolver 함수가 있습니다.
1 | @NgModule({ |
우리는 resolver
가 사용될때 항상 같은 contact
object가 반환되는걸 바라지 않습니다. 우리는 Angular의 dependency injection(DI, 의존성 주입)
를 사용해서 간단한 resolver
함수를 등록할 수 있습니다. 어떻게 이 resolver
를 route
에 연결하면 될까요? resolve 프로퍼티를 resolver
를 사용할 route 구성안에 추가하면 됩니다.
아래는 어떻게 우리의 resolver
함수를 route
구성에 추가하는지 보여줍니다.
1 | export const AppRoutes: Routes = [ |
이게 다인가요? 네 맞습니다! 'contact'
는 resolver
를 route 구성에 추가할때에 참고하는 provider
토큰입니다.
이제 우리가해야 할 일은 ContactsDetailComponent
가 contact
객체를 유지하는 방법을 변경하는 것입니다. route resolvers를 통해 전달되는 모든 것은 ActivatedRoute
의 데이터 속성에 노출됩니다. 즉, 이제 우리는 다음과 같이 ContactsService
종속성(dependency)을 제거 할 수 있습니다.
1 | @Component() |
사실, resolver함수를 정의하면, 우리는 RouterStateSnapshot
뿐 아니라 ActivatedRouteSnapshot
에도 접근할 수 있습니다.
1 | @NgModule({ |
이것은 라우터 파라미터 같이 우리가 접근이 필요한 곳에 접근할 수 있어, 유용하게 쓰일 수 있습니다.
하지만, 우리는 ContactsService
인스턴스도 필요합니다. 하지만 우리는 여기에 서비스를 주입할 수 없습니다. 그러면 dependency injection
이 필요한 resolver
는 어떻게 만들어야 할까요?
Resolvers with dependencies
이미 알다시피, dependency injection
은 class
구조에서 작동합니다. 따라서 우린 class
가 필요합니다. 다행히 우리는 class
를 써서 resolver
를 만들 수 있습니다. 우리가 해야하는 유일한 것은, resolver
클래스를 Resolve
인터페이스를 구현(implement)하고, resolve()
메소드를 추가하는 것입니다. 이 resolve()
메소드는 위에서 DI
를 통해 등록한 것과 거의 같은 함수입니다.
아래는 클래스로 contact resolver
를 구현한 것입니다.
1 | import { Injectable } from '@angular/core'; |
resolver
가 클래스가 되면, 클래스는 provider token
으로 사용될 수 있기 때문에 provider
구성은 매우 심플해집니다.
1 | @NgModule({ |
그리고 동일한 토큰을 route에 resolver
로 사용하면 됩니다.
1 | export const AppRoutes: Routes = [ |
Angular는 resolver
가 함수인지, 클래스인지, resolve()
를 호출하는 클래스인지 탐지하기에 충분히 똑똑합니다.
어떻게 Angular가 데이터가 도착할때까지 컴포넌트를 인스턴스화 시키는 것을 지연시키는지(3초), 아래의 데모에서 확인해보세요.