본문 바로가기

FrontEnd/react

Axios Wrapping

현재 프로젝트에는 전역적으로 사용하는 axios 인스턴스를 만들고, 해당 인스턴스에 request/ response 의 인터셉터를 추가한 뒤, api 호출 부분을 요청 별 함수를 만들어 export 하도록 구성되어 있다.

현재 api 호출에 대한 구성 파일 중 일부

현재로서는 개발할 때 큰 문제점은 없었기 때문에 이런 형태의 구성으로 개발은 끝마쳤지만 의문점이 두가지 생겼다. 

1. axios 인스턴스는 꼭 전역으로 애플리케이션 전체 생명주기 동안 유지되어야만 하는가? 

2. 도메인별로 인터셉터 로직이 달라져야한다면 또 다른 전역적으로 유지되는 axios 인스턴스를 만들어야하는가? 

 

이 두가지 의문점을 개선해보기 위해 axios 인스턴스를 감싸는 클래스를 만들어보기로 했다. 

1. 클래스로 instance를 감싸고 생성자 함수에서 인스턴스를 create 한다. 

2. 공통적으로 사용되는 interceptor 로직을 부모 클래스에서 선언하고 각 도메인에서 추가적으로 필요한 interceptor 로직은 추상메소드로 자식 클래스에서 직접 구현하도록 만든다. 

3. 각 도메인별 자식클래스들을 api 요청이 필요한 곳에서 해당 클래스의 인스턴스를 생성해서 사용한다. 

 

개선한 api 요청 클래스 구조

실제 코드는 다음과 같다 

 

BaseHttpClient.ts

/* eslint-disable no-param-reassign */
/* eslint-disable class-methods-use-this */
import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { config } from 'config'
import { camelizeKeys, decamelizeKeys } from 'humps'
import { history } from 'index'
import LOCATIONS from 'utils/constants/locations'

export abstract class BaseHttpClient {
  protected readonly instance: AxiosInstance

  public constructor() {
    if (!config.API_HOST) throw new Error('environment valuable not set : API_HOST')
    this.instance = axios.create({ baseURL: config.API_HOST })
    this.initializeResponseInterceptor()
    this.initializeRequestInterceptor()
  }

  private initializeRequestInterceptor = () => {
    this.instance.interceptors.request.use(this.handleRequestFulfilled)
  }

  private initializeResponseInterceptor = () => {
    this.instance.interceptors.response.use(this.handleResponseFulfilled, this.handleResponseRejected)
  }

  private handleResponseFulfilled = ({ data }: AxiosResponse) => camelizeKeys(data)

  private handleResponseRejected = (error: AxiosError) => {
	(...)
    return Promise.reject(error)
  }

  protected handleRequestFulfilled = (axiosRequestConfig: AxiosRequestConfig) => {
    const requestConfig = { ...axiosRequestConfig }
    (...)
    return requestConfig
  }

  /**
   * sub class 에서 extraRequestInterceptor 를 구현했을 때
   * axios instance 에 extraRequestInterceptor 를 할당해주는 method
   * extraRequestInterceptor를 추가할 경우 sub class 의 생성자에서 호출
   */
  protected addRequestInterceptor = () => {
    this.instance.interceptors.request.use(this.extraRequestInterceptor)
  }

  /**
   * 추가 request interceptor 정의
   */
  protected abstract extraRequestInterceptor(axiosRequestConfig: AxiosRequestConfig): AxiosRequestConfig
}

AuthorizedHttpClient

/* eslint-disable class-methods-use-this */
import { AxiosRequestConfig } from 'axios'
import { AuthStorage } from 'services/storages'
import { BaseHttpClient } from './BaseHttpClient'

export class AuthorizedHttpClient extends BaseHttpClient {
  public constructor() {
    super()
    // BaseHttpClient 클래스에 protected 로 정의된 addRequestInterceptor를 호출하여 
    // 이 클래스에서 정의한 extraRequestInterceptor를 추가 interceptor로 등록한다. 
    this.addRequestInterceptor()
  }

  protected extraRequestInterceptor(axiosRequestConfig: AxiosRequestConfig<unknown>): AxiosRequestConfig<unknown> {
    const requestConfig = { ...axiosRequestConfig, headers: axiosRequestConfig.headers || {} }
    requestConfig.headers.Authorization = `Bearer ${AuthStorage.get()}`
    return requestConfig
  }
}

ContentHttpClient

import { ListRequestParams } from 'apis/apis'
import { AuthorizedHttpClient } from 'apis/base'
import { GetContentOverviews } from 'types/contents'

export class ContentHttpClient extends AuthorizedHttpClient {
  private END_POINT = '/contents' as const

  public getContentOverviews: GetContentOverviews = params => this.instance.get(this.END_POINT, { params })
}

 

사용 예시 

const contentClient = new ContentHttpClient()
const res = await contentClient.getContentOverviews(listRequestParam)
  • 이 같은 구조를 사용하면 해당 클래스를 만든 스코프 안에서만 axios 인스턴스가 생성되고 사라진다. 그리고 각 도메인별로 필요한 interceptor를 쉽게 재정의 할 수 있다.
  • 다만 필요할 때마다 axios 인스턴스를 새로 만드는 방식이 더 올바른 방식인지, 그냥 전역적으로 axios 인스턴스를 만들어놓고 사용하는 방식이 올바른 방식인지에 대한 의문이 남아있다.