HTTP

소스코드

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

이 게시물의 소스코드는 Tour of Heroes / Routing(router-outlet, routerLink, ActivatedRoute, Observable)에서 이어집니다.

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

git reset --hard
git pull
git reset --hard 58820f3
git reset --soft 548f57d
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 58820f3
git reset --soft 548f57d
npm install
atom .

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


Angular사이트의 공식 tutorial인 Tour of Heroes의 마지막 강의, HTTP입니다. Tutorial은 여기서 끝이지만 Angular 공식 문서 사이트(https://angular.io/doc)에 더 깊은 내용들이 있으니 참고해 주세요.

공식 tutorial link : https://angular.io/docs/ts/latest/tutorial/toh-pt6.html
온라인 예제 : https://angular.io/generated/live-examples/toh-pt6/eplnkr.html


이번 강의에서는 API를 만들고 이 API를 사용해서 hero의 CRUD 기능을 모두 완성합니다. API를 만들기 위해서는 Angular In Memory Web API package를 사용합니다.

Angular In Memory Web API는 말그대로 In Memory API를 만드는 package인데, 이 API는 database를 memory에 가상으로 만들고 동일한 angular app에서만 접근이 가능한 일종의 API 시뮬레이터입니다. 그러므로 이 package에 대해 깊게 공부하지 않으셔도 됩니다.

나중에 실제 API로 전환시에 service에서 API 주소만 바꿔주면 동일하게 작동하기 때문에 이번강의가 중요하다고 할 수 있습니다.

이번 tutorial에는 실시간 hero 검색 기능도 추가됩니다. 사실 이 부분은 기초 tutorial이라기 보다는 advance강의라고 생각이 되는데.. 구글에서 한글자 한글자 입력할때마다 검색결과가 화면에 바뀌는 것처럼 angular에서 이 기능을 어떻게 효율적으로 만드는 지에 관해서 입니다. 키입력이 있을 때 마다 자료를 확인하는 것이 아니라, 키입력 직후 자료 검색 시작 -> 만약 다음 키입력이 300 밀리초 이내에 이루어 졌을 경우, 이전 자료 검색을 취소하고 새로 자료 검색 시작 하는 방법에 대해서 설명하고 있습니다. 개인적으로 이 부분은 Angular 기초 강의에서 중요하다고 생각하지 않기 때문에 따로 code를 설명하지 않겠습니다.

폴더구조

hero-search component들과 hero-search service 그리고 in-memory-data service가 추가되었습니다. mock-heroes는 없어졌죠.

Package 설치

아래 명령어를 입력하여 Angular In Memory Web API package를 설치합니다.

npm install angular-in-memory-web-api --save

코드

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

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};
  }
}

Angular In Memory Web API package에서 InMemoryDbService를 import한 후 이를 implement한 InMemoryDataService 클라스를 만듭니다. 이 클라스를 app.module.ts에 특정한 방법으로 넣어주면 Angular app 내에서 사용할 수 있는 API를 생성합니다.

7 Standard Actions 중 form 생성과 관련이 없는 5가지 action들(index, show, create, update, destroy)이 생성되고 data를 검색할 수 있는 API도 생성됩니다. API들을 표로 나타내면 다음과 같습니다.

기능http verbsroute
indexgetapi/heroes
showgetapi/heroes/:id
createpostapi/heroes
updateputapi/heroes/:id
destroydeleteapi/heroes/:id
검색getapi/heroes/?name=

heroes라는 이름으로 data를 생성하였기 때문에 route에 heroes가 들어갑니다.

// src/app/hero.service.ts

import { Injectable }    from '@angular/core';
import { Headers, Http } from '@angular/http'; //1-1

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

import { Hero } from './hero';

@Injectable()
export class HeroService {

  private headers = new Headers({'Content-Type': 'application/json'}); //2
  private heroesUrl = 'api/heroes';  // URL to web api //3

  constructor(private http: Http) { }

  getHeroes(): Promise<Hero[]> { //4
    return this.http.get(this.heroesUrl)
               .toPromise()
               .then(response => response.json().data as Hero[])
               .catch(this.handleError);
  }

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

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

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

  update(hero: Hero): Promise<Hero> { //8
    const url = `${this.heroesUrl}/${hero.id}`;
    return this.http
      .put(url, JSON.stringify(hero), {headers: this.headers})
      .toPromise()
      .then(() => hero)
      .catch(this.handleError);
  }

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

1-1. REST API에서 data를 읽어오기 위해서는 http package가 필요합니다. header는 http header를 작성하기 위해 불러옵니다.

1-2. Observable을 promise로 바꾸어 주는 함수(.toPromise)를 제공하는 package입니다.

2. api에 data를 전달하는 경우, json형식으로 data를 전달하기 때문에 header에 content type을 json으로 설정합니다.

3. hero service의 기본이 되는 API주소를 설정합니다.(api/heroes)

4. getHeroes는 hero의 목록(index)을 가져오는 함수입니다. http를 사용해서 api/heroes 에 get 으로 요청합니다. http는 Observable을 리턴하기 때문에 .toPromise를 사용하여 promise로 바꾸어 줍니다. response.json().data as Hero[]는 response를 json 형식으로 변환하고, response.data 위치의 값을 Hero 배열 타입 바꾸게 됩니다.

5. getHero는 id를 입력받아 해당 id의 hero를 가져오는 함수입니다. api/heroes/id 에 get 으로 요청합니다. 4번과 거의 동일하지만 Hero를 배열로 전달하는 것이 아니라 Hero 하나를 전달하는 것이 차이점입니다.

6. delete은  id를 입력받아 해당 id의 hero를 삭제하는 함수입니다.  api/heroes/id 에 delete 으로 요청하면 API가 해당 hero를 삭제하게 됩니다. 삭제한 후에는 null을 전달합니다.

7.create은 문자열을 입력받아 새로운 hero를 생성하는 함수입니다. api/heroes 에 post 로 요청하면서 json을 전달하면 API가 새로운 hero를 생성하게 됩니다. 생성한 후에는 생성된 hero를 전달합니다. 이번에는 api에 json형식의 data를 전달하기 때문에 2번에서 만든 header를 함께 전달합니다.

8. update는 hero와 id를 입력받아 해당 id의 hero를 갱신하는 함수입니다. api/heroes/id 에 put 으로 요청하면서 json을 전달하면 API가 해당 id의 hero를 갱신하게 됩니다. 갱신한 후에는 갱신한 hero를 전달합니다. 이번에도 api에 json형식의 data를 전달하기 때문에 2번에서 만든 header를 함께 전달합니다.

9. 4-8번에서 catch에서 사용되는 함수입니다.

// src/app/heroes.component.ts

//...

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

  constructor(
    private router: Router,
    private heroService: HeroService) { }

  getHeroes(): void { //*
    this.heroService
        .getHeroes()
        .then(heroes => this.heroes = heroes);
  }

  add(name: string): void { //*
    name = name.trim();
    if (!name) { return; }
    this.heroService.create(name)
      .then(hero => {
        this.heroes.push(hero);
        this.selectedHero = null;
      });
  }

  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에서 만든 getHeroes, create, delete를 사용하는 함수가 HeroesComponent 클라스에 생성되었습니다.

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

<h2>My Heroes</h2>
<div>
  <label>Hero name:</label> <input #heroName />
  <button (click)="add(heroName.value); heroName.value=''"> //*
    Add
  </button>
</div>
<ul class="heroes">
  <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>
</ul>
<div *ngIf="selectedHero">
  <!-- ... -->
</div>

* hero를 생성하고 hero를 삭제하는 함수가 template에서 어떻게 사용되는지를 이해하시고 다음으로 진행하도록 합니다.

// src/app/hero-detail.component.ts

// ...

  ngOnInit(): void {
    this.route.params
      .switchMap((params: Params) => this.heroService.getHero(+params['id']))
      .subscribe(hero => this.hero = hero);
  }

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

  goBack(): void {
    this.location.back();
  }
}

1. hero service에서 만든 update를 사용하는 함수입니다. update한 후에는 goBack함수를 호출합니다.

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

<div *ngIf="hero">
  <h2>{{hero.name}} details!</h2>
  <div>
    <label>id: </label>{{hero.id}}</div>
  <div>
    <label>name: </label>
    <input [(ngModel)]="hero.name" placeholder="name" />
  </div>
  <button (click)="goBack()">Back</button>
  <button (click)="save()">Save</button> //1
</div>

1.  바로 위에서 만든 save 함수를 호출하는 부분입니다.

// src/app/hero-search.service.ts

import { Injectable } from '@angular/core';
import { Http }       from '@angular/http';

import { Observable }     from 'rxjs/Observable'; //1
import 'rxjs/add/operator/map'; //1

import { Hero }           from './hero';

@Injectable()
export class HeroSearchService {

  constructor(private http: Http) {}

  search(term: string): Observable<Hero[]> { //2
    return this.http
               .get(`api/heroes/?name=${term}`)
               .map(response => response.json().data as Hero[]);
  }
}

1. HeroService와 비슷한 형태이긴 한데 toPromise대신 Observable 과 map이 사용되었습니다.  

2. http를 사용하여 api/heroes/?name=term 에 get 으로 요청하지고 map을 사용해서 Promise가 아닌 Observable을 그대로 return합니다. 이건 위에서 설명했던 것처럼 실시간 검색을 효율적으로 사용하기 위한 것으로 여기서는 설명하지 않겠습니다.

// src/app/hero-search.component.ts

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);
  }
}

이 부분은 위 코드와 영문 설명을 보고 이해가 안되시는 분들은 그냥 넘어가셔도 됩니다. HeroSearchService는 여기서만 사용되기 때문에 @component에 providers로 들어갔습니다. component에서 사용하고 싶으면? app.module의 providers에 넣으면 되는것 기억하고 계시나요?

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

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

 1. click이 아니라 keyup이 있을때마다 search를 호출 하는 것을 알 수 있습니다.

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

<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> //1

1.  검색 component가 삽입되었습니다. 이처럼 component 단위로 기능이 분리되면, 여러명이서 작업을 할 때 편합니다. 만약 hero search를 다른 팀이 만들었다면 이게 어떻게 작동되는지, 혹은 잘 작동하는지를 생각하지 않고 쉽게 template에 넣을 수 있습니다.

마지막으로 app.module입니다.

// src/app/app.module.ts

import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule }   from '@angular/forms';
import { HttpModule }    from '@angular/http';

import { AppComponent }        from './app.component';
import { DashboardComponent }  from './dashboard.component';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroesComponent }     from './heroes.component';
import { HeroService }         from './hero.service';
import { HeroSearchComponent } from './hero-search.component'; //1

// Imports for loading & configuring the in-memory web api
import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; //2
import { InMemoryDataService }  from './in-memory-data.service'; //2

import { AppRoutingModule }    from './app-routing.module';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    InMemoryWebApiModule.forRoot(InMemoryDataService), //2
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    DashboardComponent,
    HeroDetailComponent,
    HeroesComponent,
    HeroSearchComponent //1
  ],
  providers: [ HeroService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

1. 이번 강의에서 새로 생성된 HeroSearchComponent를 declarations에 넣고 있습니다.

2. Angular In Memory Web API는 app.module에 이런식으로 들어가는 것만 알고 넘어가시면 됩니다.

마치며

드디어 Angular 기본 tutorial 을 모두 살펴보았습니다. 남이 만든 코드 설명만 하는 거라 금방 될 줄 알았는데 개인적인 사정으로 한달이 넘게 걸렸네요. 지금도 계속 바쁜 중이라서.. 다음 강의 시리즈는 좀 시간이 나면 올리고 그동안은 단편 강의를 올릴 예정입니다.

수고 하셨습니다!

댓글

S
SunWoong Lee 2017.09.08
궁금한게 있습니다 ㅠㅠ Node.js 기반으로 서버를 돌리는 와중에 Angular4를 프론트엔드 프레임워크로 가져갈 경우에 어떤식으로 구현을 해야 하는지에 대한 감이 잡히지 않습니다. public 폴더 내부에 모든 angular의 static 파일들을 넣어놓고 그 자체를 serve하는 방법을 생각해보기도 했으나 뭔가 비효율적이라는 말도 들리고.. 프론트 서버를 따로 띄워놓고 서비스부분의 API 기능만을 수행하는 백엔드 서버를 따로 띄워놓은 후에 이 강의의 in memory DB 쓰듯이 통신해서 주고받는 것이 바람직한가요? 어떤 것이 옳은가요?
S
SunWoong Lee 2017.09.08
요약하자면,  angular2 이상 버전에서 어떤식으로 MEAN stack이 구현되는지 궁금합니다.
S
SunWoong Lee 2017.09.08
네... 다음 카테고리의 강의를 보니 잘 설명이 되어 있는데 제가 성급하게 질문을 한 것 아닌가 죄송스럽기도 하네요 허허..!
I
Ian H 2017.09.08
@SunWoong Lee,
http://www.a-mean-blog.com/ko/blog/단편강좌/_/Angular-CLI-사이트에-REST-API-서버-추가하기 강의에 설명이 되어 있습니다. 위 강의처럼 서버를 하나로 해서 돌리든, 아니면 프론트엔드 사이트 서버, 백엔드 API 서버를 따로 해서 2개로 돌리든 자신이 처해 있는 상황에 맞게 구성하시면 됩니다. 정답은 없어요. 다만 혼자서 연습하기에는 위 강의처럼 하나로 묶어서 하는 것이 덜 수고스럽죠^^ 아. 그리고 댓글은 언제나 환영합니다. 댓글이 없으면 내가 열심히 쓴 글을 사람들이 보기는 하는건지 그런 생각이 들 때도 있거든요 ㅋ
S
SunWoong Lee 2017.09.10
정말 잘 보고 있습니다. 부끄럽지만 잡쇼퍼라는 작은 스타트업에서 개발총괄을 맡고 있는데 인턴들 들어오면 맨날 여기 보라고 추천해줘요 ! 저희 서비스도 전에 네이버 블로그 올리셨을 때 그 블로그 글 보고 배운 지식으로  큰 뼈대는 구성했구요..! 다시 처음부터 보니까 네이버 블로그때보다 더 코드 정리가 깔끔하게 잘 되어있더라구요 ㅠㅠ 그래서 다시 또 보면서 첨부터 공부 해볼까 생각중입니다.  강의짱짱...!!
I
Ian H 2017.09.21
@SunWoong Lee,
도움이 된다니 기쁘네요 ㅠㅠ 저도 배우면서 새로 익힌것들을 기록하는 식으로 블로그를 운영하고 있는데.. 혹시라도 잘못된 정보가 나가게 될까봐 걱정이네요. 앞으로 더 열심히 하도록 하겠습니다!
비밀s 2018.01.31
안녕하세요. API관련 궁금한점이 있어서요. in-memory-data를 씀으로써 api/heroes API구현없이 자동적용 되는건가요? 호출부분은 hero.service.ts인것 같은데, 요청받은 후 응답해주는부분소스가 무엇인지 알고싶어서요.
I
Ian H 2018.02.01
@비밀s,
in-memory-data는 사이트 접속자(client)의 브라우저의 memory에 가상 API를 만드는 package로 실제 data가 서버DB에 저장되지는 않습니다. Angular 개발자가 서버DB없이 Angular를 개발할 수 있게 해주는 package입니다.
응답해주는 부분의 소스는 src/app/in-memory-data.service.ts입니다. 접속자가 접속을 하면 해당 데이터를 브라우저의 memory에 생성하게 되고 요청에 따라 해당 브라우저상의 데이터가 변경되게 됩니다.
댓글쓰기

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

UP