Skip to content

axios 호출 위치 추적

@platform/vue-debugger는 네트워크 요청마다 이 요청을 보낸 개발자 코드 위치(호출 스택) 를 네트워크 패널에 표시합니다. 그런데 axios를 쓰면, onMounted 깊은 곳에서 시작된 호출 체인이 마지막에 axios를 부르더라도 — 정작 네트워크 탭엔 axios 내부 프레임만 잡히고 그 위 호출자(onMounted부터의 체인)는 끊겨 보이지 않습니다. 이 페이지는 그 끊긴 체인을 되살리는 한 줄짜리 옵트인 traceAxios를 설명합니다.

선택 기능입니다. axios를 쓰지 않거나 호출 위치 복원이 필요 없다면 건너뛰어도 됩니다. 기본 연동은 앱에 연동을 보세요.

왜 axios만 끊기나

디버거는 네트워크 요청이 실제로 나가는 순간(XHR send / fetch)에 new Error().stack으로 호출 스택을 캡처합니다. 직접 fetch()를 부르면 그 시점 스택에 호출자가 그대로 남아 복원됩니다. 그런데 axios는 내부적으로 요청을 Promise.then(dispatchRequest) 체인 + new Promise executor로 흘려보냅니다. 즉 실제 전송은 원래 호출 자리에서 비동기 경계를 한 번 건넌 뒤에 일어나, 그 시점엔 fetchMenu → loadBaseData → onMounted 같은 호출 체인이 이미 이벤트루프 경계 너머로 사라져 있습니다.

text
onMounted → loadBaseData → fetchMenu → axios.get('/menu')
                                          └─ 내부 .then(dispatchRequest) ─┐  ← 여기서 체인이 끊김
                                                       (비동기 경계)        │
                                          ┌──────────────────────────────┘
                                          └─ xhr.send()  ← 디버거가 캡처하는 시점: 호출자 이미 소멸

끊긴 동기 콜스택은 어떤 엔진·APM 벤더도 사후 복원하지 못합니다(콜스택 복원의 한계 참고). 방법은 하나 — 호출되는 그 순간(체인이 아직 살아 있을 때) 스택을 잡아두는 것입니다. traceAxios가 그 일을 합니다.

정확히 언제 끊기나 — 요청 인터셉터가 조건입니다

axios가 위처럼 비동기 경로로 가는 건 요청 인터셉터가 (기본값으로) 하나라도 있을 때입니다. axios는 모든 요청 인터셉터가 { synchronous: true }가 아니면 요청을 Promise.resolve().then(dispatchRequest)로 보냅니다 — 그래서 전송 시점엔 체인이 끊깁니다. 인터셉터가 0개면 axios는 동기 어댑터 경로(new Promise executor가 동기 실행 → xhr.send도 동기)라 체인이 안 끊겨, traceAxios 없이도 호출 위치가 보입니다. 즉 인증 토큰 주입 같은 요청 인터셉터를 둔 실제 앱에서 "axios 마지막 줄만 잡힘" 증상이 나고, 바로 그때 traceAxios가 필요합니다. (사용자의 호출 체인이 동기든 async/await든 상관없습니다 — 끊는 주체는 axios의 내부 경계이고, 호출 시점 캡처는 V8 async stitching으로 await 체인도 이어 붙입니다.)

한 줄 — traceAxios(instance)

axios 인스턴스를 만든 직후, 그 인스턴스에 traceAxios를 한 번 적용하세요. 보통 인스턴스를 만드는 api 클라이언트 모듈이 가장 자연스러운 자리입니다.

ts
// api/client.ts
import axios from 'axios'
import { traceAxios } from '@platform/vue-debugger' // 정적 import (프로덕션선 트리셰이킹됨)

export const apiClient = axios.create({ baseURL: '/api', timeout: 10_000 })

if (import.meta.env.DEV || (import.meta.env.MODE === 'staging' && import.meta.env.VITE_DEBUGGER === 'on')) {
  traceAxios(apiClient) // ← 이제 이 인스턴스의 모든 요청에 onMounted부터의 호출 체인이 복원된다
}

이게 전부입니다. 이후 apiClient.get()/.post() 등으로 나가는 요청은, 네트워크 패널의 '이 요청을 보낸 코드 위치'fetchMenu ← loadBaseData ← bootSequence 전체 체인이 표시되고, 요약 옆에 '· axios 호출 위치에서 복원(traceAxios)' 라고 출처가 정직하게 붙습니다.

인스턴스마다 한 번씩

앱에 axios 인스턴스가 여럿이면(예: 인증용·결제용) 각각에 traceAxios를 부르세요. 추적할 인스턴스만 켜면 됩니다. 멱등이라 같은 인스턴스에 두 번 불러도 안전합니다(HMR 포함).

webpack / Vue CLI

import.meta.env가 없는 webpack·Vue CLI에서는 process.env.NODE_ENV !== 'production' || (__VDBG_STAGING__ && process.env.VUE_APP_DEBUGGER === 'on')로 대체하세요 — VUE_APP_DEBUGGERNODE_ENV 체크에 OR로만 붙이면 안 되고, 빌드 스크립트에 묶인 상수(__VDBG_STAGING__)와 AND로 결합해야 합니다(연동의 webpack 예시 참고). DefinePlugin이 정적 치환해 동일한 dead-code 제거 효과를 줍니다.

앱 동작에 영향이 없습니다 (검증됨)

traceAxios는 요청을 감싸기만 할 뿐, 동작·전송 내용을 그대로 보존합니다. 이 점은 구현 전에 실측으로 먼저 검증했습니다 — 응답·에러·취소·병렬 요청 결과가 래핑 전후 동일하고, 서버가 받는 요청 바이트(메서드·URL·헤더·본문)가 한 글자도 다르지 않았습니다.

  • 순수 pass-through — 래퍼는 new Error().stack을 읽는 것 외에 아무 일도 하지 않습니다. 인자·this·반환 프라미스를 일절 건드리지 않고 원본을 그대로 호출합니다.
  • 요청에 헤더를 주입하지 않습니다 — 호출 위치와 네트워크 항목의 연결은 헤더가 아니라 method+URL+시간창 매칭으로 합니다. 따라서 요청 서명(HMAC 등)을 쓰는 금융 API도 안전합니다(헤더를 더하면 서명이 깨질 수 있어 일부러 배제).
  • 켜지 않으면 0 비용traceAxios를 부르지 않으면 내부 버퍼가 비어, 네트워크 경로의 매칭 조회가 즉시 반환합니다. 추적을 켠 인스턴스에서만 호출당 스택 1회(약 12µs, 저빈도)를 더 씁니다.
  • 원복 가능untraceAxios(instance) 또는 traceAxios가 돌려주는 함수로 언제든 원래 메서드로 되돌립니다.
ts
const untrace = traceAxios(apiClient)
// …
untrace() // 또는 untraceAxios(apiClient)

왜 인터셉터가 아니라 메서드 래핑인가

axios에 익숙하면 "인터셉터로 하면 되지 않나?" 싶지만, 두 가지 이유로 인터셉터로는 안 됩니다.

  1. 인터셉터는 기본 비동기입니다. 요청 인터셉터는 axios 내부의 .then 체인 안에서 실행돼, 그 시점에도 이미 호출자 체인이 끊겨 있습니다. 호출 위치를 잡으려면 사용자가 .get()부르는 동기 순간에 잡아야 합니다.
  2. 인스턴스는 bind된 함수입니다. inst.get이 내부적으로 부르는 this.request는 프로토타입의 원본이라, request 하나만 감싸면 get/post가 우리 래퍼를 거치지 않습니다. 그래서 traceAxios사용자가 직접 부르는 메서드 각각(get·post·put·delete·patch·head·options·request)을 감쌉니다.
직접 대조해 보기 — 8443 플레이그라운드

/#/lab/callstack'🔗 axios 호출 위치 복원' 카드에서 같은 호출 체인을 추적 OFF(별도 인스턴스)추적 ON(traceAxios 적용 인스턴스) 으로 각각 보내고, 네트워크 탭에서 두 요청의 '호출 위치'를 비교할 수 있습니다. OFF는 axios 내부 프레임만, ON은 전체 체인 + 출처 표기가 보입니다.

콜스택이 아니라 맥락이 필요할 때

traceAxiosaxios가 끊은 동기 호출 체인을 되살립니다. 반면 setTimeout·DOM 이벤트·.then 같은 앱 코드 자체의 비동기 경계로 끊긴 경우는, 호출 스택을 복원하는 대신 '무엇이 이 요청을 이끌었나'(직전 클릭·네비게이션) 를 breadcrumb로 보여줍니다. 둘은 보완 관계입니다 — 자세한 구분은 패널 레퍼런스 · 네트워크와 플레이그라운드 /#/lab/callstack을 보세요.

정리

  • axios는 내부 비동기 경계 때문에 전송 시점엔 호출자 체인이 끊깁니다 — 네트워크 탭에 axios 내부 프레임만 보이는 이유입니다.
  • 인스턴스 생성 직후 traceAxios(instance) 한 줄(가드 분기 안)이면 호출 시점에 체인을 잡아 복원합니다.
  • 동작 무영향(검증됨) — 순수 pass-through, 요청에 헤더 무주입(서명 API 안전), 안 켜면 0 비용, untraceAxios로 원복.
  • 정적 import + 정적 분기 + sideEffects:false프로덕션 누출은 0입니다.

다음으로, 이 분기가 환경마다 어떻게 켜지고 꺼지는지는 환경별 설정에서, 설치가 실제로 동작하는지 확인하는 절차는 설치 검증에서 이어집니다.

개발/검증 환경 전용 인페이지 Vue 디버거 · 진짜 프로덕션엔 코드가 들어가지 않습니다