Tutorial: Tour of Heroes - Add Services

kukudas·2022년 2월 16일
0

Angular

목록 보기
8/15

Why services

컴포넌트는 데이터를 직접적으로 fetch하거나 save하지 않아서
데이터를 present하는데에만 집중하고 데이터에 접근하는 거는
서비스에 위임해줌.

이번에는 HeroService를 만들어서 앱의 모든 클래스들이
hero를 가져오는데 사용할 수 있도록 할 거임.

서비스는 서로의 존재를 모르는 클래스들간에 정보를 공유하기에
좋은 방법임. 여기서는 MessageService를 만들어서 아래 두 군데에
inject할 거임.
1. HeroService에 inject해서 메시지를 보내는데 사용함
2. 메시지를 보여주는 MessagesComponent에 inject해서
메시지도 보여주고 유저가 hero를 클릭하면 ID도 보여줄 것임

Create the HeroService

/src/app에서 앵귤러 CLI로 hero 서비스 생성함

ng generate service hero

이 명령어는 src/app/hero.service.tsHeroService 클래스를 생성해줌.

// src/app/hero.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class HeroService {

  constructor() { }
}

@Injectable() services

위에서 만든 새 서비스는 앵귤러 Injectable symbole을 import해서 클래스를
@Injectable()로 annotae 하는 것을 볼 수있음. 이렇게 하면 클래스가 dependency injection system
에 참여하도록 하는 거임. HeroService 클래스(src/app/hero.service.ts에 있음)는 이제 injectable service를 제공하게 되고
자신만의 injected dependencies도 가질 수 있게됨. 아직까지는 아무 의존성도 없음.

@Component 데코레이터가 컴포넌트 클래스에 메타데이터 object를 accpet 하는 것처럼
@Injectable() 데코레이터도 같은 방식으로 서비스를 위한 메타데이터 object를 accept해줌.

Get hero data

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() { }
}

Provide the HeroService

앵귤러가 HeroServiceHeroesComponent에 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에서 사용할 준비가 됐음.

Update HeroesComponent

HeroesComponent 클래스 파일에서 HEROES를 삭제하고 HeroService를 import 해줌.
HEROES를 삭제했으니 heroes property도 바꿔줌.

// src/app/heroes/heroes.component.ts
import { HeroService } from '../hero.service';

heroes: Hero[] = [];

Inject the Service

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로 설정해줌.

Add getHeroes()

서비스에서 heroes를 가져오는 메소드를 만들어줌.

// src/app/heroes/heroes.component.ts
getHeroes(): void {
    this.heroes = this.heroService.getHeroes();
}

Call it in ngOnInit()

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에서 데이터를 가져와서 이전과 같이 동작하는 것을 볼 수 있음.

Observable data

지금 만든 HeroService.getHeroes() 메소드는 synchronous signature를 가지고 있음. 이 말은 HeroService
heroes를 synchronoulsy하게 가져올 수 있다는 뜻임. HeroesComponentgetHeroes()의 결과를
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.getObservable을 리턴해줌.

Observable HeroService

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함.

Subscribe in HeroesComponent

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의 배열을 HeroesComponentheroes proeprty에 할당줬는데
이 작업은 synchronously하게 일어나서 서버가 heores를 바로 리턴해주지 못하면 브라우저가
서버의 응답을 기다릴때까지 멈춰야했음. 이거는 HeroService가 remote 서버에서 request를 할때는
작동하지 않는 방식임.

따라서 새 버전을 만들었는데, 새 버전은 Observable이 heroes array를 emit할때까지 기다림.
emit은 바로 될수도있고 몇분 기다려야 할 수도있음. subscribe()메소드는 emit된 array를 받아서
HerroesComponentheroes property에 할당해줌.

이 비동기 접근법은 HeroService가 heroes를 서버에 request할때 동작함.

Show messages

이 섹션은 아래 작업을 할 것임.

  • 앱 아래부분에 메시지를 보여주는 MessageComponent 추가
  • 보여줄 메시지를 보낼 앱 전역에서 동작하는 injectable한 MessageService 생성
  • MessageServiceHeroService에 inject하기
  • HeroService가 heroes 가져오는데 성공하면 메시지 보여주기

Create MessageComponent

아래 CLI 명령어로 MessageComponent 생성하기

ng generate component messages

CLI는 src/app/messages 폴더를 만들고 src/app/app.module.ts의 AppModuleMessagesComponent를 추가해줌.

AppComponent 템플릿을 아래처럼 수정해 생성한 MessageComponent를 보여주게할거임.

<h1>{{title}}</h1>
<app-heroes></app-heroes>
<app-messages></app-messages>

Create the MessageService

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() { }
}

MessageServicemessages와 message를 추가해주는 add()랑 message를 초기화 해주는 clear()를 가지고 있음.

Inject it into the HereoService

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 시나리오임. MessageServiceHeroService에 inject했는데 HeroServiceHeroesComponent에 inject됨.

Send a message from HeroService

getHeroes() 메소드를 수정해서 heroes가 fetch됐을때 메시지를 보내도록함.

// src/app/hero.service.ts
getHeroes(): Observable<Hero[]> {
  const heroes = of(HEROES);
  this.messageService.add('HeroService: fetched heroes');
  return heroes;
}

Display the message from HeroService

MessageComponentHeroeService가 heroes를 fetch 했을때 보낸 메시지를 포함해서 모든 메시지를 보여줄 수 있어야함.
MessageCompnoent를 열어서 MessageService를 import 해줌. 그리고 생성자에 아래처럼 public messageService property를 parameter로 넣어줌.
이제 앵귤러가 MessageComponent를 생성할때 싱글톤인 MessageServicemessageService 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에서만 적용되서 써줘야함.
일반적인 함수에서 인자를 받았을때 인자를 함수에서만 쓸 수 있는거랑 똑같음.

Bind to MessageService

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()에 바인드함.

Add additional messages to hero service

아래는 유저가 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);
  }
}

Summary

  • 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 됐음.

출처

0개의 댓글