다크 모드
전체 예시 — 실제 앱에 포팅하기
빠른 시작은 로컬에서 ⚙ 버튼을 띄우는 최소 경로(설치 + main.ts 몇 줄)였습니다. 하지만 실제 앱, 특히 배포(검증계) 빌드에서도 디버거를 제대로 쓰려면 main.ts 외에 vite.config·라우터·.env 등 파일 몇 개를 더 건드립니다.
이 페이지는 전형적인 Vue 3 앱(Vite + TypeScript + Pinia + vue-router + axios)에 디버거를 처음부터 끝까지 붙이는 전 과정을, 건드리는 파일 전부와 함께 순서대로 보여줍니다. 각 파일이 왜 필요한지 표로 먼저 정리하고, 그다음 각 파일의 전체 예시를 차례로 제시합니다.
0. 출발점 — 예시 앱 구조
아래는 디버거를 붙이기 전의, 흔한 Vue 3 앱입니다. 주문 관리 화면이 있고 Pinia로 인증 상태를, vue-router로 라우팅을, axios로 API를 호출합니다.
shop-web/
├─ package.json
├─ vite.config.ts
├─ index.html
└─ src/
├─ main.ts ← 부트스트랩
├─ App.vue
├─ router/
│ ├─ index.ts ← 라우트 + 전역 가드
│ └─ guards.ts ← authGuard
├─ stores/
│ └─ auth.ts ← Pinia 스토어
├─ api/
│ └─ client.ts ← axios 인스턴스
└─ pages/
├─ Home.vue · Orders.vue · Admin.vue1. 무엇을 건드리나 — 파일 체크리스트
핵심부터
로컬(vite dev)에서만 쓸 거라면 main.ts 한 파일이면 끝납니다. 아래 표의 나머지(vite.config·.env.staging·router)는 ① 배포(검증계) 빌드에 디버거를 넣을 때 또는 ② 전역 라우터 가드를 추적할 때만 필요합니다.
| 파일 | 변경 | 왜 필요한가 | 언제 필요 |
|---|---|---|---|
package.json | devDependency + 스크립트 추가 | 디버거 설치(-D) + 검증계 빌드 명령(build:staging) | 항상 |
src/main.ts | async 부트스트랩 + 조건부 attachDebugger | 부착 진입점. 디버거를 앱에 붙이는 단 한 곳 | 항상(핵심) |
vite.config.ts | mode==='staging' 분기로 __VUE_PROD_DEVTOOLS__ + sourcemap:'hidden' | 배포 빌드에서 컴포넌트 introspection(값·출처칩·식평가)과 스택 원본 복원이 동작 | 검증계 배포 시 |
.env.staging | VITE_DEBUGGER=on | 이 모드 빌드에만 디버거를 포함시키는 스위치 | 검증계 배포 시 |
src/router/index.ts | traceGuards(router) 한 줄(정적 import) | attach 이전에 등록되는 전역 beforeEach/beforeResolve를 Router 패널에 귀속 | 전역 가드를 추적할 때만 |
건드리지 않는 것 — 컴포넌트·스토어·axios·서비스
앱 코드는 한 줄도 바꾸지 않습니다. 디버거는 Pinia 스토어·라우터·axios를 import하지 않고 런타임에 발견하고, 네트워크는 XHR/fetch를 바닥에서 잡습니다. 그래서 stores/·pages/·api/는 그대로 둬도 디버거가 알아서 관찰합니다(격리·호환·한계 참고).
2. 파일별 전체 예시 (순서대로)
아래 순서대로 따라가면 됩니다. ①설치 → ②package.json → ③main.ts → ④router → ⑤vite.config → ⑥.env.staging.
2-1. 설치
bash
npm install -D @platform/vue-debuggervue는 peerDependency라 호스트 앱의 vue를 그대로 씁니다(중복 설치 없음). 사내 레지스트리·타르볼·폐쇄망 설치는 설치와 폐쇄망 반입을 보세요.
2-2. package.json — 의존성 + 빌드 스크립트
build(프로덕션, 디버거 0)와 build:staging(검증계, 디버거 포함)을 나눕니다.
json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"build:staging": "vite build --mode staging",
"preview": "vite preview"
},
"devDependencies": {
"@platform/vue-debugger": "^0.17.4"
}
}2-3. src/main.ts — 부착 진입점 (전체)
app.use(pinia)·app.use(router) 뒤, app.mount() 전에 붙입니다. 부트스트랩 전체를 async 함수로 감싸 디버거를 동적 import()로 지연 로드합니다.
ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import { router } from './router'
async function bootstrap() {
const app = createApp(App)
// ① 상태·라우팅을 먼저 등록 — 그래야 디버거가 스토어·라우터를 발견한다
app.use(createPinia())
app.use(router)
// ② 디버거: 로컬(vite dev) 또는 VITE_DEBUGGER=on 검증계 빌드에서만.
// 프로덕션 빌드에선 두 조건이 '정적 false' → 이 블록과 동적 import가 통째 제거된다(디버거 0).
if (import.meta.env.DEV || (import.meta.env.MODE === 'staging' && import.meta.env.VITE_DEBUGGER === 'on')) {
const { attachDebugger } = await import('@platform/vue-debugger')
attachDebugger(app, {
// 옵션은 모두 선택. 아래는 자주 쓰는 예시(전체는 옵션 레퍼런스).
ignoreUrls: ['/heartbeat', /datadog|sentry/], // APM 비콘 노이즈 제외
report: {
endpoint: '/internal/reports',
allowHosts: ['reports.corp.example'], // 전송 허용 호스트(필수)
},
})
}
// ③ 마운트는 디버거 부착 '뒤'에
app.mount('#app')
}
bootstrap()top-level await는 금지
await import()를 모듈 최상단에 직접 쓰지 마세요. vite dev(esbuild)에선 되지만 vite build 기본 타깃(es2020)이 top-level await를 거부해 검증계 빌드가 깨집니다. 반드시 위처럼 async function으로 감싸세요.
옵션 전체(bridges·maxEntries·hotkey·ignoreStorageKeys·report.redact…)는 설정 옵션에 있습니다. attachDebugger는 { version, open(), close(), toggle(), destroy() } 핸들을 반환합니다(비브라우저 환경에선 null).
2-4. src/router/index.ts — 전역 가드 추적 (선택)
/admin처럼 **라우트별 beforeEnter**가 막는 경우는 설정이 필요 없습니다(디버거가 attach 시 자동으로 '어느 가드가 막았나'를 잡습니다). **전역 beforeEach/beforeResolve**만, 그것을 등록하기 직전에 traceGuards(router) 한 줄을 넣습니다.
ts
import { createRouter, createWebHistory } from 'vue-router'
// ⚠ traceGuards는 '정적 import'다 — 가드 등록 직전에 '동기'로 불려야 하므로 동적 import로는 순서를 못 맞춘다.
import { traceGuards } from '@platform/vue-debugger'
import { useAuthStore } from '@/stores/auth'
import { authGuard } from './guards'
export const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/', component: () => import('@/pages/Home.vue') },
{ path: '/orders', component: () => import('@/pages/Orders.vue') },
// 라우트별 가드 — 디버거가 자동 귀속(설정 불필요)
{ path: '/admin', component: () => import('@/pages/Admin.vue'), beforeEnter: () => (useAuthStore().isAdmin ? true : '/') },
],
})
if (import.meta.env.DEV || (import.meta.env.MODE === 'staging' && import.meta.env.VITE_DEBUGGER === 'on')) {
traceGuards(router) // ← 전역 가드 등록 '전에' (동기 — 이 한 줄이 전부)
}
router.beforeEach(authGuard) // 이제 authGuard가 막거나 리다이렉트하면 Router 패널에 귀속된다정적 import인데 왜 프로덕션에 안 새나
프로덕션 빌드에선 분기가 false가 되어 traceGuards(...) 호출이 제거되고, 디버거 패키지의 sideEffects: false 덕에 import 자체가 트리셰이킹됩니다(누출 0). 가드 동작은 바뀌지 않습니다(원래 반환값·length 보존). 자세한 이유는 라우터 가드 추적을 보세요.
2-5. vite.config.ts — 배포 빌드용 두 스위치 (전체)
mode === 'staging'일 때만 두 가지를 켭니다. 둘 다 배포(vite build) 빌드에서 디버거가 '제대로' 보이게 하는 데 필요합니다(로컬 vite dev는 둘 다 불필요).
ts
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig(({ mode }) => {
const debug = mode === 'staging'
return {
plugins: [vue()],
resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } },
// ① 컴포넌트 introspection: 켜야 setup 바인딩이 노출돼 '값·출처칩·식 평가'가 보인다.
// 끄면(=기본 프로덕션) 트리는 보여도 State가 전부 비거나 undefined.
define: debug ? { __VUE_PROD_DEVTOOLS__: 'true' } : {},
// ② 소스맵: .map은 만들되 JS엔 sourceMappingURL 주석을 안 남긴다('hidden').
// 디버거가 .map을 직접 받아 스택을 'src/…:라인 (함수)' 원본 위치로 복원한다.
build: { sourcemap: debug ? 'hidden' : false },
}
}).map은 외부 공개 금지
소스맵은 원본 코드를 복원합니다. 검증계(내부망)에만 두고 외부 공개망엔 배포하지 마세요. 매트릭스·이유 전체는 환경별 설정에 있습니다.
2-6. .env.staging — 검증계 빌드 스위치
--mode staging 빌드에서만 로드되는 환경 파일입니다. 이 한 줄이 main.ts·router의 VITE_DEBUGGER === 'on' 분기를 켭니다.
bash
# .env.staging — 이 모드(staging) 빌드에만 디버거를 포함시킨다
VITE_DEBUGGER=on2-7. (변경 없음) 스토어 · axios — 그대로 둡니다
참고로, 디버거가 관찰하는 앱 코드는 전혀 바꾸지 않습니다. 아래는 예시 앱의 스토어와 axios 인스턴스인데, 한 줄도 손대지 않아도 Pinia 탭·네트워크 탭에 그대로 잡힙니다.
ts
// src/stores/auth.ts — 변경 없음. 디버거가 런타임에 발견해 Pinia 탭에 띄운다.
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', {
state: () => ({ user: null as null | { name: string }, isAdmin: false }),
actions: { login(name: string) { this.user = { name } } },
})ts
// src/api/client.ts — 변경 없음. axios 인스턴스가 몇 개든 인터셉터 등록 없이 네트워크 탭에 잡힌다.
import axios from 'axios'
export const api = axios.create({ baseURL: '/api' })(선택) 깔끔하게 — debuggerBootstrap(app) 한 줄로
app.use(...)가 여기저기 흩어져 main.ts가 길어진다면, §2-3의 조건 + 동적 import + attachDebugger 블록을 함수 하나로 분리해 app.mount() 바로 위에서 한 줄로 부르면 됩니다.
ts
import type { App } from 'vue'
export async function debuggerBootstrap(app: App): Promise<void> {
// ⚠ 조건은 반드시 '리터럴'로 — import.meta.env.* 가 정적 치환돼야 프로덕션에서 통째 제거된다(아래 표)
if (!(import.meta.env.DEV || (import.meta.env.MODE === 'staging' && import.meta.env.VITE_DEBUGGER === 'on'))) return
const { attachDebugger } = await import('@platform/vue-debugger')
attachDebugger(app, {
ignoreUrls: ['/heartbeat', /datadog|sentry/],
report: { endpoint: '/internal/reports', allowHosts: ['reports.corp.example'] },
})
}ts
import { debuggerBootstrap } from './debugger-bootstrap'
async function bootstrap() {
const app = createApp(App)
app.use(createPinia())
app.use(router)
// ... 흩어진 app.use(...) 전부 위에 ...
await debuggerBootstrap(app) // ← app.mount 바로 위 한 줄
app.mount('#app')
}
bootstrap()함수로 빼도 프로덕션 누출 0은 그대로 유지됩니다 — import.meta.env.*는 전역 정적 치환이라 함수 안이어도 if (false)로 죽고, 그 안의 동적 import()도 함께 제거됩니다. 단 아래를 지키세요.
| 규칙 | 이유 |
|---|---|
조건을 함수 안에 리터럴로 (import.meta.env.DEV || …) | 정적 치환 대상이라야 프로덕션에서 본문·동적 import가 제거됨 |
조건을 boolean 인자로 빼지 말 것 (debuggerBootstrap(app, enabled) ❌) | 정적 치환 대상이 사라져 dead-code 제거가 깨지고 디버거가 프로덕션에 새어 나감 |
async + await 후 mount | collector가 첫 렌더·요청 전에 설치되도록 |
traceGuards는 합치지 말 것 | 라우터 모듈에서 정적 import + 가드 등록 직전 동기 호출이라 비동기 함수에 못 넣음(§2-4 그대로) |
검증:
npm run build(프로덕션) 후grep -rl "__VUE_DEBUGGER__" dist/assets→ 0.
3. 실행 — 세 가지 환경
같은 코드가 환경에 따라 다르게 빌드됩니다. 핵심은 디버거는 로컬·검증계에만 들어가고, 프로덕션엔 0이라는 점입니다.
bash
npm run dev
# import.meta.env.DEV === true → 디버거 자동 ON. vite.config·.env 설정 불필요.bash
npm run build:staging # = vite build --mode staging
# .env.staging의 VITE_DEBUGGER=on → 디버거 + __VUE_PROD_DEVTOOLS__ + .map
npm run preview # 빌드본 미리보기 → ⚙ FAB 확인bash
npm run build
# 분기가 정적 false → 동적 import·traceGuards 호출이 dead-code로 제거. 디버거 흔적 0.4. 확인
npm run dev→ 우측 하단 ⚙ FAB.Alt+Shift+D(또는Esc로 닫기).- 화면 조작 → 네트워크 탭에 axios 요청 기록, 컴포넌트 탭 인스펙션으로 값·출처칩 확인.
- 프로덕션 안전:
npm run build후 누출 0 확인.bashgrep -rl "__VUE_DEBUGGER__" dist/assets 2>/dev/null && echo "⚠ 누출!" || echo "✓ 프로덕션 깨끗"
전체 검증 체크리스트와 CI 누출 게이트는 설치 검증을, 안 보이거나 값이 비면 트러블슈팅을 보세요.
부록 — webpack / Vue CLI 변형
import.meta.env가 없는 webpack/Vue CLI는 두 군데만 바꿉니다(나머지 파일 구조는 동일):
| Vite | webpack / Vue CLI |
|---|---|
import.meta.env.DEV || (import.meta.env.MODE === 'staging' && import.meta.env.VITE_DEBUGGER === 'on') | process.env.NODE_ENV !== 'production' || (__VDBG_STAGING__ && process.env.VUE_APP_DEBUGGER === 'on') — __VDBG_STAGING__는 빌드 스크립트에 묶인 DefinePlugin 상수(연동의 webpack 예시 참고) |
vite.config.ts의 define/build.sourcemap | vue.config.js의 configureWebpack.devtool('hidden-source-map') + DefinePlugin으로 __VUE_PROD_DEVTOOLS__ |
DefinePlugin이 process.env.*를 정적 치환해야 프로덕션 dead-code 제거가 똑같이 동작합니다. 분기 매트릭스 전체는 환경별 설정에 있습니다.