# Angular :: HttpClient

- Author: @baealex
- Published: 2023-10-13
- Updated: 2023-11-17
- Source: http://blex.me/@baealex/2023-10-13-til-angular-httpclient
- Tags: 앵귤러

---

## HttpClient

클라이언트에서 서버와 통신하기 위해선 보통 기본적으로 사용가능한 `fetch`나 `axios`와 같은 라이브러리를 설치하여 사용하게 되는데 앵귤러에서는 `HttpClient`라는 모듈을 제공하고 있다. 이 모듈을 사용하려면 우선 앱 모듈에서 임포트를 해주어야 한다.

```ts
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를 처음 접해봤으므로 관련된 내용도 작게나마 기록할 예정이다. ~~(할게... 많네...)~~

<br>

#### Observable

서비스를 만들 때 아래와 같이 선언하여 통신을 진행하도록 만들 수 있다.

```ts
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` 메서드를 호출하는 시점에 실행된다.

```ts
export class AppComponent {
    title = 'http-client-test';

    constructor(private myService: MyService) {
        const observableFetchMessage = this.myService.fetchMessage() // 이때는 실행되지 않음
        observableFetchMessage.subscribe((res) => { // 이때 실행됨
            console.log(res);
        })
    }
}
```

지금 사용하는 API에서는 아래와 같은 값을 반환하는데

```ts
{
    message: "Hello, World!"
}
```

현재 `console.log`에 찍히는 값은 API의 반환 값과 동일하다. 기본적으로 `body`와 `json` 타입으로 반환하는 옵션이 기본이기 때문이다. 만약 `body` 내용뿐 아니라 `header`의 내용도 받으려면 2번째 매개변수에 옵션 값을 넘겨주면 된다.

```ts
fetchMessage() {
    return this.http.get<MessageResponse>('/api/message', {
        observe: 'response',
    })
}
```

그러면 console.log에 찍히는 값은 다음과 같다.

```ts
{
    "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의 타입만 지정해주면 되므로 아래와 같이 타이핑해주면 된다.

```ts
this.http.get<MessageResponse>('/api/message', {
    observe: 'response',
});
```

다른 옵션의 타입 정의는 다음과 같이 되어있다.

```ts
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` 요청은 두번째 인자에 데이터를 받는다.

```ts
return this.http.post<MessageResponse>('/api/message', data, {
    observe: 'response',
})
```

<br>

#### Pipe

`Pipe`는 `RxJS`에서 제공하는 기능으로 `Observable`을 조작 및 변환하는데 사용된다. 가령 API 요청이 실패했을 때 재시도를 하려면 아래와 같이 pipe를 작성할 수 있다.

```ts
this.http.get<MessageResponse>('/api/message', {
    observe: 'response',
}).pipe(
    retry(3)
);
```

`pipe`는 `...args` 형태로 함수를 인자로 받는다.

```ts
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())
    })
);
```

<br>

#### Interceptor

Http 통신의 요청과 응답을 핸들링 할 수 있는 기능도 제공하고 있다. 다음은 각각 요청에 대한 인터셉터, 응답에 대한 인터셉터, 요청과 응답에 대해서 캐싱하는 인터셉터를 구현하는 예시이며 제일 하단에 인터셉터를 provider에 등록하는 방법을 작성하였다.

###### Request Interceptor

인터셉터를 구현하는 공통적인 방법은 `HttpInterceptor`라는 인터페이스를 이용해서 `intercept`라는 메서드를 구현해주면 된다. 요청 인터셉터는 요청에 대한 편집을 진행한 후 `next.handle`이라는 객체의 메서드를 활용해서 다음 절차를 이어서 진행하도록 할 수 있다. (다음 인터셉터가 등록되어 있으면 다음 인터셉터로 전달된다.)

```ts
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`이 응답이라는 것을 확인하여 판별할 수 있다.

```ts
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에서 사용되는 방식) 키 값을 서버에 전달되는 헤더로 활용해서 바람직한 코드라고 보기엔 어렵겠지만... 공부 용도로 만들어 보았다. (헤더에서 지워버리면 되겠지만, 뭔가 찜찜하다.)

```ts
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

```ts
...
providers: [
    { provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: ReqInterceptor, multi: true },
    { provide: HTTP_INTERCEPTORS, useClass: ResInterceptor, multi: true }
],
....
```
