Communicating with backend services using HTTP

kukudas·2022년 3월 1일
0

Angular

목록 보기
13/15

Setup for server communication

AppModule에다가 HttpClient를 import 해서 HttpClient 서비스가 앱의 어느곳에서든 사용가능하게 해줌.

// src/app/app.module.ts

// HttpClient 사용하기 위함
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [
    HttpClientModule,
  ],
})
export class AppModule { }

임시로 test.service.ts라는 서비스 만들어줌.
HttpClient는 observable을 transaction에 사용하기 때문에 obersavlbe을 import 해줘야함

// test.service.ts
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs';

Requesting data from a server

HttpClient.get() 메소드로 서버에서 데이터를 가져올거임. 얘는 비동기 메소드로 HTTP 리퀘스트를 보내고 response를 받으면 Observable을 리턴해주는데 Observable은 response를 받으면 리퀘스트 했던 데이터를 emit 해줌. 리턴타입은 메소드를 호출할때 넘겨줬던 observeresponseType에 따라서 달라짐.

get()은 2개의 인자를 받음. 하나는 데이터를 가져올 endpoint URL이고 다른 하나는 request를 configure 해주는 option 오브젝트임.

options: {
    headers?: HttpHeaders | {[header: string]: string | string[]},
    observe?: 'body' | 'events' | 'response',
    params?: HttpParams|{[param: string]: string | number | boolean | ReadonlyArray<string | number | boolean>},
    reportProgress?: boolean,
    responseType?: 'arraybuffer'|'blob'|'json'|'text',
    withCredentials?: boolean,
  }

옵션은 observeresponseType property를 포함하는데 observe는 HTTP response를 어떤 범위까지 반환할지 정해주고responseType은 리턴된 데이터의 포맷을 정해줌.

앱이 JSON data를 서버에 요청할 때 get()의 option은 {observe: 'body', responseType: 'json'}여야 하는데 이 값은 default라서 안써줘도 됨.

따라서 데이터를 가져오려면 아래처럼 서비스에서 가져오고 observable을 리턴해주고 컴포넌트에서 서비스를 구독해서 리턴된거를 받아오면됨.

// app/config/config.service.ts (getConfig v.1)
configUrl = 'assets/config.json';
getConfig() {
  return this.http.get<Config>(this.configUrl);
}

// app/config/config.component.ts (showConfig v.1)
showConfig() {
  this.configService.getConfig()
    .subscribe((data: Config) => this.config = {
        heroesUrl: data.heroesUrl,
        textfile:  data.textfile,
        date: data.date,
    });
}

Requesting a typed response

HttpClient request에 reponse type을 정해줄 수 있음. 이렇게하면 compile time에 type assertion이 됨.

이거는 build-time에 체크가 되고 서버가 실제로 이 타입으로 객체를 리턴하는지는 보장못함. 서버가 리턴하는거는 전적으로 서버 API가 결정함.

response 객체의 타입을 특정해주려면 인터페이스로 하면됨.

// 예시
export interface Config {
  heroesUrl: string;
  textfile: string;
  date: any;
}

이렇게 인터페이스를 만들었으면 해당 인터페이스를 HttpClient.get()의 타입 패러미터로 넣어주면됨.

// app/config/config.service.ts (getConfig v.2)
getConfig() {
  // now returns an Observable of Config
  return this.http.get<Config>(this.configUrl);
}

인터페이스를 HttpClient.get()의 타입 패러미터로 넘겨주면 RxJs의 map operator를 사용해서 reponse 데이터를 필요에 맞게 변환시키면됨. 이렇게 변환시킨 데이터는 async pipe로도 넘겨줄 수 있음.

컴포넌트의 콜백 메소드에서 위에서 정한 Config 타입의 데이터를 받게됨. 이렇게 하면 좀 더 안전하고 쉽게 데이터를 사용할 수 있음.

// app/config/config.component.ts (showConfig v.2)

config: Config | undefined;

showConfig() {
  this.configService.getConfig()
    // clone the data object, using its known Config shape
    .subscribe((data: Config) => this.config = { ...data });
}

인터페이스에 정의된 프로퍼티에 접근하려면 JSON으로 받은 객체를 해당 RESPONSE 타입으로 바꿔줘야함. 아래의 subscribe 콜백은 data를 객체로 받아서 property에 접근하기위해 타입캐스트를 해줌.

.subscribe(data => this.config = {
  heroesUrl: (data as any).heroesUrl,
  textfile:  (data as any).textfile,
});

OBSERVE AND RESPONSE TYPES
observeresponse 옵션의 타입은 plain string이 아니라 string uninon임.

options: {
    ...
    observe?: 'body' | 'events' | 'response',
    ...
    responseType?: 'arraybuffer'|'blob'|'json'|'text',
    ...
  }

따라서 문제가 생길 수 있는데 2번째 예시에서 타입스크립트가 options의 타입을 {responseType: string}으로 하게되서 HttpClient.get가 기대하는 responseType의 타입인 특정한 string이랑 안맞게됨.

// this works
client.get('/foo', {responseType: 'text'})
// but this does NOT work
const options = {
  responseType: 'text',
};
client.get('/foo', options)

그렇기 때문에 아래처럼 as const를 사용해서 타입스크립트가 constant string type을 사용한다고 알려줘야함.

const options = {
  responseType: 'text' as const,
};
client.get('/foo', options);

Reading the full response

typed response를 받을때는 HttpClient.get()에 아무 옵션도 안넣어줬음. 옵션 안넣으면 HttpClient.get()는 default로 response body에 있는 JSON 데이터를 리턴해줌.

response body말고 헤더나 status code를 보고 싶을 경우가 있으니 full response를 봐야할 때도 있음. full response를 보고 싶으면 HttpClient.get(){ observe: 'response' }옵션을 주면됨.
이렇게하면 HttpClient.get()는 이제 response body에 있는 JSON 데이터가아니라 HttpResponse타입인 Observable을 리턴해줌.

// app/config/config.service.ts
getConfigResponse(): Observable<HttpResponse<Config>> {
  return this.http.get<Config>(
    this.configUrl, { observe: 'response' });
}

이제 그러면 컴포넌트에서는 헤더랑 body랑 같이 볼 수 있음.

// app/config/config.component.ts
headers: string[] = [];

showConfigResponse() {
  this.configService.getConfigResponse()
    // resp is of type `HttpResponse<Config>`
    .subscribe(resp => {
      // display its headers
      const keys = resp.headers.keys();
      this.headers = keys.map(key =>
        `${key}: ${resp.headers.get(key)}`);

      // access the body directly, which is typed as `Config`.
      this.config = { ...resp.body! };
    });
}

아래처럼 해서 볼 수 있음.

// app/config/config.component.html
<div>{{config | json}}</div>

Handling request errors

request가 서버에서 실패하면 HttpClienterror 객체를 리턴해줌.

서버와의 transaction을 ㄷ마당하는 서비스가 에러도 해결해야함.

에러가 발생하면 에러가 발생한 이유도 알 수 있어서 유저에게 알려줄 수도 있고 자동으로 request를 다시 보내게 만들 수도 있음.

Getting error details

2가지의 에러가 발생할 수 있음.

  • 서버 벡엔드가 request를 거절해서 404나 500을 리턴할 경우임. 이를 error response라고함.
  • 클라이언트 쪽에서 네트워크 에러나 RxJS operator가 예외를 throw하는 경우임. 이 에러들은 status가 0이고 error property가ProgressEvenet 객체를 가지고 있는데 얘의 type이 추가적인 정보를 제공할 수 있음.

HttpClient는 위 2가지의 에러를 잡아서 HttpErrorResponse에 넣어주니까 이걸 보면 에러의 원인을 알 수 있음.

이렇게 에러 핸들러를 서비스 안에 만들어 주면됨.
핸들러는 RxJS ErrorObservable랑 유저 친화적인 에러 메시지를 리턴해줌.

// app/config/config.component.ts
private handleError(error: HttpErrorResponse) {
  if (error.status === 0) {
    // A client-side or network error occurred. Handle it accordingly.
    console.error('An error occurred:', error.error);
  } else {
    // The backend returned an unsuccessful response code.
    // The response body may contain clues as to what went wrong.
    console.error(
      `Backend returned code ${error.status}, body was: `, error.error);
  }
  // Return an observable with a user-facing error message.
  return throwError(() => new Error('Something bad happened; please try again later.'));
}

아래는 에러 핸들러를 추가한 것임. pipe를 이용해서 HttpClient.get()가 리턴하는 모든 observable을 에러 핸들러에 보냈음.

// app/config/config.service.ts
getConfig() {
    // now returns an Observable of Config
    return this.http.get<Config>(this.configUrl);
  }
// 위에는 에러 처리 없는데 아래처럼 에러 처리 추가해야함.
getConfig() {
  return this.http.get<Config>(this.configUrl)
    .pipe(
      catchError(this.handleError)
    );
}

이렇게 에러 검출한거 컴포넌트에서 아래처럼해서 가져올 수 있음.

// app/config/config.component.ts
showConfig() {
    this.testService.getConfig()
      // clone the data object, using its known Config shape
      // https://rxjs.dev/deprecations/subscribe-arguments
      .subscribe({
        next: (v) => this.config = v,
        error: (e) => this.errorMessage = e
      });
  }

Retrying a failed request

다시 시도하면 에러가 사라지는 경우도 있음. 예를 들어서 모바일 환경에서 네트워크 장애는 자주일어나는 일이어서 다시 시도하면 잘되는 경우가 많음.

RxJS 라이브러리는 여러개의 retry operator를 제공함. 예시로 retry() operator는 자동으로 실패한 Observable에 특정 횟수만큼 재구독해줌. HttpClient메소드의 결과를 재구독하는거는 HTTP request를 다시 하는거랑 같은 효과임.

// app/config/config.service.ts (getConfig with retry)
getConfig() {
  return this.http.get<Config>(this.configUrl)
    .pipe(
      retry(3), // retry a failed request up to 3 times
      catchError(this.handleError) // then handle the error
    );
}

Sending data to a server

서버에서 데이터를 가져오는 것 뿐만 아니라 HttpClient는 HTTP 메소드인 PUT, POST, DELETE로 서버의 데이터를 변경해 줄 수 있음.

Making a POST request

앱들은 폼을 제출할때 데이터를 POST request를 사용해서 서버로 보냄. 아래는 서버로 POST request를 보낸 것임.

// app/config/config.service.ts
import { HttpHeaders } from '@angular/common/http';

// httpOptions으로 헤더 만들어서 넣어줄 수 있음.
token ='Token 여따토큰넣으면됨';
httpOptions = {
    headers: new HttpHeaders({ 'Authorization': this.token })
  };

addHero(post: Post1): Observable<Post1> {
    return this.http.post<Post1>(this.postUrl, post, this.httpOptions)
    .pipe(
      catchError(this.handleError)
    );
  }
  
  
// app/config/config.component.ts
interface Post1 {
  title: string,
  text: string
}
name: Post1 = {title: '테스트제목1', text: '테스트내용1'}
  
add(name: Post1): void {
	if (!name) { return; }
		this.testService.addHero(name as Post1)
			.subscribe();
}

Making a DELETE request

HttpClient.delete를 사용해서 hero를 삭제할 수도 있음.

// app/config/config.service.ts
/** DELETE: delete the hero from the server */
deleteHero(id: number): Observable<unknown> {
    const url = `${this.postUrl}/${id}`;

    return this.http.delete<Post1>(url, this.httpOptions)
    .pipe(
      catchError(this.handleError)
    );
  }
    
// app/config/config.component.ts
// 이거 리턴안받아도 Observable에 제너릭 써줘야함.
deleteHero(id: number): Observable<unknown> {
    const url = `${this.postUrl}/${id}`;

    return this.http.delete<Post1>(url, this.httpOptions)
    .pipe(
      catchError(this.handleError)
    );
}

컴포넌트는 delete opertation에서 result를 기대하지 않으니까 콜백없이 subscribe함. result가 없어도 구독해야하는데 subscribe()를 해야지 실제 DELETE request가 시작되는거임. 이거안하고 config.service.deleteHero 불러도 DELETE request 시작안함.

Always subscribe!

모든 HttpClient 메소드는 메소드가 리턴하는 obeservable에 대해서 subscribe()를 안부르면 HTTP request를 보내지 않음. 따라서 subscribe()부르기전에

AsyncPipe는 구독과 구독해제를 자동으로 해줌.

옵저버블 리턴된건 데이터를 바로바로 담고있는게 아니라 말 그대로로 observable이고 데이터스트림을 의미함. 구독을 해야지 뭔가 되는거고 실제로 데이터가 올지안올지는 모르는거임. 그래서 거기다가 미리 데이터가 오면 동작할것들을 정의해두고 데이터가 실제로 와야지 그런 동작이 실행이됨.

Making a PUT request

PUT request도 보낼 수 있음. 아래는 예시임.

// app/config/config.service.ts
/** PUT: update the hero on the server */
  updateHero(hero: Post1): Observable<Config> {
    return this.http.put<Config>(this.postUrl2, hero, this.httpOptions)
    .pipe(
      catchError(this.handleError)
    );
  }
// app/config/config.component.ts
update(name: Post1): void {
    this.testService.updateHero(name)
    .subscribe({
      next: (v) => this.temp = v,
      error: (e) => this.errorMessage = e
    });
  }

Adding and updating headers

서버가 추가적인 헤더를 요구하는 경우가 많음. 예를 들어서 auth token이나 Content-Type을 필요로하는 경우임.

Adding headers

아래 처럼 헤더 만들어줄 수 있음.

httpOptions = {
    headers: new HttpHeaders({ 'Authorization': this.token })
  };

Updating headers

HttpHeaders 클래스의 인스턴스는 immutable하니 직접적으로 값을 변경해줄 수 없음. 따라서 set() 메소드로 현재 인스턴스의 클론을 만들고 클론에다가 업데이트를 해서 사용하면됨.

아래처럼하면 토큰이 만료되었을때 새 토큰을 넣어줄 수 있음.

this.httpOptions.headers =
      this.httpOptions.headers.set('Authorization', 'my-new-auth-token');

Configuring HTTP URL parameter

HttpParams 클래스의 params request 옵션으로 HttpRequest의 URL에 쿼리스트링 담을 수 있음.

아래처럼하면 request URT은 term이 나는쿼리스트링이니 postURL/?name=나는쿼리스트링 이렇게 가고 이거는 URL-encode해서 감.

// app/config/config.service.ts
import {HttpParams} from "@angular/common/http";

searchHeroes() {
    let term = '나는쿼리스트링'

    // Add safe, URL encoded search parameter if there is a search term
    const options = term ?
      { headers: new HttpHeaders({ 'Authorization': this.token }), params: new HttpParams().set('name', term) } : {};

    return this.http.get(this.postUrl, options)
      .pipe(
        catchError(this.handleError)
      );
}

아래 처럼 잘 나오는 것을 볼 수 있음.

HttpParams 객체도 불변이어서 값을 바꿔주려면 .set()으로 복사해서 새로 만들어줘야함.

fromString을 사용해서 HTTP parameters를 쿼리스트링에서 직접적으로 만들수도 있음.

const params = new HttpParams({fromString: 'name=foo'});

const options =
      { headers: new HttpHeaders({ 'Authorization': this.token }), params: new_params }

Intercepting requests and responses

interceptors를 만들어서 앵귤러 앱에서 서버로 가는 HTTP request를 바꿔줄 수 있음. 같은 인터셉터로 서버에서 앱으로 오는 response도 받아서 변경가능함. 여러개의 인터셉터를 이어서 reqeust/reponse 핸들러 체인을 만들 수 있음.

인터셉터가 없으면 HttpClient 메소드를 사용할 때마다 일일이 바꿔주는 작업을 넣어줘야함.

Write an interceptor

인터셉터를 사용하기 위해서 HttpInterceptor 인터페이스의intercept() 메소드를 구현한 클래스를 만들어야함.

아래는 아무것도 안하는 인터페이스임. 지금은 아무것도 안하고 있어서 reqeust를 받아서 암것도 안하고 다시 넘겨줌.

// app/http-interceptors/noop-interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
} from '@angular/common/http';

import { Observable } from 'rxjs';

/** Pass untouched request through to the next request handler. */
@Injectable()
export class NoopInterceptor implements HttpInterceptor {

  intercept(req: HttpRequest<any>, next: HttpHandler):
    Observable<HttpEvent<any>> {
    return next.handle(req);
  }
}

interceptor 메소드가 request를 HTTP response를 리턴하는 Observable로 바꿔줌. 따라서 각각의 인터셉터는 request를 혼자서도 처리할 수 있음.

interceptor()처럼 handle()도 HTTP request를 HttpEventsObservable로 바꿔주는데 얘는 궁극적으로 서버의 response를 포함하게됨. interceptor() 메소드는 해당 observable을 보고 내용을 바꿔줘서 호출한 곳으로 돌려줌.

The next object

next 객체는 인터셉터 체인에서 다음 인터셉터를 나타냄. 마지막에 나오는 nextHttpClient 백엔드 핸들러로 서버로 request를 보내고 response를 받아줌.

대부분의 인터셉터가 next.handle()를 호출해서 request가 다음 인터셉터로 넘어가게 하고 결과적으로 백엔드 핸들러가 받아서 서버로 나가고 서버에서 들어오도록함. 인터셉터가 next.handle() 호출을 건너뛰고 자신만의 인공적인 서버 response인 Observable을 만들어서 리턴할 수도 있음.

Provide the interceptor

NoopInterceptor은 앵귤러의 dependency injection(DI)이 관리하는 서비스임. 다른 서비스들과 같이 앱이 사용하기전에 인터셉터 클래스를 provide 해줘야함.

인터셉터가 HttpClient 서비스의 optional 한 dependency기 때문에 HttpClien를 provide 해주는 인젝터에 provide 해줘야함(아니면 injector의 parent에다가 해야함). DI가 HttpClien를 생성한 다음에 provide하는 인터셉터는 무시됨.

지금 계속 만드는 앱은 HttpClientModuleAppModule에 import 하고 있기 때문에 root injector에다가 HttpClient를 provide 하고 있음. 따라서 AppModule에다가 인터셉터를 provide 해줘야함.

HTTP_INTERCEPTORS를 import 한다음에 아래처럼 provider를 만들 수 있음. multi: true는 앵귤러에 HTTP_INTERCEPTORS가 하나가 아니라 배열로 여러개를 inject 한다고 알려주는것임.

{ provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },

AppModuel의 provider에 직접 provider를 넣어줄 수도 있는데 그렇게 하면 가독성이 떨어지니 barrel 파일을 만들어서 모든 interceptor provider를 httpInterceptorProviders 배열에 넣어서 provide 해주는게 좋음. 이 배열은 NoopInterceptor으로 시작함. 이거 인터셉터 provide 하는 순서 중요함. provide 하는 순서대로 인터셉터가 적용됨.

// app/http-interceptors/index.ts
/* "Barrel" of Http Interceptors */
import { HTTP_INTERCEPTORS } from '@angular/common/http';

import { NoopInterceptor } from './noop-interceptor';

/** Http interceptor providers in outside-in order */
export const httpInterceptorProviders = [
  { provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true },
];

// app.module.ts
import { httpInterceptorProviders } from './_http-interceptors';

providers: [
  httpInterceptorProviders
],

위 처럼 배럴 파일 만들고 AppModuel의 providers 배열에 넣어주면 됨.

Interceptor order

앵귤러는 인터셉터를 provide 한 순서대로 인터셉터를 적용함. 얘를 들어서 HTTP request를 보내기전에 authenticatio 작업을 하고 그 다음에 request를 기록하고 서버로 request를 보내고 싶다고 할때, 해당 작업을 수행하려면 다음과 같이해야함. AuthInterceptor service를 먼저 provide 하고 그 다음에 LoggingInterceptor service를 provide 해야함. 나가는 request는 AuthInterceptor를 거친후에 LoggingInterceptor를 통과하고 나가게됨. response가 오면 반대로 LoggingInterceptor를 거쳤다가 AuthInterceptor를 통과하게 됨.

마지막 인터셉터는 언제나 HttpBacken로 서버와의 통신을 담당함.

인터셉터 순서를 나중에 못바꿈. 변경하려면 인터셉터 안에다가 기능을 넣어줘야함.

Handling interceptor events

대부분의 HttpClient 메소드는 HttpResponse<any> 인 observable을 리턴해줌. HttpResponse는 자체가 HttpEventType.Response 타입인 이벤트임. 하나의 HTTP request가 다양한 타입을 가진 여러개의 이벤트를 생성할 수 있음.

많은 인터셉터는 나가는 request에 관련되어 있고 대부분 request를 수정하지 않고 next.handle()에서 나오는 이벤트 스트림을 리턴해줌. 그렇지만 몇몇 인터셉는 next.handle()에서 나오는 response를 보고 수정하기도 함.

인터셉터가 request랑 response를 수정가능하긴 하지만 HttpRequestHttpResponse의 인스턴스 property들은 readonly여서 immutable함. immutable의 장점은 request가 실패하면 똑같은 request로 인터셉터 체인을 다시 탈 수가 있음.

특별한 이유가 없으면 인터셉터는 이벤트를 수정하지않고 그대로 리턴해줘야함.

아래처럼 하면 Cannot assign to 'url' because it is a read-only property.로 수정못한다고 나옴.

req.url = req.url.replace('http://', 'https://');

request를 수정해야하면 먼저 clone한다음에 clone한거를 next.handle()로 넘겨주면됨.

// clone request and replace 'ht://' with 'http://' at the same time
 const secureReq = req.clone({
            url: req.url.replace('ht://', 'http://')
        });

// send the cloned, "secure" request to the next handler.
return next.handle(secureReq);

Modifying a request body

readonly는 deep update를 방지 못하고, 특히 request body 객체의 property 변경을 막지 못함.
아래는 잘못된 예시임.

req.body.name = req.body.name.trim(); // bad idea!

따라서 request body를 수정하려면 아래의 단계를 밟아야함.

  1. body를 복사해서 복사본에서 수정을함
  2. request 객체를 clone()으로 클론함.
  3. 클론의 body를 수정한 body로 바꿔줌.
// app/http-interceptors/trim-name-interceptor.ts (excerpt)
// copy the body and trim whitespace from the name property
const newBody = { ...body, name: body.name.trim() };
// clone request and set its body
const newReq = req.clone({ body: newBody });
// send the cloned request to the next handler.
return next.handle(newReq);

Clearing the request body in a clone

가끔 request body를 바꾸는 것보다 없애야 할 경우가 있음. 그렇게하려면 clone한 request body를 null로 해주면됨.

clone한 request body를 ndefined로 하면 앵귤러는 body를 그냥 원래 그대로 냅둠.

newReq = req.clone({ ... }); // body not mentioned => preserve original body
newReq = req.clone({ body: undefined }); // preserve original body
newReq = req.clone({ body: null }); // clear the body

Http interceptor use-cases

아래는 흔한 인터셉터 사용 예시임.

Setting default headers

앱은 나가는 request에 default 헤더를 추가해야하는 경우가 많음.

아래는 AuthInterceptor에서 AuthService를 사용해서 토큰을 가져와 헤더에 토큰을 넣어서 모든 나가는 reqeust의 헤더에 토큰을 넣어줌.

// app/http-interceptors/auth-interceptor.ts
import { AuthService } from '../auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private auth: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    // Get the auth token from the service.
    const authToken = this.auth.getAuthorizationToken();

    // Clone the request and replace the original headers with
    // cloned headers, updated with the authorization.
    const authReq = req.clone({
      headers: req.headers.set('Authorization', authToken)
    });

    // send cloned request with header to the next handler.
    return next.handle(authReq);
  }
}

reqeust를 클론해서 새 헤더를 넣어주는 상황이 많아서 setHeaders가 있는데 이거를 사용하면 좀 더 편함.

// Clone the request and set the new header in one step.
const authReq = req.clone({ setHeaders: { Authorization: authToken } });

헤더를 바꾸는 인터셉터는 다음과 같은 작업에 사용될 수 있음.

  • Authentication/authorization
  • Caching behavior; for example, If-Modified-Since
  • XSRF protection

Logging request and response pairs

인터셉터가 request랑 response를 둘 다 처리할 수 있기 때문에 전체 HTTP 작업을 기록하는데 사용할 수 있음.

아래는 LoggingInterceptor에서 request를 한 시간, response가 돌아온 시간을 보고 얼마나 걸린지를 MessageService에 기록해줌.

// app/http-interceptors/logging-interceptor.ts)

import { finalize, tap } from 'rxjs/operators';
import { MessageService } from '../message.service';

@Injectable()
export class LoggingInterceptor implements HttpInterceptor {
  constructor(private messenger: MessageService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler) {
    const started = Date.now();
    let ok: string;

    // extend server response observable with logging
    return next.handle(req)
      .pipe(
        tap({
          // Succeeds when there is a response; ignore other events
          next: (event) => (ok = event instanceof HttpResponse ? 'succeeded' : ''),
          // Operation failed; error is an HttpErrorResponse
          error: (error) => (ok = 'failed')
        }),
        // Log when response observable either completes or errors
        finalize(() => {
          const elapsed = Date.now() - started;
          const msg = `${req.method} "${req.urlWithParams}"
             ${ok} in ${elapsed} ms.`;
          this.messenger.add(msg);
        })
      );
  }
}

RxJS의 tap operator는 request가 성공하거나 실패한거를 capture함. RxJS의 finalize operator는 response observable의 성공, 실패와 상관없이 MessageService에다가 저장해줌.

tap이랑 finalize 모두 observable stream의 값을 건드리지 않고 caller에게 리턴해줌.

출처

0개의 댓글