Page tree
Skip to end of metadata
Go to start of metadata

7장 - HTTP, RxJS

참고문서

HTTP 통신

Angular 에서는 HttpModule을 통해 HTTP 프로토콜을 지원한다.
이는 @angular/http 패키지에 포함되어 있다.
CLI로 생성된 프로젝트, 자동 생성된 모듈에는 이미 임포트 되어 있음.

HttpModule에서 제공하는 서비스

해당 모듈은 HTTP 통신을 위한 여러 서비스를 제공한다.
<예제 7-2> 참조. 반환값이 전부 Observable인걸 유의. (뒤에 다시 나옴)
이 서비스를 사용하려면 의존성 주입으로 인스턴스를 받아야한다. 의존성 주입에 대해선 저번 시간에 다뤘으니 생략하도록 한다.

  request(url: string|Request, options?: RequestOptionsArgs): Observable<Response> {
  }
  get(url: string, options?: RequestOptionsArgs): Observable<Response> {
  }
  post(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
  }
  put(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
  }
  delete (url: string, options?: RequestOptionsArgs): Observable<Response> {
  }
  patch(url: string, body: any, options?: RequestOptionsArgs): Observable<Response> {
  }
  head(url: string, options?: RequestOptionsArgs): Observable<Response> {
  }
  options(url: string, options?: RequestOptionsArgs): Observable<Response> {
  }

[https://github.com/angular/angular/blob/master/packages/http/src/http.ts#L102-L184Reference]

UserService 예제

=map=, =subscribe= 등은 뒤의 RxJS에서 다룰 예정이지만 간략하게 아래와 같은 흐름을 따른다.
클라이언트 -> httpmodule method (서버에 값 요청 -> 서버에서 값 반환) -> json 변환 -> subscribe 함수의 callback function 호출.
subscribe 함수는 RxJS 에서 나올 Observable 자료형의 메서드.

Q : httpmodule method의 반환값은 Observable 이다. 따라서 json 변환은 굳이 안해줘도 될 것 같은데 왜 해줬는지 궁금.

json은 객체를 표현하는 방법. 콜백 내에서 해당 객체를 맵핑하기 위한 방법 중 하나.
User라는 값을 만약 json으로 변환하지 않는다면 callback에서 json 또는 또다른 표현 형태로 변환해야 함.
앞선 스트림에서 변환을 해서 넘겨주면 편리하게 처리.

Const callback = res => {
	Const newUser: User = res.data;
	console.log(JSON.stringify(newUser));
	alert(`ID:${newUser.id}`);
};

237p 예제 7-7 일부 발췌

 

#+name : =user.service.ts=

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

  @Injectable()
  export class UserService {
    constructor(public http: Http) { }

    getUser(id: number, callback) {
      this.http.get(`/api/users/${id}`).map(res => res.json()).subscribe(callback);
    }

    addUser(user: any, callback) {
      this.http.post('/api/users', user).map(res => res.json()).subscribe(callback);
    }

    modifyUser(user: any, callback) {
      this.http.put(`/api/users/${user.id}`, user).map(res => res.json()).subscribe(callback);
    }

    removeUser(user: any, callback) {
      this.http.delete(`/api/users/${user.id}`).subscribe(callback);
    }
  }

실습: 초간단 사용자 조회 어플리케이션

Template

#+name : =app.component.html=

  <h2>Contact Manager : Http 실습</h2>
  <div>
    <h4>사용자 조회 / 삭제</h4>
    <label for="user-no">사용자 Id: </label><input type="number" name="user-id" #userIdInput />
    <button type="button" (click)="findUser(userIdInput.value)">검색</button>
    <button type="button" (click)="removeUser(userIdInput.value)">삭제</button>
    <div>{{searchedUser | json}}</div>
  </div>
  <div>
    <h4>사용자 등록 / 수정</h4>
    <label for="user-id">사용자 ID:</label> <input type="number" name="user-id" [(ngModel)]="user.id"> <br/>
    <label for="user-name">사용자 이름:</label> <input type="text" name="user-name" [(ngModel)]="user.name"> <br/>
    <label for="user-age">사용자 나이:</label> <input type="number" for="user-age" [(ngModel)]="user.age" />
    <button type="button" (click)="modifyUser()">사용자 등록/수정</button>
  </div>

Typescript code

#+name : =app.component.ts=

  export class AppComponent {
    user: User;
    searchedUser: User;

    constructor(public userService: UserService) {
      this.user = new User();
    }

    findUser(id) {
      const onSuccess = res => {
        const user = res.data;
        this.searchedUser = user;
      };

      this.userService.getUser(id, onSuccess);
    }

    addUser() {
      const newUser = { name: this.user.name, age: this.user.age };
      const callback = res => {
        const newUser: User = res.data;
        console.log(JSON.stringify(newUser));
        alert(`사용자 생성\nID:${newUser.id}\n이름:${newUser.name}\n나이:${newUser.age}`);
      };

      this.userService.addUser(newUser, callback);
      this.user = new User();
    }

    modifyUser() {
      if (this.user.id === 0) {
        this.addUser();
        return;
      }

      const callback = res => {
        const newUser: User = res.data;
        console.log(JSON.stringify(newUser));
        alert(`사용자 변경\nID:${newUser.id}\n이름:${newUser.name}\n나이:${newUser.age}`);
      }

      this.userService.modifyUser(this.user, callback);
    }

    removeUser(id) {
      const onSuccess = res => {
        if (res.status === 204) {
          alert(`사용자 ID:${id} 삭제 성공`);
          console.log(res);
          return;
        }
        alert(`사용자 ID:${id} 삭제 실패`);
      };

      this.userService.removeUser(id, onSuccess);
    }

  }

실습: angular-in-memory-web-api

서버가 없으면 Html 통신을 해도 의미 없다.
npm 패키지 중 =angular-in-memory-web-api= 를 활용하면 http통신을 서버 없이(통신 없이) 로컬 컴퓨터 내에서 진행 해 볼 수 있다.
생략함

RxJS

자바스크립트 라이브러리.
비동기 이벤트 처리에 최적화 되어있음.

비동기 이벤트 처리에는 까다로운 점이 많음.
1. 코드 호출 시점과 결과가 리턴되서 처리되는 시점이 다름.
2. 코드 작성 순서에 따라 실행되지 않기 때문에, 코드 선후 관계 처리가 어려움.

자바스크립트, Ajax
콜백호출, 이벤트와 이벤트 리스너 콜백의 조합으로 해결. ===> 코드가 복잡해지고 길어지면 힘들어 질 수 있음.
이벤트 기반 로직이 필수!

Observable, Observer, Subscription

Observable

  • 데이터 소스의 wrapper 클래스
  • List, Stream, Function 등을 Observable 로 만들 수 있음.
  • Observable 은 Observer 를 Subscribe 메서드 인자로 전달 받을 수 있음.
  • Observable 은 subscribe 인자로 전달된 observer의 =next= , =error=, =complete= 메서드를 호출함. 이를 통해 Observer로 적절한 값 전달.
  • 개념적으로 ‘이벤트들의 Stream’이라는 개념과 유사

Observer

  • =next=, =error=, =complete= 세 종류의 메서드를 가지고 있음.
  • Observable에 등록해 데이터를 전달 받을 수 있음.
  • =Rx.Observer.create= 를 이용해 직접 만들 수도 있음.
  • 보통은 =source.subscribe(func_x, func_e, func_c)= 를 이용해 implicit 하게 만들어 쓰는 듯.

http://reactivex.io/rxjs/class/es6/MiscJSDoc.js~ObserverDoc.html

  interface Observer<T> {
    closed?: boolean;
    next: (value: T) => void;
    error: (err: any) => void;
    complete: () => void;
  }

직접 만들어 쓰는 경우:

  var source = Rx.Observable.return(42);
  var observer = Rx.Observer.create(
      function (x) {
          console.log('Next: ' + x);
      },
      function (err) {
          console.log('Error: ' + err);
      },
      function () {
          console.log('Completed');
      }
  );
  var subscription = source.subscribe(observer);

  // => Next: 42
  // => Completed

Implicit 하게 쓰는 경우:

  // Creates an observable sequence of 5 integers, starting from 1
  var source = Rx.Observable.range(1, 5);

  // Create observer
  var observer = Rx.Observer.create(
    function (x) { console.log('onNext: %s', x); },
    function (e) { console.log('onError: %s', e); },
    function () { console.log('onCompleted'); });

  // Prints out each item
  var subscription = source.subscribe(observer);

  // => onNext: 1
  // => onNext: 2
  // => onNext: 3
  // => onNext: 4
  // => onNext: 5
  // => onCompleted

RxJS Operators

RxJS 대부분의 오퍼레이터의 반환값은 Observable.
연산자 사용을 위해 아래와 같이 연산자 임포트를 해줘야한다. (원하는 파일에)

  import 'rxjs/add/operator/map'

Map

[http://xgrommx.github.io/rx-book/content/observable/observable_instance_methods/map.htmlGraphical Representation!]
인자로 주어진 함수를 Source의 각 value에 적용.

[1, 2, 3, 4, 5].map(x = > 10*x)
==> [10, 20, 30, 40, 50]

Reduce

[http://xgrommx.github.io/rx-book/content/observable/observable_instance_methods/reduce.htmlReduce]

[1, 2, 3, 4, 5].reduce((x, y) => x+y))
==> 0 + reduce([1, 2, 3, 4, 5])
==> 1 + reduce([2, 3, 4, 5])
==> 3 + reduce([3, 4, 5])
==> 6 + reduce([4, 5])
==> 10 + reduce([5])
==> 15 + reduce([])
==> 15

Filter

[http://xgrommx.github.io/rx-book/content/observable/observable_instance_methods/filter.htmlfilter]

[1, 2, 3, 4, 5].filter( x => x%2 === 0)
==> [2, 4]

Zip

[http://xgrommx.github.io/rx-book/content/observable/observable_instance_methods/zip.htmlzip]

zip([1, 2, 3, 4], [a, b, c, d, e, f], ( (num, char) => (num , char)))
==> [ (1, a), (2, b), (3, c), (4, d) ]

Code Examples

Map

  const num$ = Rx.Observable.from([1,2,3,4,5,6,7,8,9,10]);

  num$
      .map.(n => n*2)
      .subscribe(n => console.log('num: ${n}'));

Reduce

  const num$ = Rx.Observable.from([1,2,3,4,5,6,7,8,9,10]);
  
  num$
      .filter(n => n % 2 ===0)
      .reduce((acc, val) => acc + val, 0)
      .subscribe(res => console.log('result: ${res}'));

Filter

  const num$ = Rx.Observable.from([1,2,3,4,5,6,7,8,9,10]);
  num$
      .filter(n => n % 2 === 0)
      .subscribe(n => console.log('num: ${n}'))

Zip

(num, int) 의 값은 앞서 전달한 Observable들의 순서에 의해서 매칭됨
(num, int) => num은 두 번째 Observable 값은 버려버리겠다는 뜻.
2번째 Observable인 UntilFive$ 의 역할은 출력되는 숫자의 개수를 5개로 제한하는 것.

  untilFive$ = Rx.Observable.interval(1000).take(5);
  const num$ Rx.Observable.from([1,2,3,4,5,6,7,8,9,10]).map(n => n* 2);
  Observable.zip(num$, untilFive$, (num, int) => num).subscribe(n=>console.log(n));

 

RxJS를 이용한 마우스 위치 로거 코드 개선

Subject

var subject = new Rx.Subject();

var subscription = subject.subscribe(
    function (x) {
        console.log('Next: ' + x.toString());
    },
    function (err) {
        console.log('Error: ' + err);
    },
    function () {
        console.log('Completed');
    });

subject.onNext(42);

// => Next: 42

subject.onNext(56);

// => Next: 56

subject.onCompleted();

// => Completed

http://xgrommx.github.io/rx-book/content/subjects/subject/index.html

  <div class="track-zone" (mousemove) = "captureMousePos($event)"></div>

코드가 github에 업로드 되있지 않음.

  export class MouseTrackZoneComponent implements onInit {
      logger: LoggerService;
      moveSubject: Subject<MouseEvent> = new Subject<MouseEvent()>;
      move$: Observable<MouseEvent> = this.moveSubject.asObservable();

      constructor(
          @Host() @Optional() mySpecialLogger: MySpecialLoggerService,
          anotherLogger: AnotherLoggerService
      ){
          this.logger = mySpecialLogger ? mySpecialLogger : anotherLogger;
      }
      ngOnInit() {
          this.move$
              .throttleTime(1000)
              .map(evt => [evt.clientX, evt.clientY])
              .subscribe(pos => this.logger.info('x:${pos[0]} y:${pos[1]}'))
      }

      captureMousePos($event: MouseEvent) {
          this.logger.debug('click event occured');
          this.moveSubject.next($event);
      }
  }

1. HTML 코드에서, captureMousePos로 mouse event를 전달.

mousemove 이벤트를 =captureMousePos= 에 바인딩.

  <div class="track-zone" (mousemove) = "captureMousePos($event)"></div>

2. 전달받은 이벤트를 Observer에 전달.

      captureMousePos($event: MouseEvent) {
          this.logger.debug('click event occured');
          this.moveSubject.next($event);
      }

이 경우 Observer는 또 다른 Observable인 move$
move$는 Observe를 moveSubject를 subscript 함수가 아닌 asObservable 메서드로 하는중.

  moveSubject: Subject<MouseEvent> = new Subject<MouseEvent()>;
  move$: Observable<MouseEvent> = this.moveSubject.asObservable();

3. 최종적으로 이벤트는 move$를 subscribe 하고있는 observer(이 경우 람다펑션)로 전달됨.

          this.move$
              .throttleTime(1000)
              .map(evt => [evt.clientX, evt.clientY])
              .subscribe(pos => this.logger.info('x:${pos[0]} y:${pos[1]}'))

4. 로거에 x, y의 포지션을 찍게 됨.

    pos => this.logger.info('x:${pos[0]} y:${pos[1]}')

사용자 관리 어플리케이션을 통한 예제

user.service.ts

addUser(user: any, callback) {
      this.http.post('/api/users', user).map(res => res.json()).subscribe(callback);
    }

app.component.ts

addUser() {
      const newUser = { name: this.user.name, age: this.user.age };
      const callback = res => {
        const newUser: User = res.data;
        console.log(JSON.stringify(newUser));
        alert(`사용자 생성\nID:${newUser.id}\n이름:${newUser.name}\n나이:${newUser.age}`);
      };

      this.userService.addUser(newUser, callback);
      this.user = new User();
    }
  • No labels