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';
HttpClient.get()
메소드로 서버에서 데이터를 가져올거임. 얘는 비동기 메소드로 HTTP 리퀘스트를 보내고 response를 받으면 Observable을 리턴해주는데 Observable은 response를 받으면 리퀘스트 했던 데이터를 emit 해줌. 리턴타입은 메소드를 호출할때 넘겨줬던 observe
랑 responseType
에 따라서 달라짐.
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,
}
옵션은 observe
랑 responseType
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,
});
}
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
observe
랑response
옵션의 타입은 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);
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>
request가 서버에서 실패하면 HttpClient
는 error
객체를 리턴해줌.
서버와의 transaction을 ㄷ마당하는 서비스가 에러도 해결해야함.
에러가 발생하면 에러가 발생한 이유도 알 수 있어서 유저에게 알려줄 수도 있고 자동으로 request를 다시 보내게 만들 수도 있음.
2가지의 에러가 발생할 수 있음.
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
});
}
다시 시도하면 에러가 사라지는 경우도 있음. 예를 들어서 모바일 환경에서 네트워크 장애는 자주일어나는 일이어서 다시 시도하면 잘되는 경우가 많음.
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
);
}
서버에서 데이터를 가져오는 것 뿐만 아니라 HttpClient
는 HTTP 메소드인 PUT, POST, DELETE로 서버의 데이터를 변경해 줄 수 있음.
앱들은 폼을 제출할때 데이터를 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();
}
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 시작안함.
모든 HttpClient
메소드는 메소드가 리턴하는 obeservable에 대해서 subscribe()
를 안부르면 HTTP request를 보내지 않음. 따라서 subscribe()
부르기전에
AsyncPipe
는 구독과 구독해제를 자동으로 해줌.
옵저버블 리턴된건 데이터를 바로바로 담고있는게 아니라 말 그대로로 observable이고 데이터스트림을 의미함. 구독을 해야지 뭔가 되는거고 실제로 데이터가 올지안올지는 모르는거임. 그래서 거기다가 미리 데이터가 오면 동작할것들을 정의해두고 데이터가 실제로 와야지 그런 동작이 실행이됨.
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
});
}
서버가 추가적인 헤더를 요구하는 경우가 많음. 예를 들어서 auth token이나 Content-Type을 필요로하는 경우임.
아래 처럼 헤더 만들어줄 수 있음.
httpOptions = {
headers: new HttpHeaders({ 'Authorization': this.token })
};
HttpHeaders
클래스의 인스턴스는 immutable하니 직접적으로 값을 변경해줄 수 없음. 따라서 set()
메소드로 현재 인스턴스의 클론을 만들고 클론에다가 업데이트를 해서 사용하면됨.
아래처럼하면 토큰이 만료되었을때 새 토큰을 넣어줄 수 있음.
this.httpOptions.headers =
this.httpOptions.headers.set('Authorization', 'my-new-auth-token');
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 }
interceptors를 만들어서 앵귤러 앱에서 서버로 가는 HTTP request를 바꿔줄 수 있음. 같은 인터셉터로 서버에서 앱으로 오는 response도 받아서 변경가능함. 여러개의 인터셉터를 이어서 reqeust/reponse 핸들러 체인을 만들 수 있음.
인터셉터가 없으면 HttpClient
메소드를 사용할 때마다 일일이 바꿔주는 작업을 넣어줘야함.
인터셉터를 사용하기 위해서 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를 HttpEvents
의 Observable
로 바꿔주는데 얘는 궁극적으로 서버의 response를 포함하게됨. interceptor()
메소드는 해당 observable을 보고 내용을 바꿔줘서 호출한 곳으로 돌려줌.
next
객체는 인터셉터 체인에서 다음 인터셉터를 나타냄. 마지막에 나오는 next
는 HttpClient
백엔드 핸들러로 서버로 request를 보내고 response를 받아줌.
대부분의 인터셉터가 next.handle()
를 호출해서 request가 다음 인터셉터로 넘어가게 하고 결과적으로 백엔드 핸들러가 받아서 서버로 나가고 서버에서 들어오도록함. 인터셉터가 next.handle()
호출을 건너뛰고 자신만의 인공적인 서버 response인 Observable
을 만들어서 리턴할 수도 있음.
NoopInterceptor
은 앵귤러의 dependency injection(DI)이 관리하는 서비스임. 다른 서비스들과 같이 앱이 사용하기전에 인터셉터 클래스를 provide 해줘야함.
인터셉터가 HttpClient
서비스의 optional 한 dependency기 때문에 HttpClien
를 provide 해주는 인젝터에 provide 해줘야함(아니면 injector의 parent에다가 해야함). DI가 HttpClien
를 생성한 다음에 provide하는 인터셉터는 무시됨.
지금 계속 만드는 앱은 HttpClientModule
를 AppModule
에 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 배열에 넣어주면 됨.
앵귤러는 인터셉터를 provide 한 순서대로 인터셉터를 적용함. 얘를 들어서 HTTP request를 보내기전에 authenticatio 작업을 하고 그 다음에 request를 기록하고 서버로 request를 보내고 싶다고 할때, 해당 작업을 수행하려면 다음과 같이해야함. AuthInterceptor
service를 먼저 provide 하고 그 다음에 LoggingInterceptor
service를 provide 해야함. 나가는 request는 AuthInterceptor
를 거친후에 LoggingInterceptor
를 통과하고 나가게됨. response가 오면 반대로 LoggingInterceptor
를 거쳤다가 AuthInterceptor
를 통과하게 됨.
마지막 인터셉터는 언제나
HttpBacken
로 서버와의 통신을 담당함.
인터셉터 순서를 나중에 못바꿈. 변경하려면 인터셉터 안에다가 기능을 넣어줘야함.
대부분의 HttpClient
메소드는 HttpResponse<any>
인 observable을 리턴해줌. HttpResponse
는 자체가 HttpEventType.Response
타입인 이벤트임. 하나의 HTTP request가 다양한 타입을 가진 여러개의 이벤트를 생성할 수 있음.
많은 인터셉터는 나가는 request에 관련되어 있고 대부분 request를 수정하지 않고 next.handle()
에서 나오는 이벤트 스트림을 리턴해줌. 그렇지만 몇몇 인터셉는 next.handle()
에서 나오는 response를 보고 수정하기도 함.
인터셉터가 request랑 response를 수정가능하긴 하지만 HttpRequest
와 HttpResponse
의 인스턴스 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);
readonly
는 deep update를 방지 못하고, 특히 request body 객체의 property 변경을 막지 못함.
아래는 잘못된 예시임.
req.body.name = req.body.name.trim(); // bad idea!
따라서 request body를 수정하려면 아래의 단계를 밟아야함.
clone()
으로 클론함.// 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);
가끔 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
아래는 흔한 인터셉터 사용 예시임.
앱은 나가는 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 } });
헤더를 바꾸는 인터셉터는 다음과 같은 작업에 사용될 수 있음.
인터셉터가 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에게 리턴해줌.