HttpClient
클라이언트에서 서버와 통신하기 위해선 보통 기본적으로 사용가능한 fetch
나 axios
와 같은 라이브러리를 설치하여 사용하게 되는데 앵귤러에서는 HttpClient
라는 모듈을 제공하고 있다. 이 모듈을 사용하려면 우선 앱 모듈에서 임포트를 해주어야 한다.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http'; // +
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
HttpClientModule // +
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
이 모듈은 RxJS를 기반으로하는데, 필자는 RxJS를 처음 접해봤으므로 관련된 내용도 작게나마 기록할 예정이다. (할게... 많네...)
Observable
서비스를 만들 때 아래와 같이 선언하여 통신을 진행하도록 만들 수 있다.
import { HttpClient } from '@angular/common/http'; // +
import { Injectable } from '@angular/core';
import { catchError, retry } from 'rxjs';
interface MessageResponse {
message: string;
}
@Injectable({
providedIn: 'root'
})
export class MyService {
constructor(private http: HttpClient) { } // +
fetchMessage() {
return this.http.get<MessageResponse>('/api/message')
}
}
신기하게도 위 함수는 즉시 실행되는 형태가 아닌 Observable
이라는 타입으로 반환되는데 사용하는 측에서 subscribe
메서드를 호출하는 시점에 실행된다.
export class AppComponent {
title = 'http-client-test';
constructor(private myService: MyService) {
const observableFetchMessage = this.myService.fetchMessage() // 이때는 실행되지 않음
observableFetchMessage.subscribe((res) => { // 이때 실행됨
console.log(res);
})
}
}
지금 사용하는 API에서는 아래와 같은 값을 반환하는데
{
message: "Hello, World!"
}
현재 console.log
에 찍히는 값은 API의 반환 값과 동일하다. 기본적으로 body
와 json
타입으로 반환하는 옵션이 기본이기 때문이다. 만약 body
내용뿐 아니라 header
의 내용도 받으려면 2번째 매개변수에 옵션 값을 넘겨주면 된다.
fetchMessage() {
return this.http.get<MessageResponse>('/api/message', {
observe: 'response',
})
}
그러면 console.log에 찍히는 값은 다음과 같다.
{
"headers": {
"normalizedNames": {},
"lazyUpdate": null
},
"status": 200,
"statusText": "OK",
"url": "http://localhost:4200/api/message",
"ok": true,
"type": 4,
"body": {
"message": "Hello, World!"
}
}
옵션에 observe 값을 어떻게 넣는지와 상관없이 get의 재내릭 타입은 body의 타입만 지정해주면 되므로 아래와 같이 타이핑해주면 된다.
this.http.get<MessageResponse>('/api/message', {
observe: 'response',
});
다른 옵션의 타입 정의는 다음과 같이 되어있다.
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,
}
post
, put
, delete
요청은 두번째 인자에 데이터를 받는다.
return this.http.post<MessageResponse>('/api/message', data, {
observe: 'response',
})
Pipe
Pipe
는 RxJS
에서 제공하는 기능으로 Observable
을 조작 및 변환하는데 사용된다. 가령 API 요청이 실패했을 때 재시도를 하려면 아래와 같이 pipe를 작성할 수 있다.
this.http.get<MessageResponse>('/api/message', {
observe: 'response',
}).pipe(
retry(3)
);
pipe
는 ...args
형태로 함수를 인자로 받는다.
this.http.get<MessagesResponse>('/api/messages', {
observe: 'response',
}).pipe(
retry(3),
filter(x => x.message.includes('Hello')),
map(x => x.message),
catchError((err) => {
if (err.status === 0) {
toast('네트워크 연결 상태를 확인해주세요,')
} else {
toast('요청중 오류가 발생했습니다.')
}
throwError(() => new Error())
})
);
Interceptor
Http 통신의 요청과 응답을 핸들링 할 수 있는 기능도 제공하고 있다. 다음은 각각 요청에 대한 인터셉터, 응답에 대한 인터셉터, 요청과 응답에 대해서 캐싱하는 인터셉터를 구현하는 예시이며 제일 하단에 인터셉터를 provider에 등록하는 방법을 작성하였다.
Request Interceptor
인터셉터를 구현하는 공통적인 방법은 HttpInterceptor
라는 인터페이스를 이용해서 intercept
라는 메서드를 구현해주면 된다. 요청 인터셉터는 요청에 대한 편집을 진행한 후 next.handle
이라는 객체의 메서드를 활용해서 다음 절차를 이어서 진행하도록 할 수 있다. (다음 인터셉터가 등록되어 있으면 다음 인터셉터로 전달된다.)
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable()
export class ReqInterceptor implements HttpInterceptor {
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const modifiedRequest = request.clone({
headers: request.headers.set('Authorization', 'Bearer my-auth-token')
});
console.log('Request sent to server:', modifiedRequest);
return next.handle(modifiedRequest);
}
}
Response Interceptor
응답 인터셉터는 next.handle
메서드에 pipe
메서드를 이용해서 event.type
이 응답이라는 것을 확인하여 판별할 수 있다.
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest, HttpEvent, HttpEventType } from '@angular/common/http';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class ResInterceptor implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
tap(event => {
if (event.type === HttpEventType.Response) {
console.log('Response received:', event);
}
})
);
}
}
Cache Interceptor
다음은 인터셉터에서 요청의 키 값을 확인해서 해당 키가 메모리에 캐시되어 있으면 해당 키에 대한 값을 우선 응답한 뒤 실제 요청을 보내서 해당 결과를 다시 반환하도록 하는 인터셉터이다. (React Query에서 사용되는 방식) 키 값을 서버에 전달되는 헤더로 활용해서 바람직한 코드라고 보기엔 어렵겠지만... 공부 용도로 만들어 보았다. (헤더에서 지워버리면 되겠지만, 뭔가 찜찜하다.)
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpEventType } from '@angular/common/http';
import { Observable, Observer, tap } from 'rxjs';
@Injectable()
export class CacheInterceptor implements HttpInterceptor {
private cache: Map<string, unknown> = new Map();
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<any> {
const key = request.headers.get('client-cache-key')
const cacheSet = () => tap((event: HttpEvent<unknown>) => {
if (event.type === HttpEventType.Response && key) {
this.cache.set(key, event);
}
});
if (key) {
const cachedData = this.cache.get(key);
if (cachedData) {
return new Observable((observer: Observer<unknown>) => {
observer.next(cachedData),
next.handle(request).pipe(
cacheSet()
).subscribe(observer);
});
}
return next.handle(request).pipe(
cacheSet()
);
}
return next.handle(request);
}
}
How to use
...
providers: [
{ provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ReqInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ResInterceptor, multi: true }
],
....
Ghost