평범한 개발자 행복한 가족, 패밀리그램

개발/React

axios + rxjs 를 이용한 API retry policy

패밀리그램 2021. 9. 26. 20:59

https://family-gram.tistory.com/603

 

axios 를 이용한 REST API 호출 및 취소

Project 에 axios dependencies 추가 yarn add axios 사용하는 모듈에서 바로 axios 를 사용하여 호출하는 것 보다는 특정 Service 모듈을 만들어 호출하는 것이 API error, retry 처리에 용이함 import axios, {..

family-gram.tistory.com

REST API 호출 및 취소이후 API retry policy 관련
기존 API 코드
import axios, { AxiosRequestConfig, CancelTokenSource } from "axios";

const defaultConfig: AxiosRequestConfig = {};

const axiosApi = axios.create(defaultConfig);

type Executor<T> = (
    resolve: (value: T | PromiseLike<T>) => void,
    reject: (reason?: any) => void
) => void;

export class ApiPromise<T> extends Promise<T> {
    private $source: CancelTokenSource;
    constructor(executor: Executor<T>, cancelSource: CancelTokenSource) {
        super(executor);
        this.$source = cancelSource;
    }

    public cancel(msg?: string) {
        this.$source.cancel(msg);
    }

    get source(): CancelTokenSource {
        return this.$source;
    }
}

class Service {
    get(url: string, params?: { [key: string]: string | number }): ApiPromise<any> {
        const cancelSource = axios.CancelToken.source();
        const api = axiosApi.get(url, {
            params: params,
            cancelToken: cancelSource.token,
        });
        return new ApiPromise((resolve, reject) => {
            api.then((resp) => {
                resolve(resp);
            }).catch((error) => {
                reject(error);
            });
        }, cancelSource);
    }
}
yarn add rxjs

https://rxjs.dev/

 

RxJS

 

rxjs.dev

Rx ( ReactiveX ) 는 RxJs, RxJava, Rx... 많은 언어에 해당하는 라이브러리가 있다. 사용언어에 따라 표현법은 조금씩 다르나 개념은 동일하니 알아두면 다른언어에서도 사용가능하다 

Javascript 경우에는 UI에서 Background thread pool 이 따로 없기 때문에 subscriptionOn 이나 observeOn 같은 API 가 없는듯 하다

코드 업데이트
import axios, { AxiosRequestConfig, CancelTokenSource } from "axios";
// 추가
import { concatMap, delay, mergeMap, of, retryWhen, take, tap, throwError } from "rxjs";

const defaultConfig: AxiosRequestConfig = {};

const axiosApi = axios.create(defaultConfig);

// 추가
const RETRY_COUNT = 2;
const onGlobalError = (error: any) => {
    console.debug("ERROR !", error);
};

type Executor<T> = (
    resolve: (value: T | PromiseLike<T>) => void,
    reject: (reason?: any) => void
) => void;

export class ApiPromise<T> extends Promise<T> {
    private $source: CancelTokenSource;
    constructor(executor: Executor<T>, cancelSource: CancelTokenSource) {
        super(executor);
        this.$source = cancelSource;
    }

    public cancel(msg?: string) {
        this.$source.cancel(msg || "Cancel");
    }

    get source(): CancelTokenSource {
        return this.$source;
    }
}

class Service {
    get(url: string, params?: { [key: string]: string | number }): ApiPromise<any> {
        const cancelSource = axios.CancelToken.source();
        const data = {
            url: url,
            data: {
                params: params,
                cancelToken: cancelSource.token,
            },
        };
        return new ApiPromise((resolve, reject) => {
            const onSuccesed = (resp: any) => {
                resolve(resp);
            };

            const onError = (error: any) => {
                reject(error);
            };
            of(data)
                .pipe(
                    concatMap((e) => axiosApi.get(e.url, e.data)),
                    tap({
                        error: onGlobalError,
                    }),
                    retryWhen((errors) =>
                        errors.pipe(
                            mergeMap((error) => {
                                const { response } = error;
                                if (response && response.status) {
                                    const { status } = response;
                                    if (status >= 500) {
                                        return of(status).pipe(delay(1000));
                                    }
                                }
                                return throwError(() => error);
                            }),
                            take(RETRY_COUNT)
                        )
                    )
                )
                .subscribe({
                    next: onSuccesed,
                    error: onError,
                });
        }, cancelSource);
    }
}
API 요청 실패 시, status를 확인하여 재요청 여부를 체크
            const onSuccesed = (resp: any) => {
                resolve(resp);
            };

            const onError = (error: any) => {
                reject(error);
            };
             of(data)
                .pipe(
                    concatMap((e) => axiosApi.get(e.url, e.data)),
                    tap({
                        error: onGlobalError,
                    }),
                    retryWhen((errors) =>
                        errors.pipe(
                            mergeMap((error) => {
                                const { response } = error;
                                if (response && response.status) {
                                    const { status } = response;
                                    if (status >= 500) {
                                        return of(status).pipe(delay(1000));
                                    }
                                }
                                return throwError(() => error);
                            }),
                            take(RETRY_COUNT)
                        )
                    )
                )
                .subscribe({
                    next: onSuccesed,
                    error: onError,
                });

최초 응답이 실패했을 경우 retryWhen 에서 response 의 status 를 확인하여 재요청 여부를 체크한다. 500대 이상 에러의 경우는 재요청.

재요청 상황

기존 promise 인 api 를 RxJs의 of을 이용하여 Observable 형태로 변환한다.

pipe - tap 을 통해 요청하는 모든 에러에 대해서 onGlobalError function 으로 체크한다.

pipe - retryWhen retry 조건을 파악하여 요청이 필요한 경우 delay 형태의 Observable 을 반환, take를 이용하여 재요청 카운트 확인

에러요청을 체크하는 onGlobalError 는 retryWhen 이전에 정의되어야함

 

반응형