Angular 튜터리얼 - HTTP

HTTP

이 페이지에서 다음과 같이 개선할 것입니다.

  • 서버에서 Hero 데이터를 가져옵니다.
  • 사용자가 Hero 이름을 추가, 편집 및 삭제할 수 있습니다.
  • 변경 사항을 서버에 저장합니다.

원격 서버의 웹 API에 해당 HTTP 호출을 하도록 앱을 수정합니다.

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

어디까지 했었나?

이전 페이지에서는 대시 보드와 고정된 Hero들 목록을 Navigate하면서 선택한 Hero를 편집하는 방법을 배웠습니다. 그것이 이 페이지의 출발점입니다.

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

터미널 창에서 다음 명령을 입력합니다.

1
npm start

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

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

HTTP Service 지원하기

HttpModule은 Angular core 모듈이 아닙니다. HttpModule은 웹 접근에 대한 Angular의 선택적인 접근 방식입니다. 이것은 @angular/http라고 하는 별도의 애드온 모듈로 존재하며 Angular npm 패키지의 일부로 별도의 스크립트 파일로 제공됩니다.

SystemJS가 필요할 때 라이브러리를 로드하기 위해 systemjs.config에서 설정 했으므로 @angular/httpimport할 준비가되었습니다.

HTTP Service 등록

응용 프로그램은 Angular http서비스에 따라 달라지며, 그 자체는 다른 Service의 지원에 의존합니다. @angular/http 라이브러리의 HttpModule은 완전한 HTTP 서비스 집합을 제공합니다.

앱의 어느 곳에서나 http 서비스에 접근할 수 있게하려면 AppModuleimport 목록에 HttpModule을 추가합니다.

src/app/app.module.ts (v1)

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
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroService } from './hero.service';
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
AppRoutingModule
],
declarations: [
AppComponent,
DashboardComponent,
HeroDetailComponent,
HeroesComponent,
],
providers: [ HeroService ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

루트 NgModule AppModule에서 imports 배열의 일부로 HttpModule을 제공합니다.

Web API 시뮬래이트

우리는 루트AppModuleproviders에 app-wide 서비스를 등록할 것을 권장합니다.

Heroes 데이터에 대한 요청을 처리할 수있는 웹 서버가 있을 때까지 HTTP 클라이언트는 Mock Servicein-memory 웹 API에서 데이터를 가져오고 저장합니다.

src/app/app.module.ts을 Mock 서비스 사용하는 이 버전으로 업데이트합니다.

src/app/app.module.ts (v2)

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
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppRoutingModule } from './app-routing.module';
// Imports for loading & configuring the in-memory web api
import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroService } from './hero.service';
@NgModule({
imports: [
BrowserModule,
FormsModule,
HttpModule,
InMemoryWebApiModule.forRoot(InMemoryDataService),
AppRoutingModule
],
declarations: [
AppComponent,
DashboardComponent,
HeroDetailComponent,
HeroesComponent,
],
providers: [ HeroService ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

실제 API 서버를 요구하는 대신,이 예제는 InMemoryWebApiModuleimport에 추가함으로써 원격 서버와의 통신을 시뮬레이트하고, Http 클라이언트의 XHR 백엔드 서비스를 메모리 내에서 효과적으로 대체합니다.

1
InMemoryWebApiModule.forRoot(InMemoryDataService),

forRoot() 설정 메서드는 in-memory 데이터베이스를 준비하는 InMemoryDataService 클래스를 사용합니다. 다음과 같은 내용으로 app에 in-memory-data.service.ts 파일을 추가합니다.

src/app/in-memory-data.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 0, name: 'Zero' },
{ 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' }
];
return {heroes};
}
}

이 파일은 mock-heroes.ts를 대체합니다. 이제는 mock-heroes.ts 파일을 삭제해도 안전합니다. Hero “Zero”를 추가하여 데이터 서비스가 id == 0인 Hero를 처리할 수 있음을 확인했습니다.

in-memory 웹 API는 개발 초기와 이 Tour of Heroes와 같이 시연에서 유용합니다. 이 백엔드 대체에 대한 세부 사항은 걱정하지 마십시오. 실제 웹 API 서버를 가지고있을 때 건너 뛸 수 있습니다.
in-memory 웹 API에 대한 자세한 내용은 HTTP Client 페이지의 Appendix: Tour of Heroes in-memory web api 섹션을 참조하십시오.

Hero들과 HTTP

현재 HeroService 구현에서는 Mock Heroes가 Resolved Promise를 반환합니다.

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

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

이것은 궁극적으로 비동기 작업이어야하는 HTTP 클라이언트로 Heroes를 가져 오는 것을 예상하여 구현되었습니다.

이제 HTTP를 사용하도록 getHeroes()를 변환합니다.

src/app/hero.service.ts (updated getHeroes and new class members)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private heroesUrl = 'api/heroes'; // URL to web api
constructor(private http: Http) { }
getHeroes(): Promise<Hero[]> {
return this.http.get(this.heroesUrl)
.toPromise()
.then(response => response.json().data as Hero[])
.catch(this.handleError);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error); // for demo purposes only
return Promise.reject(error.message || error);
}

다음과 같이 import 문을 업데이트하십시오.

src/app/hero.service.ts (updated imports)

1
2
3
4
5
6
import { Injectable } from '@angular/core';
import { Headers, Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';
import { Hero } from './hero';

브라우저를 새로 고칩니다. Hero 데이터가 Mock 서버에서 성공적으로 로드 되어야합니다.

HTTP Promise

Angular http.get은 RxJS Observable을 반환합니다. Observable은 비동기 데이터 흐름을 관리하는 강력한 방법입니다. 이 페이지의 뒷부분에있는 Observable에 대해서 읽어 보십시오

지금은 toPromise 연산자를 사용하여 Observable을 Promise로 변환합니다.

1
.toPromise()

Angular Observable에는 toPromise 연산자가 기본적으로 제공되지 않습니다.

Observable을 확장하는 toPromise와 같은 유용한 연산자가 많이 있습니다. 이러한 기능을 사용하려면 연산자를 추가해야합니다. 이것은 RxJS 라이브러리에서 다음과 같이 import할 수 있습니다.

1
import 'rxjs/add/operator/toPromise';

이 튜토리얼의 뒷부분에서 더 많은 연산자를 추가하고 그렇게해야하는 이유를 배웁니다.

then callback 안에서 데이터 추출

Promise then() 콜백에서 HTTP 응답의 json 메소드를 호출하여 응답내의 데이터를 추출합니다.

1
.then(response => response.json().data as Hero[])

응답 JSON에는 호출자가 원하는 Heroes 배열을 보유하는 단일 data 속성이 있습니다. 그래서 그 배열을 해결된 Promise 값으로 반환합니다.

서버가 반환하는 데이터의 모양에 유의하십시오. 이 특정 in-memory 웹 API 예제는 데이터 프로퍼티가 있는 객체를 반환합니다. API가 다른 것을 반환할 수 있습니다. 웹 API와 일치하도록 코드를 조정하십시오.

호출자는 (Mock) 서버에서 Hero들을 가져온 것을 알지 못합니다. 그것은 이전처럼 Hero의 Promise를 받습니다.

에러 핸들링

getHeroes()의 끝에서 서버 실패를 catch하고 error handler에 전달합니다.

1
.catch(this.handleError);

이것은 중요한 단계입니다. HTTP 오류는 사용자가 제어할 수없는 이유로 자주 발생하므로 대비 해야합니다.

1
2
3
4
private handleError(error: any): Promise<any> {
console.error('An error occurred', error); // for demo purposes only
return Promise.reject(error.message || error);
}

이 데모 서비스는 오류를 console에 기록합니다. 실서비스에서는 코드의 오류를 처리할 것입니다. 데모에서는 이 방법이 유용합니다.

이 코드에는 Rejected된 Promise의 오류가 포함되어 있어 호출자가 사용자에게 적절한 오류 메시지를 표시할 수 있습니다.

id로 Hero 얻어오기

HeroDetailComponent가 Hero를 가져 오도록 HeroService에게 요청하면, HeroService는 모든 Hero등을 가져와서 id가 일치하는 하나의 Hero를 필터링 합니다. 시뮬레이션에서는 문제가 없지만 실제 서버에 모든 Hero들을 물어 보는 것은 낭비입니다. 대부분의 웹 API는 api/hero/:id (api/hero/11와 같은) 형태로 get-by-id 요청을 지원합니다.

HeroService.getHero()메서드를 업데이트하여 get-by-id 요청을 만든다.

src/app/hero.service.ts

1
2
3
4
5
6
7
getHero(id: number): Promise<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.get(url)
.toPromise()
.then(response => response.json().data as Hero)
.catch(this.handleError);
}

이 요청은 getHeroes()와 거의 같습니다. URL의 Hero id는 서버가 업데이트 해야할 Hero를 식별합니다.

또한 응답의 data는 배열이 아닌 단일 Hero 객체입니다.

getHeroes API 변경 없음

getHeroes()getHero()에 내부적으로 중요한 변경을 가했지만 public 시그니처는 변경되지 않았습니다. 두 방법 모두에서 Promise를 반환합니다. 이를 호출하는 Component를 업데이트할 필요가 없습니다.

이제 Hero을 만들고 지울 수있는 기능을 추가할 차례입니다.

Hero 상세 정보 업데이트 하기

Hero 상세보기에서 Hero의 이름을 편집 해보십시오. 입력할 때 Hero 이름이 뷰 제목에 업데이트됩니다. 그러나 뒤로 단추를 클릭하면 변경 내용이 손실됩니다.

이전에는 업데이트가 손실되지 않았습니다. 무엇이 바뀌 었을까요? 앱이 Mock Heroes 목록을 사용하면 업데이트가 단일 앱 전체 공유 목록의 Hero 개체에 직접 적용됩니다. 이제 서버에서 데이터를 가져 오는 중이므로 변경 사항을 유지하려면 서버에 다시 써야합니다.

Hero 상세 정보 저장하는 기능 추가

Hero 상세 템플릿의 끝에서 save()라는 새로운 Component 메서드를 호출하는 click 이벤트 바인딩과 함께 save 버튼을 추가합니다.

src/app/hero-detail.component.html (save)

1
<button (click)="save()">Save</button>

Hero Service update () 메서드를 사용하여 Hero 이름의 변경을 지속시키는 save() 메서드를 추가한 다음, 이전 뷰로 Navigate합니다.

src/app/hero-detail.component.ts (save)

1
2
3
4
save(): void {
this.heroService.update(this.hero)
.then(() => this.goBack());
}

Hero Service에 update() 메서드 추가

update() 메서드의 전체적인 구조는 getHeroes()와 비슷하지만 HTTP put()를 사용하여 서버에 변경을 지속합니다.

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

1
2
3
4
5
6
7
8
9
10
private headers = new Headers({'Content-Type': 'application/json'});
update(hero: Hero): Promise<Hero> {
const url = `${this.heroesUrl}/${hero.id}`;
return this.http
.put(url, JSON.stringify(hero), {headers: this.headers})
.toPromise()
.then(() => hero)
.catch(this.handleError);
}

서버가 업데이트 해야하는 Hero를 식별하기 위해 Hero id가 URL에 인코딩됩니다. put() 본문은 JSON.stringify를 호출하여 얻은 Hero의 JSON 문자열 인코딩입니다. body의 content type(application/json)은 요청 헤더에서 식별됩니다.

브라우저를 새로 고치고 Hero 이름을 변경하고 변경 사항을 저장한 다음 브라우저 뒤로 버튼을 클릭하십시오. 변경 사항이 지속되어야합니다.

Hero들 추가하는 기능 추가

Hero를 추가하려면 Hero의 이름이 필요합니다. 추가 버튼과 쌍을 이루는 input 엘리먼트를 사용할 수 있습니다.

Hero Component HTML에 제목 다음줄에 아래 내용을 추가합니다.

src/app/heroes.component.html (add)

1
2
3
4
5
6
<div>
<label>Hero name:</label> <input #heroName />
<button (click)="add(heroName.value); heroName.value=''">
Add
</button>
</div>

클릭 이벤트에 응답하여 Component의 클릭 핸들러를 호출한 다음 입력 필드를 지우고 다른 이름을 사용할 준비를 합니다.

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

1
2
3
4
5
6
7
8
9
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.create(name)
.then(hero => {
this.heroes.push(hero);
this.selectedHero = null;
});
}

주어진 이름이 비어 있지 않으면, 핸들러는 Hero Service에 지명된 Hero의 생성을 위임한 다음, 새로운 Hero를 배열에 추가합니다.

HeroService 클래스에 create() 메서드를 구현합니다.

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

1
2
3
4
5
6
7
create(name: string): Promise<Hero> {
return this.http
.post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers})
.toPromise()
.then(res => res.json().data as Hero)
.catch(this.handleError);
}

브라우저를 새로 고침하고 Hero를 만듭니다.

Hero 삭제기능 추가

Hero 뷰의 각 Hero는 삭제 버튼이 있어야합니다.

Hero Component HTML의 반복적인 <li> 엘리먼트 다음에 아래의 버튼 엘리먼트를 추가합니다.

1
2
<button class="delete"
(click)="delete(hero); $event.stopPropagation()">x</button>

<li> 엘리먼트는 이제 다음과 같아야합니다.

src/app/heroes.component.html (li-element)

1
2
3
4
5
6
7
<li *ngFor="let hero of heroes" (click)="onSelect(hero)"
[class.selected]="hero === selectedHero">
<span class="badge">{{hero.id}}</span>
<span>{{hero.name}}</span>
<button class="delete"
(click)="delete(hero); $event.stopPropagation()">x</button>
</li>

Componentdelete() 메서드를 호출하는 것 외에도, 삭제 버튼의 클릭 핸들러 코드는 클릭 이벤트의 전파를 중지합니다. <li> 클릭 핸들러는 사용자가 삭제할 영웅을 선택하기 때문에 트리거되기를 원하지 않습니다.

delete() 핸들러의 로직은 조금 복잡합니다.

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

1
2
3
4
5
6
7
8
delete(hero: Hero): void {
this.heroService
.delete(hero.id)
.then(() => {
this.heroes = this.heroes.filter(h => h !== hero);
if (this.selectedHero === hero) { this.selectedHero = null; }
});
}

물론 Hero Service에 Hero 삭제를 위임하지만 Component는 여전히 디스플레이 업데이트에 대한 책임이 있습니다. 삭제된 Hero를 배열에서 제거하고 필요한 경우 선택된 Hero를 재설정합니다.

Hero 항목의 맨 오른쪽에 삭제 버튼을 배치하기 위해 다음 CSS를 추가합니다.

src/app/heroes.component.css (additions)

1
2
3
4
5
6
7
button.delete {
float:right;
margin-top: 2px;
margin-right: .8em;
background-color: gray !important;
color:white;
}

Hero Service에 delete() 메서드

Hero Service delete() 메서드를 추가합니다. 이 메서드는 HTTP delete () 메서드를 사용하여 서버에서 Hero를 제거합니다.

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

1
2
3
4
5
6
7
delete(id: number): Promise<void> {
const url = `${this.heroesUrl}/${id}`;
return this.http.delete(url, {headers: this.headers})
.toPromise()
.then(() => null)
.catch(this.handleError);
}

브라우저를 새로 고치고 새로운 삭제 기능을 사용해보십시오.

Observables

각각의 Http Service 메소드는 HTTP Response 객체의 Observable을 반환합니다.

HeroServiceObservablePromise로 변환하고 호출자에게 Promise를 리턴합니다. 이 절에서는 Observable을 언제, 어떻게, 왜 직접 리턴해야 하는지를 설명합니다.

Background

Observable은 배열과 비슷한 연산자로 처리할 수있는 이벤트 스트림입니다.

Angular core는 Observable를 기본적으로 지원합니다. 개발자는 RxJS library의 연산자 및 확장 프로그램을 사용하여 지원을 확대할 수 있습니다. 곧 보게 될 것입니다.

HeroServicetoPromise 연산자를 http.get()Observable 결과에 연결 시켰습니다. 그 연산자는 ObservablePromise로 변환했고 그 Promise를 호출자에게 다시 전달했습니다.

Promise으로 전환하는 것은 종종 좋은 선택입니다. 일반적으로 http.get()을 호출하여 단일 데이터 덩어리를 가져옵니다. 데이터를 받으면 작업이 완료됩니다. 호출 ComponentPromise의 형태로 단일 결과를 쉽게 사용할 수 있습니다.

그러나 요청이 항상 한 번만 수행되는 것은 아닙니다. 서버가 첫 번째 요청에 응답하기 전에 하나의 요청을 시작하여 취소하고 다른 요청할 수 있습니다.

request-cancel-new-request 순서는 Promise로 구현하기가 어렵지만 Observable에서는 쉽게 구현할 수 있습니다.

이름으로 검색하는 기능 추가

Tour of Hero들에 영웅 검색 기능을 추가할 예정입니다. 사용자가 검색 상자에 이름을 입력하면 해당 이름으로 필터링된 Hero에 대한 반복적인 HTTP 요청을합니다.

우선 서버의 웹 API에 검색 쿼리를 보내는 HeroSearchService를 생성합니다.

src/app/hero-search.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import { Hero } from './hero';
@Injectable()
export class HeroSearchService {
constructor(private http: Http) {}
search(term: string): Observable<Hero[]> {
return this.http
.get(`api/heroes/?name=${term}`)
.map(response => response.json().data as Hero[]);
}
}

HeroSearchService에 있는 http.get() 호출은 URL에 질의 문자열이 있는데 HeroService에 있는 것과 유사합니다.

더욱 중요한 것은 toPromise ()를 호출하지 않는다는 것입니다. 대신 http.get()에서 Observable을 다른 RxJS 연산자인 map ()에 연결 한 후 응답 데이터에서 Hero를 추출합니다. RxJS 연산자 체인은 응답 처리를 쉽고 읽기 쉽게 만듭니다. discussion below about operators를 참조하십시오.

HeroSearchComponent

새로운 HeroSearchService를 호출하는 HeroSearchComponent를 만듭니다.

Component 템플릿은 단순한 텍스트 상자 및 일치하는 검색 결과 목록입니다.

src/app/hero-search.component.html

1
2
3
4
5
6
7
8
9
10
<div id="search-component">
<h4>Hero Search</h4>
<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
<div>
<div *ngFor="let hero of heroes | async"
(click)="gotoDetail(hero)" class="search-result" >
{{hero.name}}
</div>
</div>
</div>

또한 새로운 Component에 Style을 추가하십시오.

src/app/hero-search.component.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.search-result{
border-bottom: 1px solid gray;
border-left: 1px solid gray;
border-right: 1px solid gray;
width:195px;
height: 16px;
padding: 5px;
background-color: white;
cursor: pointer;
}
.search-result:hover {
color: #eee;
background-color: #607D8B;
}
#search-box{
width: 200px;
height: 20px;
}

사용자가 검색 상자에 입력하면 keyup 이벤트 바인딩은 Componentsearch() 메서드를 새 검색 상자 값으로 호출합니다.

예상대로 *ngForComponentheroes 프로퍼티에서 Hero들을 반복합니다.

그러나 곧 보게 되겠지만 heroes 프로퍼티는 이제 Hero 배열이 아니라 Hero 배열의 Observable 프로퍼티입니다. *ngForasync Pipe(AsyncPipe)를 통해 라우트할 때까지 Observable로 아무 것도 할 수 없습니다. async Pipe Observable을 Subscribe하고 Heroes들의 배열을 *ngFor로 Produces 합니다.

HeroSearchComponent 클래스 및 메타 데이터를 만듭니다.

src/app/hero-search.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
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
// Observable class extensions
import 'rxjs/add/observable/of';
// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { HeroSearchService } from './hero-search.service';
import { Hero } from './hero';
@Component({
selector: 'hero-search',
templateUrl: './hero-search.component.html',
styleUrls: [ './hero-search.component.css' ],
providers: [HeroSearchService]
})
export class HeroSearchComponent implements OnInit {
heroes: Observable<Hero[]>;
private searchTerms = new Subject<string>();
constructor(
private heroSearchService: HeroSearchService,
private router: Router) {}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
this.heroes = this.searchTerms
.debounceTime(300) // wait 300ms after each keystroke before considering the term
.distinctUntilChanged() // ignore if next search term is same as previous
.switchMap(term => term // switch to new observable each time the term changes
// return the http search observable
? this.heroSearchService.search(term)
// or the observable of empty heroes if there was no search term
: Observable.of<Hero[]>([]))
.catch(error => {
// TODO: add real error handling
console.log(error);
return Observable.of<Hero[]>([]);
});
}
gotoDetail(hero: Hero): void {
let link = ['/detail', hero.id];
this.router.navigate(link);
}
}

검색어

searchTerms에 중점을 둡니다.

1
2
3
4
5
6
private searchTerms = new Subject<string>();
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}

Subject는 Observable 이벤트 스트림의 생성자입니다. searchTerms는 이름 검색을 위한 필터 기준인 문자열 Observable을 생성합니다.

search()를 호출할 때마다 next()를 호출하여 이 subject의 Observable 스트림에 새로운 문자열을 넣습니다.

heroes property (ngOnInit) 초기화하기

Subject는 또한 Observable입니다. 검색 용어의 스트림을 Hero 배열의 스트림으로 변환하고 그 결과를 heroes 프로퍼티에 지정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
heroes: Observable<Hero[]>;
ngOnInit(): void {
this.heroes = this.searchTerms
.debounceTime(300) // wait 300ms after each keystroke before considering the term
.distinctUntilChanged() // ignore if next search term is same as previous
.switchMap(term => term // switch to new observable each time the term changes
// return the http search observable
? this.heroSearchService.search(term)
// or the observable of empty heroes if there was no search term
: Observable.of<Hero[]>([]))
.catch(error => {
// TODO: add real error handling
console.log(error);
return Observable.of<Hero[]>([]);
});
}

HeroSearchService에 직접 모든 사용자 키 스트로크를 전달하면 과도한 양의 HTTP 요청이 생성되어 서버 리소스를 부담시키거나 셀룰러 네트워크 데이터가 많아질 것입니다.

대신 Observable 연산자를 연결하여 Observable 문자열에 대한 요청 흐름을 줄일 수 있습니다. HeroSearchService에 대한 호출 횟수를 줄이고 적절한 시간에 결과를 얻습니다. 방법은 다음과 같습니다.

  • debounceTime(300)은 새로운 문자열 이벤트의 흐름이 300 밀리초 동안 일시 중지 될 때까지 기다린 후 최신 문자열을 전달합니다. 300ms보다 자주 요청을하지 않습니다.
  • distinctUntilChanged는 필터 텍스트가 변경된 경우에만 요청을 보냅니다.
  • switchMap()debouncedistinctUntilChanged를 통해 검색하는 각 검색어에 대해 검색 서비스를 호출합니다. 이전 검색 관측 값을 취소하고 파기하며 Observable 최신 검색 서비스만 반환합니다.

switchMap 연산자 (이전에는 flatMapLatest라고 함)를 사용하여 모든 규정 키 이벤트가 http() 메서드 호출을 트리거할 수 있습니다. 요청간에 300ms의 일시 중지가 있더라도 여러 개의 HTTP 요청을 보낼 수 있으며 전송된 순서대로 반환하지 않을 수 있습니다.

switchMap()은 가장 최근의 http 메서드 호출에서 Observable만 리턴하면서 원래 요청 순서를 보존합니다. 이전 호출의 결과는 취소되고 폐기됩니다.

검색 텍스트가 비어 있으면 http() 메서드 호출도 일단락되고 빈 배열을 포함하는 Observable이 반환됩니다.

서비스가 해당 기능을 지원할 때까지 HeroSearchService Observable을 취소한다고 해서 보류중인 HTTP 요청이 실제로 중단되지는 않습니다. 지금은 원치 않는 결과는 무시됩니다.

  • catch는 실패한 Observable 항목을 차단합니다. 간단한 예제는 콘솔에 오류를 인쇄합니다. 실제 응용 프로그램이 더 잘할 것입니다. 그런 다음 검색 결과를 지우려면 빈 배열을 포함하는 Observable 객체를 반환합니다.

RxJS 연산자 Import

대부분의 RxJS 연산자는 Angular 기반 Observable 구현에 포함되지 않습니다. 기본 구현에는 Angular 자체에서 필요한 것만 포함됩니다.

더 많은 RxJS 기능이 필요하면 정의된 라이브러리를 가져 와서 Observable을 확장하십시오. 이 Component에 필요한 모든 RxJS import는 다음과 같습니다.

src/app/hero-search.component.ts (rxjs imports)

1
2
3
4
5
6
7
8
9
10
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
// Observable class extensions
import 'rxjs/add/observable/of';
// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';

가져 오기 rxjs/add/...구문이 익숙하지 않을 수 있습니다. 중괄호 사이에 일반적인 심볼 목록이 없습니다. {...}.

연산자 심볼 자체는 필요하지 않습니다. 각각의 경우에, 라이브러리를 임포트하는 단순한 행위는 라이브러리의 스크립트 파일을 로드하고 실행 시켜서, 연산자를 Observable 클래스에 추가합니다.

Search Component를 Dashboard에 추가하기

Hero 검색 HTML 엘리먼트를 DashboardComponent 템플리트의 맨 아래에 추가합니다.

src/app/dashboard.component.html

1
2
3
4
5
6
7
8
9
<h3>Top Heroes</h3>
<div class="grid grid-pad">
<a *ngFor="let hero of heroes" [routerLink]="['/detail', hero.id]" class="col-1-4">
<div class="module hero">
<h4>{{hero.name}}</h4>
</div>
</a>
</div>
<hero-search></hero-search>

마지막으로 hero-search.component.ts에서 HeroSearchComponentimport하고 declarations 배열에 추가합니다.

src/app/app.module.ts (search)

1
2
3
4
5
6
7
declarations: [
AppComponent,
DashboardComponent,
HeroDetailComponent,
HeroesComponent,
HeroSearchComponent
],

앱을 다시 실행합니다. 대시 보드에서 검색 상자에 텍스트를 입력합니다. 기존의 Hero 이름과 일치하는 문자를 입력하면 다음과 같은 메시지가 표시됩니다.

앱 구조와 코드

이 예제 페이지의 라이브 예제/예제 다운로드에서 샘플 소스 코드를 확인하십시오. 그리고 다음과 같은 구조인지 확인하십시오.

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
angular-tour-of-heroes
┣ src
┃ ┣ app
┃ ┃ ┣ app.component.ts
┃ ┃ ┣ app.component.css
┃ ┃ ┣ app.module.ts
┃ ┃ ┣ app-routing.module.ts
┃ ┃ ┣ dashboard.component.css
┃ ┃ ┣ dashboard.component.html
┃ ┃ ┣ dashboard.component.ts
┃ ┃ ┣ hero.ts
┃ ┃ ┣ hero-detail.component.css
┃ ┃ ┣ hero-detail.component.html
┃ ┃ ┣ hero-detail.component.ts
┃ ┃ ┣ hero-search.component.html (new)
┃ ┃ ┣ hero-search.component.css (new)
┃ ┃ ┣ hero-search.component.ts (new)
┃ ┃ ┣ hero-search.service.ts (new)
┃ ┃ ┣ hero.service.ts
┃ ┃ ┣ heroes.component.css
┃ ┃ ┣ heroes.component.html
┃ ┃ ┣ heroes.component.ts
┃ ┃ ┗ in-memory-data.service.ts (new)
┃ ┣ main.ts
┃ ┣ index.html
┃ ┣ styles.css
┃ ┣ systemjs.config.js
┃ ┗ tsconfig.json
┣ node_modules ...
┗ package.json

Home Stretch

당신의 여행이 끝나고 많은 것을 성취했습니다.

  • 앱에서 HTTP를 사용하기 위해 필요한 의존성을 추가했습니다.
  • HeroService를 리팩토링하여 웹 API에서 Hero를 로드했습니다.
  • post(), put()delete() 메소드를 지원하도록 HeroService를 확장했습니다.
  • Hero를 추가, 편집 및 삭제할 수 있도록 Component를 업데이트했습니다.
  • in-memory 웹 API를 구성했습니다.
  • Observable 사용법을 배웠습니다.

이 페이지에서 추가하거나 변경 한 파일은 다음과 같습니다.

https://angular.io/tutorial/toh-pt6#home-stretch 에서 확인하세요

Next step

이것으로 “Tour of Heroes”튜토리얼을 마칩니다. 이제 Angular 개발에 대해 자세히 배울 준비가되었습니다. Architecture 가이드부터 시작하세요.


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

참고

공유하기