Tour of Heroes - Service (@injectable, observable)

소스코드

이 게시물에는 코드작성이 포함되어 있습니다. 소스코드를 받으신 후 진행해 주세요. MEAN Stack/개발 환경 구축에서 설명된 프로그램들(git, npm, atom editor)이 있어야 아래의 명령어들을 실행할 수 있습니다.

이 게시물의 소스코드는 Tour of Heroes / Tour of Heroes - 마스터-디테일 Components (@Input)에서 이어집니다.

tour-of-heroes.git 을 clone 한 적이 있는 경우: 터미널에서 해당 폴더로 이동 후 아래 명령어들을 붙여넣기합니다. 폴더 내 모든 코드가 이 게시물의 코드로 교체됩니다. 이를 원치 않으시면 이 방법을 선택하지 마세요.

git reset --hard
git pull
git reset --hard e1a7ce6
git reset --soft 2a17f91
npm install
atom .

tour-of-heroes.git 을 clone 한 적이 없는 경우: 터미널에서 코드를 다운 받을 폴더로 이동한 후 아래 명령어들을 붙여넣기하여 tour-of-heroes.git 을 clone 합니다.

git clone https://github.com/a-mean-blogger/tour-of-heroes.git
cd tour-of-heroes
git reset --hard e1a7ce6
git reset --soft 2a17f91
npm install
atom .

- Github에서 소스코드 보기: https://github.com/a-mean-blogger/tour-of-heroes/tree/e1a7ce688ff9b9c37bdcf681486663caf7982b81


이번 강의에서는 service와 observable의 개념을 익히고 service를 만들어 봅시다.

또한 message service와 message component를 웹사이트에 추가합니다. Message는 hero service가 사용될 때마다 어떤 작업이 있었는지를 웹사이트에 보여주는 log 기능입니다.

Service

Service는 여러 component에서 필요로 하는 기능을 분리한 class입니다. 즉 Angular 사이트에서 사용되는 특정한 기능들을 service로 생성한 후 해당 기능이 요구되는 component는 이 service를 불러와서 사용할 수 있습니다. 코드가 중복되는 것을 막고, 코드의 관리의 편의성을 위해, 프로그램의 효율성 향상를 위해 사용됩니다.

일반적으로 Angular 프로젝트에서 CRUD기능은 service를 이용해서 이루어 집니다. 우리는 hero들의 리스트를HEROES 상수에서 가져오고 있는데, 이 기능을 hero service로 작성해 봅시다. 이후 강의에 추가될 나머지 CRUD기능들도 이 hero service에 작성됩니다.

Observable

Observable은 자바스크립트의 라이브러리인 rxjs에 들어 있는 class로 아래의 형태로 사용됩니다.

Observable을_리턴하는_함수.subscribe(Observable을_처리하는_함수);
  • Observable을_리턴하는_함수에서 특정조건이 갖춰지면 Observable이 리턴되고 subscribe함수를 통해 Observable을_처리하는_함수로 전달되어 함수가 실행됩니다.

Promise를 더 발전시킨 개념으로 Promise와 유사한 점이 많고, 이벤트.subscribe(이벤트의_처리)로 단순화 시켜 이해할 수도 있습니다.

Angular의 많은 함수들이 Observable을 return하기 때문에 이러한 함수들은 subscribe함수를 통해서 값을 받아 사용할 수 있습니다. 물론 직접 만들어서 사용하기도 하죠.

폴더 구조

아래 명령어들로 이번강의에서 추가되는 파일들을 생성해 줍시다.

$ ng g c messages --skipTests
$ ng g s message --skipTests
$ ng g s hero --skipTests

$ ng g s$ ng generate service명령어의 축약형입니다. 예상하셨죠?

코드 - Services

// 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 = [];
  }
}

ng generate service 명령어로 service를 생성하면 @injectable decorator를 달고 있는 class가 생성됩니다. ()안에는 providedIn: 'root'가 자동으로 생성되는데, root는 뿌리/근본을 뜻하죠. 이 서비스는 이 프로젝트의 근본 module인 app.module과 연결이 되며, app.module의 declarations에 등록된 component에서 이 service를 사용할 수 있게 됩니다.

MessageService class에는 3개의 항목(property)가 있습니다.

  • messages: string[] 타입으로 생성된 메세지들을 담습니다.
  • add: string 타입 message를 인자로 받아 this.messages에 message를 추가하는 함수입니다.
  • clear: this.messages를 빈 배열로 바꿉니다.

Message 서비스는 메세지를 보관하고, 메세지를 추가하고, 메세지를 비우는 역할을 하는 service임을 알 수 있습니다.

// src/app/hero.service.ts

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';

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

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

  constructor(private messageService: MessageService) { } // 1

  getHeroes(): Observable<Hero[]> { // 2
    this.messageService.add('HeroService: fetched heroes');
    return of(HEROES);
  }
}

MessageServiceHeroService 안에서 사용되는데, 이처럼 service안에서 service를 사용하는 것이 가능합니다.

1. service를 사용하기 위해서는 현재 class의 constructor함수에 인자(parameter)로 작성되어야 합니다. 위 코드에서는 MessageServicemessageService인자로 constructor에 작성했고, 이제 this.messageServiceMessageService class의 항목들을 사용할 수 있습니다.

2. getHeroes함수는 호출되면 'HeroService: fetched heroes'(영웅들 받음)이라는 메세지를 추가하고, of함수에 HEROES를 담아서 리턴하는 일을 합니다. of함수는 전달받은 값으로 observable을 바로 생성하여 리턴하는 함수입니다. 처음에 observable을 설명할 때, 이벤트처럼 함수내 조건이 충족되면 observable를 리턴한다고 했었는데, of함수를 사용하면 observable를 바로 만들 수 있습니다.

** 현재의 코드에서는 observable를 사용할 이유가 전혀 없습니다. 하지만 DB를 통한 CRUD기능이 Observable을 통해 일어나기 때문에 이렇게 흉내를 내는 겁니다. 나중에 진정한 observable을 사용하는 것을 보여드릴텐데 지금 여기선 이런 게 있구나 정도로 알고 넘어가도록 합시다.

코드 - Messages Component

// src/app/messages/messages.component.ts

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

import { MessageService } from '../message.service'; // 1

@Component({
  selector: 'app-messages',
  templateUrl: './messages.component.html',
  styleUrls: ['./messages.component.css']
})
export class MessagesComponent implements OnInit {

  constructor(public messageService: MessageService) {} // 1

  ngOnInit() {
  }

}

1. HeroService 코드에서 설명했던 것과 마찬가지로 MessageService를 사용하기 위해 constructor함수의 인자로 작성하였습니다.

다만, HeroService에서는 messageServiceprivate 인자로 작성했었지만, 여기에서는 public으로 작성되었습니다.

Class내에서 private로 선언된 항목들은 class밖에서 사용할 수 없지만, public으로 선언된 항목들은 class밖에서도 사용할 수 있습니다. MessagesComponent에서는 view 코드에서 MessageService를 사용하기 위해 public로 선언하였습니다.

<!-- src/app/messages/messages.component.html -->

<div *ngIf="messageService.messages.length"> <!-- 1 -->

  <h2>Messages</h2>
  <button class="clear"
          (click)="messageService.clear()">clear</button> <!-- 2 -->
  <div *ngFor='let message of messageService.messages'> {{message}} </div> <!-- 3 -->

</div>

1. *ngIf를 사용하여 messageService의 messages 항목의 배열에 아이템이 있는 경우에만 div 태그의 코드를 render합니다.

2. clear 버튼에서 click 이벤트가 발생하면 messageService.clear()를 호출합니다.

3. *ngFor를 사용하여 messageService.messages를 div로 반복해서 보여줍니다.

이처럼 messageService를 view code에서 사용하기 때문에 component 코드에서는 messageServicepublic으로 선언하였습니다.

/* src/app/messages/messages.component.css */

h2 {
  color: red;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: lighter;
}
body {
  margin: 2em;
}
body, input[text], button {
  color: crimson;
  font-family: Cambria, Georgia;
}

button.clear {
  font-family: Arial;
  background-color: #eee;
  border: none;
  padding: 5px 10px;
  border-radius: 4px;
  cursor: pointer;
  cursor: hand;
}
button:hover {
  background-color: #cfd8dc;
}
button:disabled {
  background-color: #eee;
  color: #aaa;
  cursor: auto;
}
button.clear {
  color: #333;
  margin-bottom: 12px;
}

코드 - 그 외

// src/app/heroes/heroes.component.ts

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

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

@Component({
  selector: 'app-heroes',
  templateUrl: './heroes.component.html',
  styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
  heroes: Hero[]; // 2
  selectedHero: Hero;

  constructor(private heroService: HeroService) { } // 1

  ngOnInit() {
    this.getHeroes(); // 3
  }

  onSelect(hero: Hero): void {
    this.selectedHero = hero;
  }

  getHeroes(): void { // 3
    this.heroService.getHeroes()
      .subscribe(heroes => this.heroes = heroes);
  }
}

1. HeroesComponent에는 HeroServiceprivate로 작성하여 해당 class내에서만 사용하는 것을 알 수 있습니다.

2. 원래는 heroesHEROES 상수를 초기값으로 대입했었지만 이제는 초기값이 없습니다.

3. 대신에 ngOnInit함수안에서 this.getHeroes()를 호출합니다. ngOnInit에 작성된 코드는 class의 생성시에 실행된다고 했던것 기억나시나요?

4. getHeroes함수는 HeroServicegetHeroes를 호출하고, HeroServicegetHeroesof(Heroes)함수에 의해 subscribeHEROES를 전달해 줍니다.

subscribe함수는 화살표 함수가 인자로 전달되었으며, 이 화살표 함수는 HEROESheroes로 받아 this.heroes에 대입합니다.

ngOnInit함수에서 일련의 과정을 거쳐 mock-heroes.ts의 HEROESthis.heroes에 대입되게 되는데, 이 과정을 완전히 이해하도록 합시다. 이해가 안되면 댓글로 남겨주세요.

<!-- src/app/app.component.html -->

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

1. app component에 messages component를 배치하는 코드가 추가되었습니다.

// src/app/app.module.ts

...
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { MessagesComponent } from './messages/messages.component'; // 1

@NgModule({
  declarations: [
    AppComponent,
    HeroesComponent,
    HeroDetailComponent,
    MessagesComponent, // 1
  ],
  ...

1. 새로 생성된 message component가 추가되었습니다.

실행 결과

Messages 부분이 추가되었습니다. 어떠한 과정을 거쳐서 'HeroService: fetched heroes' 문구가 출력되는지를 잘 이해하도록합시다.

마치며..

이번 강의에서는

  • Service의 개념
  • Observable의 개념
  • ng generate service 명령어
  • @injectable decorator로 service class를 만드는 방법과 다른 class에서 service를 사용하는 법
  • of함수(rxjs)
  • subscribe함수의 예제

을 알아봤습니다. 반드시 모든 내용을 이해하신 후 계속해서 강의를 진행해 주세요. 만약 이해가 되지 않는다면 댓글로 질문을 해주세요.

댓글

댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP