AngularJS/AngularJS

▶ [Angular2 Tutorial] 5. Service

비주얼라이즈 2016. 9. 23. 16:09

▶ [Angular2 Tutorial] 5. Service


이번 글에서는 Angular.io 에서 제공하는 TUTORIAL 중 다섯 번째인 'Service'에 대한 내용을 정리해보고자 한다.




Service


여러 Component는 hero 데이터에 대한 Access를 필요로 한다. 그러나, 우리는 계속 같은 코드를 복사&붙여넣기를 하고 싶지 않다. 같은 코드를 복붙하는 대신에, 우리는 하나의 재사용가능한 Data Service 를 생성하고, 필요한 구성에 맞게 주입(Inject)하는 방법에 대해 이해할 필요가 있다.


Refactoring Data Access는 서비스를 분리 유지하여 component가 view를 supporting 하는 데 초점을 맞출 수 있도록 했다. 이것은 또한 component와 mock service의 단위 테스트를 쉽게 할 수 있도록 하는 환경이 된다.


Data Service는 변함없이 비동기(asynchronous)이기 때문에, 이 챕터에서는 Promise-based version of data service을 만드는 것까지 다룬다.


Angular에서 제공하는 Tutorial - 5. Service Live Example







Where we left off


이 글에서 소개하고 있는 것은 Angular.io의 공식 튜토리얼이며, 이 문서는 일곱가지 챕터 중 다섯 번째 챕터이다. 이 문서에서 정리하는 내용에 대한 것은 기본적으로 다음과 같은 환경과 디렉터리 구조를 바탕으로 한다.



이미지 : 이 문서를 시작하는 디렉터리구조 (angular.io > tutorial > service)


만약 이에 대한 준비가 되어있지 않다면, live example을 통해 확인하거나, angualr.io 튜토리얼의 introduction 부터 차근차근 따라서 학습하고, 만들어오는 편이 낫다. 둘다 귀찮고, 이 문서안에서의 디렉토리 형태가 궁금하다면 이전 챕터의 가장 마지막페이지에 나오는 디렉토리 코드를 복붙하면된다.


이전 챕터와 마찬가지로, application을 실행한 후 본격적인 내용으로 들어간다.


npm start





Creating Hero Service


우리의 stackholder는 우리의 application에 대한 자신의 큰 비전을 공유하고있다. 그들은 우리에게 hero들에 대해 다양한 방법으로 다른 페이지에 표현하는 것을 보고싶다고 말한다. 우리는 이미 목록에서 hero를 선택할 수 있다. 곧 우리는  대시보드를 추가하고, hero 편집기능과 hero의 상세정보 페이지를 나누어 표현하도록 만들어 볼 것이다. 여기서 말한 세 가지 view를 표현하기 위해서는 각각의 view 모두 hero data가 필요하다.


AppComponent는 표현하기 위해 mock heroes를 즉시 정의한다. 우리에게는 적어도 두 개의 objections가 있다. 

  1. heores정의(define)하는 일Component의 일이 아니다.

  2. 다른 Componentview를 공유하기가 쉽지 않다.


우리는 이 "영웅데이터 수집사업(hero data acquisition business)"을 하나의 single service로 리팩토링하고, 그리고 hero를 필요로하는 모든 ComponentService를 공유(Share)할 수 있다.

무슨말인지 모르겠다면, 다음 내용까지 한번 더 듣고 생각해보자.







Create the HeroService


app 폴더에 hero.service.ts 라는 이름의 파일을 생성한다.


angular의 convention 규칙에따라, service 파일에 대해서는 '파일명.service.ts' 의 형태로 네이밍하고있다.

새로 생성한 hero.service.ts 파일에 HeroService 라는 class를 생성하고 export한다. 이 class는 다른 곳에서 import해서 쓸 수 있도록 @Injectable 설정도 해준다.


import { Injectable } from '@angular/core';

@Injectable()
export class HeroService {
}








Injectable Service

우리는 Injectable function을 import 했고, 이 Injectable function 은 @Injectable() decorator로 적용된다.


@Injectable()에서 괄호를 생략하면 찾기 힘든 오류로 이어지기때문에, 꼭 붙여주자!


TypeScript는 @Injectable() decorator를 보고 우리의 Service에 대한 metadata를 방출(emit)한다. metadata that Angular may need to inject other dependencies into this service. 


우리가 이 @Injectable()를 추가하는 상황에서는 HeroService가 의존성을 가지고있는 것은 아니지만, 추가해준다. 이 HeroService를 처음부터 그런 목적으로 만들었다면, Injectableimport 하고, @Injectable decorator를 붙여주는 이 일련의 작업들은 시작부터 해주는것이 일관적인 작업을 하는데 도움이 된다.





Getting Heroes


getHeroes method를 추가해준다.


app/hero.service.ts


@Injectable()
export class HeroService {
    getHeroes(): void {} //stub
}

위 코드와같이 HeroService class에 getHero() 라는 메서드를 추가한다. 


우리가 만드는 Service를 사용하는 지점에서는, 이 HeroService가 어떻게 데이터를 가져오는지 알 수 없다. 우리의 HeroServicehero의 데이터를 어디에서나 얻을 수 있다. 웹 서비스에서도 얻을 수 있고, 로컬저장소에서, 또는 임의의 데이터셋을 설정해서 그것을 가져올 수도 있다.


Component에서 data  access 부분을 없애버린다는 것 자체가 "아름답다"라고 말할 수 있는 부분이다. 이로써 우리는 hero를 필요로하는 Component는 건드리지 않은채로, 어떻게 구현할지에 대한 고민을 자주, 자유롭게할 수 있게된다.




Mock Hero


우리는 이미 AppComponent에 Hero에 대한 Mock data(임의로 만든 데이터) 를 가지고있다. 다음과 같은 코드를 말한다.


app/app.component.ts


//...
export const HEROES: Hero[] = {
    { id: 11, name: 'Mr. Nice },
    { id: 12, name: 'Narco' },
    { id: 13, name: 'Bombasto' },
    { id: 14, name: 'Celeritas'},
    { id: 15, name: 'Magneta' },
    { id: 16, name: 'RubberMan' },
    { id: 17, name: 'Dynama' },
    { id: 18, name: 'Dr IQ' },
}
//...

이제 이 부분을 app/app.component 에서 잘라내기 한 후, app폴더 아래에 mock-heroes.ts 라는 새로운 파일을 생성후 붙여넣기 한다. 그리고 이와함께 import { Hero } ...부분도 함께 복사해서 붙여넣기 해준다. 이것은 우리가 복붙한 heroes라는 array가 Hero 클래스를 사용하기 때문이다.


이 과정으로 만들어지는 mock-heroes.ts 파일의 구성은 다음과 같다.

app/mock-heroes.ts


import { Hero } from './herp';

export const HEROES: Hero[] = {
    { id: 11, name: 'Mr. Nice },
    { id: 12, name: 'Narco' },
    { id: 13, name: 'Bombasto' },
    { id: 14, name: 'Celeritas'},
    { id: 15, name: 'Magneta' },
    { id: 16, name: 'RubberMan' },
    { id: 17, name: 'Dynama' },
    { id: 18, name: 'Dr IQ' },
}

우리는 HEROES constantexport했다. 따라서 우리는 이것을 다른 어딘가에서 import할 수 있게된다. (예를들면 우리의 HeroService와 같이.)


그리고 다시 app.component.ts 로 돌아가 초기화되지않은(uninitialized) heroes property를 남겨둔다. 


app/app.component.ts (heroes property)


heroes: Hero[];

이제 app.component와 mock-heroes.ts 파일에 대한 설정을 마쳤다.






Return Mocked Heroes


이제 다시 HeroService 로 돌아가서 방금 만든 mock HEROES를 import 하고, getHeroes method로부터 이것을 리턴한다. 백문이 불여일견. 우리가 만든 HeroService 는 다음과 같다.


app/hero.service.ts


import { Injectable } from '@angular/core';

import { Hero } from './hero';
import { HEROES }  from './mock-heroes';

@Injectable()
export class HeroService {
    getHeroes(): Hero[] {
        return HEROES;
    }
}

여기까지 HeroService 에 대한 구성을 마쳤다. 아래에서는 HeroService 를 사용하는 방법에 대한 내용을 다룬다.







Use the Hero Service


앞서 다룬 내용을 바탕으로 AppComponent로 시작하는 다른 Component에HeroService를 사용할 준비를 마쳤다. 


뭔가 우리가 필요할 때 import 해서 사용했던 것 처럼, HeroService도 import 구문으로 넣어준다.


import { HeroService }  from './hero.service';

service를 import 함으로써 우리는 우리의 코드에서 이것을 참조(reference)할 수 있게 된다.


How should theAppComponent acquire a runtime concrete HeroService instance?






방금만든 HeroService를 new로 활용할 수 있나?

Do we new the HeroService? No way!


물론! 우리는 방금만든  HeroeService 의 새로운 instance를  new  구문으로 만들 수 있긴하다. 


heroService = new HeroService(); //don't do this!

 할 수는 있으나, 안티패턴이다. 이에대한 몇 가지 이유가 있는데, 다음과 같다.


  • 우리의 Component는 HeroService를 만드는 방법을 알고있다.
    • 만약 우리가 HeroService 생성자(constructor)를 변경하는경우, 우리는 우리가 service를 생성한 모든 지점을 다시 찾아야하고, 찾은 다음엔 수정해주어야 한다.
  • 새로 사용할 때 마다 새로운 서비스를 만들 수는 있다. 그러나.
    • 만약 어떤 서비스가 Hero를 cache하고 다른 사람들과 그 cache를 공유하는 경우를 생각한다면? 그건 가능하지않다.
  • 우리는 HeroService에 대한 특정 구현에 대해 AppComponent에 맞물려 잠겨(locking)있다. 따라서,
    • 다른 시나리오(scenarios)가 있다고 하더라도, 구현했던 것들을 그대로 옮겨가는 것은 쉽지 않을 것이다.
    • 새로운 mock version을 가지고 test 해야할까 생각해보면 그것도 마찬가지로 쉽지 않다.








new대신 constructor, component를 만져 Inject하기

Inject the HeroService


new로 생성했던 것을 다음 두 가지 항목에 따라 바꿔준다.

  1. constructor를 추가하고, private property를 정의한다.
  2. Component에  providers  metadata를 추가한다.
앞서말한 constructor의 형태는 다음과 같다.

app/app.component.ts

constructor(private heroService: HeroService){ }

constructor는 스스로는 아무것도 하지않는다. 매개변수(parameter)가  private heroService property를 정의하는 동시에, 이것을 HeroServiceinjection site로 식별한다. 


위와 같은 코드를 통해, Angular는 새로운 AppComponent가 생성될 때 HeroService의 instance를 제공하는 방법을(해야함을) 알게 된다.


Dependency Injection에 대한 더 자세한 설명은 Angular.io의 Dependency Injection 페이지를 참고하자.


Injector는 아직 HeroService를 생성하는 방법을 알 수 없다. 지금 상태에서 코드를 실행할 경우, 다음과 같은 오류메시지를 보게 될 것이다.


EXCEPTION: No provider for HeroService! (AppComponent -> HeroService)


우리는 injector에게 어떻게 HeroService를 생성하는지를 알려줘야한다. 알려준다는 것이 별다른 것은 아니고, HeroService provider를 등록하는 것을 말한다. 다음 코드를 확인해보자.


providers: [HeroService]



 providers  array는 Angular에게, AppComponent가 새롭게 생성될 때, HeroService의 instance를 생성하라고 알려준다. 


이에따라, AppComponent는 service를 사용할 수 있게되어 heroes를 얻을 수 있게 될 뿐만아니라, 이 AppComponent의 Component tree에 있는 child component에서도 이를 사용할 수 있게 된다.









getHeroes in the AppComponent


우리는 heroServcie 라는 service를 private variable 형태로 가지고있다. 이것을 사용해보자.


우리는 service를 호출(call)하고, 데이터를 얻는 코드를 한줄로 작성할 수 있다. 


this.heroes = this.heroesService.getHeroes();

우리는 한줄의 코드를 감싸는 것을 위한 전용 method가 있는 것은 아니다. 일단 우리는 다음과 같이 쓰자.


getHeroes(): void {
    this.heroes = this.heroesService.getHeroes();
}








The ngOnInit Lifecycle Hook


AppComponent는 별 소란 없이 heroes를 가져오고, 또 view에 표현해야한다. 우리는 어디서 getHeroes method를 호출해야할까? constructor에서? 아니다!


constructor는 constructor의 매개변수(parameter)를 property로 wiring하는 정도의 간단한 초기화작업을 위한 것이다. 이외에 어떤 무겁게 돌아가는 작업을 위한 것이 아니다. 


우리는 test에서 component를 생성하면서 이것이 실제 동작할 수 있음에 대해 걱정하지 말아야한다. (서버를 호출하는 것처럼) 


 We should be able to create a component in a test and not worry that it might do real work — like calling a server! — before we tell it to do so.


constructor가 아니더라도 다른 방법으로 getHeroes를 호출할 수 있다.

만약 우리가 Angular ngOnInit Lifecycle hook을 구현한다면, Angular는  이것을 호출할 수 있다.  Angular는 componet lifecycle에서의 중요한 point에서 활용할 수 있는 몇가지 interface를 제공한다. 예를들어 생성시, 각각 변경 후 등.


각 interface는 하나의 method를 가지고 있다. component가 해당 method를 활용하여 구성할 때, Angular는 구성에 따라 적절한 시기에 그것들을 호출(call)한다.


이에대해 더 자세한 설명은 Angular.io의 Lifecycle Hooks 챕터에서 확인할 수 있다.


다음 코드를 통해 OnInit interface에 대한 핵심개요를 살펴볼 수 있다.

app/app.component.ts (ngOnInit stub)


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

export class AppComponent implements OnInit {
    ngOnInit(): void{
    }
}

우리는 ngOnInit method를 우리의 초기화 로직(initialization logic) 안에서 작성한다. 그리고 Angular에게 적절한 시기에 이것을 호출할 수 있도록 코드를 남겨둔다. 이 예제의 경우에서는 getHeroes를 호출하는 것으로 초기화를 실행한다.


ngOnInit(): void {
    this.getHeroes();
}

코드에따라, application은 (우리가 기대한 대로) hero의 목록을 표현하고, 우리가 click 했을 때 hero의 상세정보를 표현한다. 


여기까지의 과정을 통해 우리가 Service 라는 개념을 Angular에서 어떻게 작성하고, 적용되는지에 대해 대략적으로 살펴보았다. 뭔가 목표에 가까워지고 있는 것 같기도한데, 또 뭔가 확실히 옳은 방법은 아닌 것 같다는 느낌이다.









Async Service and Promise


우리의 HeroService는 mock hereos list를 그 즉시 리턴한다. Its getHeroes signature is synchronous


this.heroes = this.heroService.getHeroes();

heroes에 대한 요청을 하고, 그 결과는 리턴된 결과에 있다. 


언젠가 우리는 원격서버(remote server)에서 hero를 얻게 될 것이다. 우리는 아직 http를 호출하지 않는다.  우리는 다음장인 router chapter에서 이러한 작업을 처리해볼 예정이다.


우리가 처리를 할 때, server의 응답을 기다려야하는데, 우리가 응답을 기다릴 동안 사용자에게 보여질 인터페이스(UI)를 차단할 수 는 없다. 따라서 우리는 동기(synchronous)방식이 아닌, 비동기(asynchronous)방식의 어떤 기술을 사용해야만 하고, 그에따라 우리가 앞서 작성했던 getHeroes method signature도 변경해주어야한다.


이러한 작업을 처리하기위해 이 예제에서는 Promises 를 사용한다.







The Hero Service makes a Pomise


Promise는 요청한 어떤 결과가 준비가 되면, 그때 요청에 대한 처리를 하는 방법의 프로세스이다.  


우리는 여기서 어떤 작업을 처리하고, 결과를 콜백함수(callback function)으로 제공하는 비동기 서비스(asynch service)를 요청한다. 


이 예제 에서는 결과적으로 이것이 동작하거나, 오류가 났을때 우리의 함수를 처리하도록 하는 흐름으로 작업을 진행한다.


Promise를 포함한 ES2015에 대한 더 자세한 설명은 이 페이지를 통해 알아보자.


HeroServicePromise-returning getHeroes method로 업데이트한다.

app/hero.service.ts (excerpt)


getHeroes(): Promise {
    return Promise.resolve(HEROES);
}

이것은 여전히 mock data이다. 


우리는 여기서 mock heroes를 가지고 zero-latency server 위에서 즉시 resolved Promise를 반환하는 시뮬레이션을 해보고있다. 우리의 implementationPromise에서 resolve 할 때 변경해주어야한다. 


Promise가 성공적으로 solve 하면, 우리는 heroes를 화면에 표현할 수 있을 것이다. 우리의 콜백함수를 Promise의 인수로 전달한다. (Promise의 then 이후 처리를 위해)


app/app.component (getHeroes - revised)


getHeroes(): void { 
    this.heroService.getHeroes().then(heroes => this.heroes = heroes);
}



위 코드에서 '=>' 부분은 ES2015의 arrow function이다. 이에대한 자세한 내용은 이 페이지를 참고하자. 


우리의 콜백은 component의 heroes property를 service에 의해 리턴된 heroes 배열로 설정한다. 

우리의 app은 여전히 hero의 목록을 보여주며, 선택에 따라 hero 상세정보를 보여주어야한다.


poor connection 환경에서 Promise를 처리하는 노하우에 대해서는 알고싶다면,  take it slow 설명을 참고해보자.








Review the App Structure


이 chapter 에서 다뤘던 디렉토리 구조를 한번더 체크해보자. 다음 챕터인 Router에서 이 디렉터리 구조로 시작하게된다.








이번 글에서 다뤘던 내용들



  • 여러 Component가 공유할 수 있는  Service class를 생성했다.
  • AppComponent가 활성활 될 때 특정 작업(여기서는 hero data)을 처리하기위해 ngOnInit Lifecycle hook을 사용했다.
  • AppComponent에 대한 providerHeroService를 정의했다.
  • AppComponent에서 임의로 작성했던 mock dataservice로 옮겨 작업을 처리했다.
  • service에서 Promise를 리턴하도록하고, Component는 이 Promise에서 data를 얻도록 application을 디자인했다.





Appendix: Take It slow


우리는 slow connection 에 대해서도 시뮬레이션 할 수 있다.

Hero symbol을 import 하고, 아래 코드와같이 getHeroesSlowly method를 HeroService에 추가한다.

app/hero.service.ts (getHeroesSlowly)


getHeroesSlowly(): Promise {
    return new Promise(resolve =>
        setTimeout(resolve, 2000)) //delay 2 seconds
        .then(() => this.getHeroes());
}

getHeroes와 같이, 이것 역시 Promise를 리턴한다. 하지만 이 Promise의 경우, Promise resolve를 하기전에 2초를 기다린다.


다시 AppComponent로 돌아가서, heroService.getHeroesheroService.getHeroesSlowly로 바꾸고, application을 실행시켜 어떤 변화가 있는지 살펴보자.




[Angular2 Tutorial] 다음 chapter Routing에 대한 내용을 정리할 계획입니다.