Angular 튜터리얼 - 서비스

Services

Tour of Heroes 앱이 발전함에 따라 Hero 데이터에 액세스 해야하는 더 많은 Component가 추가됩니다.

동일한 코드를 반복해서 복사후 붙여 넣는 대신 재사용 가능한 단일 데이터 Service를 만들어 필요한 Component에 삽입합니다. 별도의 Service를 사용하면 Component를 간결하게 유지하고 View 지원에 주력할 수 있으며 Mock ServiceComponent 테스트를 쉽게 수행할 수 있습니다.

데이터 Service는 언제나 비동기식이므로 Promise 기반 버전의 데이터 서비스로 페이지를 마무리합니다.

이 페이지를 끝내면 앱은 이 라이브 예제/예제 다운로드 처럼 보일 것입니다.

어디까지 했었나?

Tour of Heroes를 계속하기 전에 다음과 같은 구조인지 확인하십시오. 그렇지 않은 경우 이전 페이지로 돌아갑니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
angular-tour-of-heroes
┣ src
┃ ┣ app
┃ ┃ ┣ app.component.ts
┃ ┃ ┣ app.module.ts
┃ ┃ ┣ hero.ts
┃ ┃ ┗ hero-detail.component.ts
┃ ┣ main.ts
┃ ┣ index.html
┃ ┣ styles.css
┃ ┣ systemjs.config.js
┃ ┗ tsconfig.json
┣ node_modules ...
┗ package.json

앱 실행과 트랜스파일을 유지하기

터미널 창에서 다음 명령을 입력하십시오.

1
npm start

이 명령은 TypeScript 컴파일러를 “watch mode”에서 실행하여 코드가 변경되면 자동으로 다시 컴파일 하도록합니다. 또한 이 명령은 앱이 브라우저에서 동시에 실행되고 코드가 변경되면 브라우저를 새로 고칩니다.

다시 컴파일하거나 새로 고치지 않고 브라우저를 일시 정지하지 않아 Tour of Heroes 앱을 계속 만들 수 있습니다.

Hero service 생성하기

이해 관계자들은 다양한 페이지에서 Hero들을 다양한 방식으로 보여주기를 원합니다. 사용자는 이미 목록에서 Hero를 선택할 수 있습니다. 조만간 최고 실적 Hero 들과 함께 Dashboard를 추가하고 Hero 세부 사항을 편집할 수 있는 별도의 보기를 만듭니다. 세 가지 모두 Hero 데이터가 필요합니다.

현재, AppComponent는 표시용 Mock Hero들을 정의합니다. 그러나 Hero들을 정의하는 것은 Component의 임무가 아니며 Hero들 목록을 다른 Component 및 View와 쉽게 공유할 수 없습니다. 이 페이지에서 Hero들 데이터 수집 비즈니스를 데이터를 제공하는 하나의 Service로 옮기고 해당 Service를 데이터가 필요한 모든 Component와 공유합니다.

HeroService 생성

app 폴더에 hero.service.ts라는 파일을 만듭니다.

서비스 파일의 명명 규칙은 소문자로된 Service 이름과 .service입니다. 다중 단어 Service 이름의 경우 대시 - 소문자를 사용합니다. 예를 들어, SpecialSuperHeroService의 파일 이름은 special-super-hero.service.ts입니다.

클래스 이름을 HeroService로 지정하고 다른 곳에서 import할 수 있도록 export 합니다.

src/app/hero.service.ts (starting point)

1
2
3
4
5
import { Injectable } from '@angular/core';
@Injectable()
export class HeroService {
}

Injectable 서비스

Angular Injectable 함수를 가져 와서 이 함수에 @Injectable()` Decorator를 적용했습니다.

1
괄호를 잊마세요. 괄호를 생략하면 진단하기 어려운 오류가 발생합니다.

@Injectable() Decorator는 TypeScript가 Service에 대한 메타 데이터를 내보내도록 지시합니다. 메타 데이터는 Angular가 이 Service에 다른 종속성을 주입해야할 수도 있음을 나타냅니다.

HeroService는 현재 종속성이 없지만 처음부터 @Injectable() Decorator를 적용하면 일관성이 있고 미래에 대한 보장이 됩니다.

Hero 데이터 가져오기

getHeroes () 메소드 스텁을 추가합니다.

src/app/hero.service.ts (getHeroes stub)

1
2
3
4
@Injectable()
export class HeroService {
getHeroes(): void {} // stub
}

HeroService는 웹 서비스, 로컬 스토리지 또는 Mock 데이터 소스와 같이 어디서나 Hero 데이터를 가져올 수 있습니다. Component에서 데이터 액세스를 제거하면 Hero 데이터가 필요한 Component를 건드리지 않고도 언제든지 implementation에 대한 생각을 바꿀 수 있습니다.

Mock hero 데이터 이동

app.component.ts에서 HEROES 배열을 잘라내어 mock-heroes.ts라는 app 폴더의 새로운 파일에 붙여 넣으십시오. 또한 Heroes 배열은 Hero 클래스를 사용하기 때문에 import {Hero} ...문을 복사하십시오.

src/app/mock-heroes.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Hero } from './hero';
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' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];

HEROES 상수는 HeroService와 같이 다른 곳에서 import할 수 있도록 export됩니다.

HEROES 배열을 잘라낸 app.component.ts에는 초기화되지 않은 heroes 프로퍼티를 추가합니다.

src/app/app.component.ts (heroes property)

1
heroes: Hero[];

Mock hero 데이터 리턴

HeroService로 돌아가서 Mock HEROES을 가져 와서 getHeroes() 메서드에서 리턴합니다. HeroService는 다음과 같습니다.

src/app/hero.service.ts

1
2
3
4
5
6
7
8
9
10
11
import { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
@Injectable()
export class HeroService {
getHeroes(): Hero[] {
return HEROES;
}
}

Hero service import하기

AppComponent로 시작하는 다른 Component에서 HeroService를 사용할 준비가 되었습니다.

HeroServiceimport해서 참조합니다.

src/app/app.component.ts (hero-service-import)

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

HeroService를 new로 사용하지 마세요

AppComponent는 어떻게 런타임에 HeroService의 인스턴스를 획득할수 있을까요?

다음과 같이 HeroService의 새 인스턴스를 new로 만들 수 있습니다.

src/app/app.component.ts

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

그러나 이 옵션은 다음과 같은 이유로 이상적이지 않습니다.

  • ComponentHeroService를 만드는 방법을 알아야합니다. HeroService 생성자를 변경하면 Service를 생성한 모든곳을 찾아서 업데이트해야합니다. 여러 곳에서 코드를 패치하는 것은 오류가 발생하기 쉽고 테스트 부담이 가중됩니다.
  • new를 사용할 때마다 Service를 만듭니다. Service가 다른 곳에서 Hero를 cache되고 그렇게 cache되는 Hero가 공유된다면 어떨까요? new로 생성되는 Service는 그렇게 할 수 없습니다.
  • AppComponentHeroService의 특정 구현에 고정되어 있기 때문에, 오프라인으로 작동하거나 테스트를 위해 다른 Mock 버전을 사용하는 것과 같은 다른 시나리오에 대한 구현을 전환하는 것은 어려울 수 있습니다.

Inject the HeroService

new를 사용하는 대신 아래의 내용을 추가합니다.

  • private 프로퍼티를 정의하는 생성자를 추가합니다.
  • Componentproviders 메타 데이터에 추가합니다.

생성자를 추가합니다.

src/app/app.component.ts (constructor)

1
constructor(private heroService: HeroService) { }

생성자 자체는 아무 것도하지 않습니다. 파라미터는 private heroService 프로퍼티를 정의하고 HeroService 주입 대상으로 식별합니다.

이제 Angular는 AppComponent를 생성할 때 HeroService의 인스턴스를 제공하는 것을 알고 있습니다.

의존성 주입 페이지에서 의존성 주입에 대해 자세히 읽어보십시오.

Injector HeroService를 만드는 방법을 아직 모릅니다. 이 코드를 지금 실행 하면 아래와 같은 오류로 인해 Angular가 실패합니다.

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

Injector에게 HeroService를 만드는 법을 알려주기 위해 @Component 호출의 Component 메타 데이터의 아래에 다음 providers 배열 프로퍼티를 추가합니다.

src/app/app.component.ts (providers)

1
providers: [HeroService]

providers 배열은 Angular에게 AppComponent를 생성할 때 HeroService의 새로운 인스턴스를 생성하도록 지시합니다. AppComponent와 그 자식 Component는 그 Service를 사용하여 Hero 데이터를 얻을 수 있습니다.

AppComponent 안의 getHeroes() 함수

Service는 private heroService 변수에 있습니다.

한 줄로 서비스를 호출하고 데이터를 가져올 수 있습니다.

src/app/app.component.ts

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

실제로 한 줄을 감싸는 전용 메서드가 필요하지 않지만 작성합니다.

src/app/app.component.ts (getHeroes)

1
2
3
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}

ngOnInit lifecycle hook

AppComponent는 Hero 데이터를 가져와 표시합니다.

생성자에서 getHeroes() 메서드를 호출하는 유혹을 받을 수도 있지만, 생성자는 복잡한 로직, 특히 데이터 액세스 메소드와 같이 서버를 호출하는 로직을 포함해서는 안됩니다. 생성자는 프로퍼티를 생성자 파라미터와 연결하는 것과 같은 간단한 초기화를 위한 것입니다.

Angular에서 getHeroes()를 사용하게 하기위해 Angular ngOnInit 라이프 사이클 훅을 구현할 수 있습니다. Angular는 Component 라이프 사이클의 중요한 순간 즉, 생성, 변경, 최종 파괴시의 작업을 위한 인터페이스를 제공합니다.

각 인터페이스별로 하나의 메서드를 지원합니다. Component가 해당 메서드를 구현하면 적절한 시간에 Angular가 메서드를 호출합니다.

Lifecycle Hooks 페이지에서 라이프 사이클 후크에 대해 자세히 읽어보십시오.

다음은 OnInit 인터페이스의 핵심 개요입니다(이것을 코드에 복사하지 마십시오).

src/app/app.component.ts

1
2
3
4
5
6
import { OnInit } from '@angular/core';
export class AppComponent implements OnInit {
ngOnInit(): void {
}
}

OnInit 인터페이스에 대한 구현을 export 문에 추가합니다.

1
export class AppComponent implements OnInit {}

내부에 초기화 로직을 가진 ngOnInit 메소드를 작성합니다. Angular는 적당한 시각에 getHeroes() 를 호출하여 초기화합니다.

src/app/app.component.ts (ng-on-init)

1
2
3
ngOnInit(): void {
this.getHeroes();
}

앱이 실행되고 예상대로 Hero 이름을 클릭하면 Hero들 목록과 Hero 상세보기가 표시됩니다.

Async services and Promises

HeroService는 Mock Heroes의 목록을 즉시 반환합니다; 그것의 getHeroes()가 동기식이기 때문입니다.

src/app/app.component.ts

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

결국, Heroes 데이터는 원격 서버에서 가져옵니다. 원격 서버를 사용할 때 사용자는 서버가 응답할 때까지 기다릴 필요가 없습니다. 또한 대기 중에는 UI를 차단할 수 없습니다.

응답에 따라 View를 조정하기 위해 Promise를 사용할 수 있습니다. 이것은 getHeroes() 메서드를 변경하는 비동기 방법입니다.

Hero 서비스를 Promise로 만들기

Promise는 본질적으로 결과가 준비될 때 콜백할 것을 약속합니다. 비동기 Service에 작업을 수행하고 콜백 기능을 제공하도록 요청합니다. Service는 그 작업을 수행하고 결과 또는 오류 함수를 호출합니다.

이것은 간단한 설명입니다. Exploring ES6ES2015 비동기 프로그래밍 Promise 페이지에서 Promise에 대해 자세히 읽어보십시오.

Promise를 반환하는 getHeroes() 메서드로 HeroService를 업데이트합니다.

src/app/hero.service.ts (excerpt)

1
2
3
getHeroes(): Promise<Hero[]> {
return Promise.resolve(HEROES);
}

여전히 Mock 데이터를 사용하고 있습니다. 결과적으로 Mock Hero를 이용해 즉시 Resolve된 Promise를 반환하여 초고속, 제로 레이턴시 서버의 동작을 시뮬레이션합니다.

Act on the Promise

HeroService를 수정하여 this.heroes는 이제 Heroes의 배열이 아닌 Promise로 설정되었습니다.

src/app/app.component.ts (getHeroes - old)

1
2
3
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}

Resolve될 때 Promise에 맞춰 구현을 변경해야합니다. Promise가 성공적으로 Resolve되면, 여러분은 Heroes를 보여줄 수 있습니다.

콜백 함수를 Promise then() 메서드의 파라미터로 전달합니다.

src/app/app.component.ts (getHeroes - revised)

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

Arrow 함수에서 설명한 것처럼 콜백의 ES2015 Arrow 함수는 동등한 다른 함수 표현식보다 간결하며 this를 정상적으로 처리합니다.

콜백은 Componentheroes 프로퍼티를 Service가 반환한 heroes 배열에 설정합니다.

앱이 아직 여전히 실행 중입니다. Hero들 목록을 표시하고 이름을 선택할 경우 상세보기를 보여줍니다.

이 페이지의 끝에서 [부록 : 느린 속도]를 이용하면 연결 상태가 좋지 않은 응용 프로그램의 모습을 보여줍니다.

앱 구조 리뷰

모든 리팩토링 후에 다음 구조를 가지고 있는지 확인합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
angular-tour-of-heroes
┣ src
┃ ┣ app
┃ ┃ ┣ app.component.ts
┃ ┃ ┣ app.module.ts
┃ ┃ ┣ hero.ts
┃ ┃ ┣ hero-detail.component.ts
┃ ┃ ┣ hero.service.ts
┃ ┃ ┗ mock-heroes.ts
┃ ┣ main.ts
┃ ┣ index.html
┃ ┣ styles.css
┃ ┣ systemjs.config.js
┃ ┗ tsconfig.json
┣ node_modules ...
┗ package.json

이 문서에서 설명하는 코드 파일은 다음과 같습니다.

src/app/hero.service.ts

1
2
3
4
5
6
7
8
9
10
11
mport { Injectable } from '@angular/core';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
@Injectable()
export class HeroService {
getHeroes(): Promise<Hero[]> {
return Promise.resolve(HEROES);
}
}

src/app/app.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import { Component, OnInit } from '@angular/core';
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<h2>My Heroes</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes"
[class.selected]="hero === selectedHero"
(click)="onSelect(hero)">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</li>
</ul>
<hero-detail [hero]="selectedHero"></hero-detail>
`,
styles: [`
.selected {
background-color: #CFD8DC !important;
color: white;
}
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes .text {
position: relative;
top: -3px;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
`],
providers: [HeroService]
})
export class AppComponent implements OnInit {
title = 'Tour of Heroes';
heroes: Hero[];
selectedHero: Hero;
constructor(private heroService: HeroService) { }
getHeroes(): void {
this.heroService.getHeroes().then(heroes => this.heroes = heroes);
}
ngOnInit(): void {
this.getHeroes();
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
}

src/app/mock-heroes.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Hero } from './hero';
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' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];

어디까지 했나?

이 문서에서 공부한 내용은 다음과 같습니다.

  • 여러 Component가 공유할 수있는 Service 클래스를 만들었습니다.
  • AppComponent가 활성화될 때 ngOnInit 라이프 사이클 후크를 사용하여 Hero 데이터를 가져 왔습니다.
  • HeroServiceAppComponentproviders로 정의했습니다.
  • Mock Hero들 데이터를 생성하여 Service로 가져 왔습니다.
  • ServicePromise를 리턴하도록 설계했으며, ComponentPromise에서 데이터를 가져오도록 했습니다.

앱은 이 라이브 예제/예제 다운로드와 비슷해야합니다.

앞으로의 여정

Tour of Heroes는 Component 및 공유 Service를 사용하여 재사용이 편하게 되었습니다. 다음 목표는 대시 보드를 작성하고, View 사이에 라우트하는 메뉴의 링크를 추가하고, 템플릿에서 데이터를 규격화하는 것입니다. 앱이 발전함에 따라 앱을 쉽게 디자인하여 확장 및 유지 관리하는 방법을 발견하게됩니다.

다음 튜토리얼 페이지에서 Angular Component Router 및 View 사이의 탐색에 대해 읽어보십시오.

Appendix: 천천히 가져오기

느린 연결을 시뮬레이트하려면 Hero 심볼을 가져 와서 HeroServicegetHeroesSlowly() 메서드를 추가하십시오.

app/hero.service.ts (getHeroesSlowly)

1
2
3
4
5
6
getHeroesSlowly(): Promise<Hero[]> {
return new Promise(resolve => {
// Simulate server latency with 2 second delay
setTimeout(() => resolve(this.getHeroes()), 2000);
});
}

getHeroes()처럼 Promise를 리턴합니다. 하지만 이 Promise는 Mock Heroes에서 Promise를 해결하기 전에 2초를 기다립니다.

AppComponent로 돌아가서, getHeroes()getHeroesSlowly()로 대체하고 어떻게 동작하는지보십시오.


이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://angular.io/tutorial/toh-pt4]

참고

공유하기