컴포넌트는 데이터를 직접적으로 fetch하거나 save하지 않아서
데이터를 present하는데에만 집중하고 데이터에 접근하는 거는
서비스에 위임해줌.
이번에는 HeroService
를 만들어서 앱의 모든 클래스들이
hero를 가져오는데 사용할 수 있도록 할 거임.
서비스는 서로의 존재를 모르는 클래스들간에 정보를 공유하기에
좋은 방법임. 여기서는 MessageService
를 만들어서 아래 두 군데에
inject할 거임.
1. HeroService
에 inject해서 메시지를 보내는데 사용함
2. 메시지를 보여주는 MessagesComponent
에 inject해서
메시지도 보여주고 유저가 hero를 클릭하면 ID도 보여줄 것임
/src/app
에서 앵귤러 CLI로 hero
서비스 생성함
ng generate service hero
이 명령어는 src/app/hero.service.ts
에 HeroService
클래스를 생성해줌.
// src/app/hero.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class HeroService {
constructor() { }
}
위에서 만든 새 서비스는 앵귤러 Injectable
symbole을 import해서 클래스를
@Injectable()
로 annotae 하는 것을 볼 수있음. 이렇게 하면 클래스가 dependency injection system
에 참여하도록 하는 거임. HeroService
클래스(src/app/hero.service.ts에 있음)는 이제 injectable service를 제공하게 되고
자신만의 injected dependencies도 가질 수 있게됨. 아직까지는 아무 의존성도 없음.
@Component
데코레이터가 컴포넌트 클래스에 메타데이터 object를 accpet 하는 것처럼
@Injectable()
데코레이터도 같은 방식으로 서비스를 위한 메타데이터 object를 accept해줌.
HeroService
는 영웅들에 대한 정보를 아무데서나 가지고 올 수 있어야함.
(e.g 웹 서비스, 로컬 스토리지, mock data source 등)
컴포넌트에서 데이터에 대한 접근을 없애는 것은(서비스에서 함???정리필요)
컴포넌트의 변경없이 데이터 접근에 대한 방식을 바꿀 수 있다는 뜻임.
컴포넌트들은 서비스가 어떻게 작동하는지 알 필요가 없음.
이 듀토리얼에서는 mock heroes를 계속 사용할 거임.
Hero
인터페이스랑 mock heroes인 HEROES
를 import 해주고
getHeores
메소드를 주가해서 mock heroes를 리턴할 수 있게 해줌.
// src/app/hero.service.ts
import { Injectable } from '@angular/core';
// 추가한 부분 코드 이탤릭채같은거로 표시할 수 있는지 확인해보기
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
@Injectable({
providedIn: 'root'
})
export class HeroService {
getHeroes(): Hero[] {
return HEROES;
}
constructor() { }
}
앵귤러가 HeroService
를 HeroesComponent
에 inject하기 전에 HeroService
를
dependency injection system에 provider로 등록해줘야함. provider는 서비스를
생성하거나 deliver 해줄 수 있는 무언가임. 여기서는 provider가
HeroService
클래스를 인스턴스화해서 서비스를 제공할 수 있도록 해줄거임.
HeroService
가 서비스를 제공할 수 있도록 하기 위해서는, HeroService
를
injector에 등록해야함. injector는 앱이 필요할때 provider를 골라서 inject해주는
객체임.
default로 앵귤러 CLI 커맨드인 ng generate service
는 서비스에 대한
provider를 root injector로 등록함. 이는 provider 메타데이터를 포함해서 할 수 있는데 @Injectable()
데코레이터의 providedIn: 'root'
로 되는거임.
원래는 모듈에 보면 아래 배열에다가 서비스를 등록해주는데
providers: [],
root로 등록하게되면 최상위 모듈인 AppModule에만 등록되서 싱글톤으로 하위모듈들에서도 사용가능함. 서비스는 정의된 모듈마다 인스턴스가 하나씩 생기기 때문에 root로 안하고 providers로 하게되면 각 모듈별로 인스턴스가 생기고 따로 동작함. 따라서 root로 provide하면 한개만 생겨서 싱글톤처럼 되는거임.
// src/app/hero.service.ts
@Injectable({
providedIn: 'root'
})
서비스를 root level에서 provide할 경우에 앵귤러는 공유되는 하나의
HeroService
인스턴스를 만들어서 이를 원하는 클래스에 inject해줌.
provider를 @Injectable
메타데이터 안에 등록하게되면 앵귤러가 서비스가
사용되지 않는다고 판단하면 서비스를 지워서 앱을 최적화 시킬 수 있게해줌.
HeroService
는 이제 HeroesComponent
에서 사용할 준비가 됐음.
HeroesComponent
클래스 파일에서 HEROES
를 삭제하고 HeroService
를 import 해줌.
HEROES
를 삭제했으니 heroes
property도 바꿔줌.
// src/app/heroes/heroes.component.ts
import { HeroService } from '../hero.service';
heroes: Hero[] = [];
HeroService
타입인 private heroService
parameter를 constructor에 추가해줌.
// src/app/heroes/heroes.component.ts
constructor(private heroService: HeroService) { }
이 heroService
parameter는 private parameter인 heroService
를 정의함과 동시에
HeroService
injection site에 identify함.
이거 그냥 생성자로 parameter 정의한거랑 똑같음.
앵귤러가 HeroesComponent
를 생성할때, dependency injection 시스템이 heroService
를 HeroService
의 싱글톤 인스턴스의 parameter로 설정해줌.
서비스에서 heroes를 가져오는 메소드를 만들어줌.
// src/app/heroes/heroes.component.ts
getHeroes(): void {
this.heroes = this.heroService.getHeroes();
}
getHeroes()
를 생성자에서 부를수도 있지만 좋은 방법이 아님. 생성자는 property에 wiring constructor
parameter 하는 것과 같이 최소한의 초기화에만 사용하도록 해야함. 생성자는 HTTP request를 하는
함수를 부르는 것 같은거는 절대 하면 안되고 그냥 아무것도 안해야함.
대신에 getHeroes()
를 ngOnInit lifecycle hook 안에서 부르고 앵귤러가 HeroesComponent
인스턴스를 만든 후에 ngOnInit()
을 호출하게할거임.
// src/app/heroes/heroes.component.ts
ngOnInit(): void {
this.getHeroes;
}
이제 mock-heroes에서 데이터를 가져와서 이전과 같이 동작하는 것을 볼 수 있음.
지금 만든 HeroService.getHeroes()
메소드는 synchronous signature를 가지고 있음. 이 말은 HeroService
가
heroes를 synchronoulsy하게 가져올 수 있다는 뜻임. HeroesComponent
는 getHeroes()
의 결과를
heroes가 synchronoulsy하게 가져와지는 것처럼 사용가능함.
// src/app/heroes/heroes.component.ts
this.heroes = this.heroService.getHeroes();
실제 상용 앱에서는 이렇게 동작하지 않음. 듀토리얼에서는 서비스가 mock heroes를 리턴해주지만
앱이 heroes를 원격 서버에서 가지고 오게되면 synchronous한 작업이 아니라 asynchronous한 작업이됨.
따라서 HeroService
는 서버가 응답할때까지 기다려야해서 getHeroes()
메소드는 hero 데이터를
바로 보내주지 못하게 되지만 서비스를 기다리는 동안 브라우저가 멈추지는(block) 않음.
따라서 HeroService.getHeroes()
는 어떤 형태로든 asynchronous signature를 가져야함.
이 듀토리얼은 HeroService.getHeroes()
가 Observable
을 리턴하도록 해서 HeroService.getHeroes()
가
asynchronous signature를 가지도록 할 것임. 어차피 해당 메소드는 나중에가면 앵귤러의 HttpClient.get
메소드를 사용해서 heroes를 가져오게 될거임. HttpClient.get
은 Observable
을 리턴해줌.
Observable
은 RxJS 라이브러리의 중요한 클래스 중 하나임.
6번째 듀토리얼인 Get Data from a Server에서 앵귤러의 HttpClient
메소드가 RxJS의 Observable
을
리턴하는 것을 배울건데 지금 당장은 서버로부터 데이터를 받는 시뮬레이션을 RxJS의 of()
함수로
할 것임.
HeroService
파일을 열어서 Observable
이랑 of
심볼을 RxJS로 import 하고
getHeroes()
수정해줌.
// src/app/hero.service.ts
import { Observable, of } from 'rxjs';
getHeroes(): Observable<Hero[]> {
const heroes = of(HEROES);
return heroes;
}
of(HEROES)
는 Observable<Hero[]>
을 리턴해주는데 애는 mock heroes라는 하나의 값을 emit함.
HeroService.getHeroes
메소드는 Hero[]
를 리턴했었는데 이제 수정해서 Observable<Hero[]>
를 리턴함.
그렇기 때문에 변경사항을 HeroesComponent
에도 적용해줘야함.
/src/app/heroes/hereos.component.ts에서 getHeroes
메소드를 아래처럼 수정해줌.
// 수정한 버전
getHeroes(): void {
// heroService는 constructor에 private으로 define돼어있음.
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
// 이전 버전
getHeroes(): void {
this.heros = this.heroService.getHeroes();
}
이전 버전에서는 heroes의 배열을 HeroesComponent
의 heroes
proeprty에 할당줬는데
이 작업은 synchronously하게 일어나서 서버가 heores를 바로 리턴해주지 못하면 브라우저가
서버의 응답을 기다릴때까지 멈춰야했음. 이거는 HeroService
가 remote 서버에서 request를 할때는
작동하지 않는 방식임.
따라서 새 버전을 만들었는데, 새 버전은 Observable
이 heroes array를 emit할때까지 기다림.
emit은 바로 될수도있고 몇분 기다려야 할 수도있음. subscribe()
메소드는 emit된 array를 받아서
HerroesComponent
의 heroes
property에 할당해줌.
이 비동기 접근법은 HeroService
가 heroes를 서버에 request할때 동작함.
이 섹션은 아래 작업을 할 것임.
MessageComponent
추가MessageService
생성MessageService
를 HeroService
에 inject하기HeroService
가 heroes 가져오는데 성공하면 메시지 보여주기아래 CLI 명령어로 MessageComponent
생성하기
ng generate component messages
CLI는 src/app/messages 폴더를 만들고 src/app/app.module.ts의 AppModule
에 MessagesComponent
를 추가해줌.
AppComponent
템플릿을 아래처럼 수정해 생성한 MessageComponent
를 보여주게할거임.
<h1>{{title}}</h1>
<app-heroes></app-heroes>
<app-messages></app-messages>
CLI로 MessageService
를 src/app에 만들어줌
ng generate service message
이제 생성한MessageService
를 열어서 아래처럼 수정해줌.
// src/app/message.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear(){
this.messages = [];
}
constructor() { }
}
MessageService
는 messages
와 message를 추가해주는 add()
랑 message를 초기화 해주는 clear()
를 가지고 있음.
HeroService
에서 MessageService
를 import 해주고,
생성자에 private messageService
property를 선언해주는 parameter를 추가해줌. 앵귤러는 MessageService
싱글톤을
// src/app/hero.service.ts
import { MessageService } from './message.service';
constructor(private messageService: MessageService) { }
위 처럼 하는게 전형적인 service-in-service 시나리오임. MessageService
를 HeroService
에 inject했는데 HeroService
는 HeroesComponent
에 inject됨.
getHeroes()
메소드를 수정해서 heroes가 fetch됐을때 메시지를 보내도록함.
// src/app/hero.service.ts
getHeroes(): Observable<Hero[]> {
const heroes = of(HEROES);
this.messageService.add('HeroService: fetched heroes');
return heroes;
}
MessageComponent
는 HeroeService
가 heroes를 fetch 했을때 보낸 메시지를 포함해서 모든 메시지를 보여줄 수 있어야함.
MessageCompnoent
를 열어서 MessageService
를 import 해줌. 그리고 생성자에 아래처럼 public messageService
property를 parameter로 넣어줌.
이제 앵귤러가 MessageComponent
를 생성할때 싱글톤인 MessageService
를 messageService
property에 inject해줌.
// src/app/messages/messages.component.ts
import { MessageService } from '../message.service';
constructor(public messageService: MessageService) {}
messageService
property는 템플릿에 bind 할거여서 무조건 public이어야함. 앵귤러는 public한 component property에만 bind 할 수 있음.
이거 타입스트립트에는 access modifier 안 써주면 기본으로 public이긴한데 constructor에서 access modifier 안써주게되면 constructor의 scope에서만 적용되서 써줘야함.
일반적인 함수에서 인자를 받았을때 인자를 함수에서만 쓸 수 있는거랑 똑같음.
MessageComponent
템플릿을 아래처럼 변경해줌.
<!--
src/app/messages/messages.component.html
-->
<div *ngIf="messageService.messages.length">
<h2>Messages</h2>
<button type="button" class="clear"
(click)="messageService.clear()">Clear messages</button>
<div *ngFor='let message of messageService.messages'> {{message}} </div>
</div>
이 템플릿은 messageService
에 직접적으로 바인드 됨.
*ngIf
로 보여줄 메시지가 있을때만 보여줌.*ngFor
로 <div>
로 메시지들보여줌.MessageService.clear()
에 바인드함.아래는 유저가 hero를 클릭할때마다 메시지를 보내고 보여주게함. 따라서 클릭할때마다 메시지가 나와서 클릭기록도 남음.
// src/app/heroes/heroes.component.ts
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
import { MessageService } from '../message.service';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
selectedHero?: Hero;
heroes: Hero[] = [];
constructor(private heroService: HeroService, private messageService: MessageService) { }
ngOnInit(): void {
this.getHeroes();
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
// ${} 이거 js template literal임
// python fstring 같은거임
this.messageService.add(`HeroesComponent: Selected hero id=${hero.id}`);
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
}
- data access 기능을
HeroService
클래스로 리팩토링했음.HeroService
를 해당 서비스의 provider로 root level에 등록해서 앱의 어디에서나 injet할 수 있게 만들었음.- Angular Dependency Injection를 사용해서 component를 inject 했음.
HeroService
의 getHeroes() 메소드에 async한 signature를 줬음- RxJS
of()
로 mock heroes의 observable을 리턴해줬음.(getHeroes(): Observable<Hero[]>)- 컴포넌트의
ngOnIint
lifecycle hook은 constructor가 아닌HeroService
의 메소드를 불렀음.// src/app/heroes/heroes.copmonent.ts ngOnInit(): void { this.getHeroes(); }
HeroService
는 자신에 injected된 서비스인MessageService
랑 같이 다른 컴포넌트에 injected 됐음.