bgyoonsContact

axios headers deep merge에 대해

FrontEnd | | 14분 읽기

axios 인스턴스를 만드는 부분 → lib/axios.js

/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 *
 * @returns {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig);
  const instance = bind(Axios.prototype.request, context);
 
  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});
 
  // Copy context to instance
  utils.extend(instance, context, null, {allOwnKeys: true});
 
  // Factory for creating new instances
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };
 
  return instance;
}

instance를 반환하고 있는데 이는

const instance = bind(Axios.prototype.request, context); // this 바인딩 해주고 있음

즉 instance는 Axios.prototype.request 자체라고 할 수 있음

= new Axios(defaultConfig)를 this로 여기는

instance.create = function create(instanceConfig) {
  return createInstance(mergeConfig(defaultConfig, instanceConfig));
};

instance.create 부분에서 instance 객체에 create라는 새로운 메소드를 직접 추가하는 것

우리는 이 instance를 갖고 다음과 같이 사용

const axiosClient = axios.create({
  baseURL,
  timeout: 5 * 1_000,
  headers: {
    'tenant-id': 1, // 커머스모듈에서 테넌트를 구분하기 위해 전달 ( cashdeal = 1 )
  },
});

따라서 초기에 create를 하여 Axios 인스턴스를 생성할 때 create에 전달한 instanceConfig와 axios 초기 config인 defaultConfig가 병합됨

// lib/core/Axios.js
 
class Axios {
  constructor(instanceConfig) {
    this.defaults = instanceConfig;
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    };
  }
 
  /**
   * Dispatch a request
   *
   * @param {String|Object} configOrUrl The config specific for this request (merged with this.defaults)
   * @param {?Object} config
   *
   * @returns {Promise} The Promise to be fulfilled
   */
  async request(configOrUrl, config) {
    try {
      return await this._request(configOrUrl, config);
    } catch (err) {
      if (err instanceof Error) {
        let dummy;
 
        Error.captureStackTrace ? Error.captureStackTrace(dummy = {}) : (dummy = new Error());
 
        // slice off the Error: ... line
        const stack = dummy.stack ? dummy.stack.replace(/^.+\n/, '') : '';
 
        if (!err.stack) {
          err.stack = stack;
          // match without the 2 top stack lines
        } else if (stack && !String(err.stack).endsWith(stack.replace(/^.+\n.+\n/, ''))) {
          err.stack += '\n' + stack
        }
      }
 
      throw err;
    }
  }
 
  _request(configOrUrl, config) {
    /*eslint no-param-reassign:0*/
    // Allow for axios('example/url'[, config]) a la fetch API
    if (typeof configOrUrl === 'string') {
      config = config || {};
      config.url = configOrUrl;
    } else {
      config = configOrUrl || {};
    }
 
    config = mergeConfig(this.defaults, config);
 
    const {transitional, paramsSerializer, headers} = config;
    ...
  }
}

this.defaults는 instanceConfig와 axios 초기 config인 defaultConfig가 병합된 config

request에서는 객체 혹은 문자열을 받고 있음

_request를 봤을 때 문자열을 받으면 config url에 병함

객체라면 config에 객체를 덮어씌움

config = mergeConfig(this.defaults, config); 이렇게 axios의 config를 합치고 있는데

//lib/core/mergeConfig.js
 
/**
 * Config-specific merge-function which creates a new config-object
 * by merging two configuration objects together.
 *
 * @param {Object} config1
 * @param {Object} config2
 *
 * @returns {Object} New object resulting from merging config2 to config1
 */
export default function mergeConfig(config1, config2) {
  // eslint-disable-next-line no-param-reassign
  config2 = config2 || {};
  const config = {};
 
  utils.forEach(Object.keys(Object.assign({}, config1, config2)), function computeConfigValue(prop) {
    const merge = mergeMap[prop] || mergeDeepProperties;
    const configValue = merge(config1[prop], config2[prop], prop);
    (utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue);
  });
 
  return config;
}

defaultConfig와 instanceConfig를 assign을 통해 합치고 key를 얻어내서 utils를 통해 headers 키를 prop로 받아와 mergeMap[prop](mergeMap[”headers”])를 찾음

forEach는 다음과 같음

/**
 * Iterate over an Array or an Object invoking a function for each item.
 *
 * If `obj` is an Array callback will be called passing
 * the value, index, and complete array for each item.
 *
 * If 'obj' is an Object callback will be called passing
 * the value, key, and complete object for each property.
 *
 * @param {Object|Array} obj The object to iterate
 * @param {Function} fn The callback to invoke for each item
 *
 * @param {Boolean} [allOwnKeys = false]
 * @returns {any}
 */
function forEach(obj, fn, {allOwnKeys = false} = {}) {
  if (isArray(obj)) {
    // Iterate over array values
    for (i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  }
}

call을 통해 받은 fn에 매개변수로 obj[i] 전달

const headersToObject = (thing) => thing instanceof AxiosHeaders ? { ...thing } : thing;
 
/**
 * Config-specific merge-function which creates a new config-object
 * by merging two configuration objects together.
 *
 * @param {Object} config1
 * @param {Object} config2
 *
 * @returns {Object} New object resulting from merging config2 to config1
 */
export default function mergeConfig(config1, config2) {
  // eslint-disable-next-line no-param-reassign
  config2 = config2 || {};
  const config = {};
 
  function getMergedValue(target, source, caseless) {
    if (utils.isPlainObject(target) && utils.isPlainObject(source)) {
      return utils.merge.call({caseless}, target, source);
    } else if (utils.isPlainObject(source)) {
      return utils.merge({}, source);
    } else if (utils.isArray(source)) {
      return source.slice();
    }
    return source;
  }
 
  // eslint-disable-next-line consistent-return
  function mergeDeepProperties(a, b, caseless) {
    if (!utils.isUndefined(b)) {
      return getMergedValue(a, b, caseless);
    } else if (!utils.isUndefined(a)) {
      return getMergedValue(undefined, a, caseless);
    }
  }
 
  const mergeMap = {
    headers: (a, b) => mergeDeepProperties(headersToObject(a), headersToObject(b), true)
  };
 
  utils.forEach(Object.keys(Object.assign({}, config1, config2)), function computeConfigValue(prop) {
    const merge = mergeMap[prop] || mergeDeepProperties;
    const configValue = merge(config1[prop], config2[prop], prop);
    (utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue);
  });
 
  return config;
}

이를 통해서 create할 때 생성한 config와 axiosClient를 호출할 때 매개변수로 전달해준 config가 병합

→ headers, params, timeout 등 설정은 key 기준으로 병합됨

→ 키가 겹치지 않으면 누적되고, 겹치면 우선순위는 config2 (즉, 요청 시 전달한 설정)

→ 두 config의 모든 키를 순회하면서 각 속성별로 적절한 병합 로직을 적용

참고자료

https://github.com/axios/axios/issues/2204

https://sangcho.tistory.com/entry/Axios-digging

https://www.timegambit.com/blog/digging/axios/01