Skip to content

격리·호환·한계

@platform/vue-debugger는 호스트 앱 안에 함께 실행되지만, 호스트의 화면·스타일·성능·의존성 어느 것도 오염시키지 않도록 설계되었습니다. 이 페이지는 그 격리 보장과 0-런타임-의존성 설계 원리, Vue 버전 호환 전략을 설명하고, 마지막에 도구가 할 수 없는 일을 정직하게 고지합니다. 한계를 숨기지 않는 것이 인페이지 디버거의 신뢰성을 지키는 방식이기 때문입니다.

소비자 입장에서 이 페이지의 결론은 단순합니다. 설치해도 호스트 앱이 깨지지 않고, 끄면 비용이 0이며, 켜도 보이는 값에는 명시적 한계가 있다는 것입니다.

격리 보장 (호스트 앱 비오염)

디버거는 호스트 앱의 일부가 아니라 그 옆에서 도는 별개 프로그램으로 취급되어야 합니다. 그래야 디버깅 도구가 디버깅 대상의 동작을 바꾸지 않습니다.

격리 축방식결과
렌더 트리별도 Vue 앱 인스턴스호스트 컴포넌트 트리에 디버거 노드가 섞이지 않음
스타일Shadow DOM + :host{all:initial}호스트 CSS가 디버거에 새지 않고, 디버거 CSS도 호스트에 새지 않음
의존성vue peerDependency 하나뿐pinia/router/axios는 import하지 않고 구조적 타입으로만 관찰
비용패널이 닫혀 있으면 트리 순회·watcher·인스펙션 0 · 열린 동안에도 방문한 비활성 탭은 동결(0.17)단, 네트워크·콘솔·에러·리렌더 수집은 백그라운드 기록(상한 버퍼). '완전 0'은 아님

별도 Vue 앱 + Shadow DOM

디버거 UI는 호스트 앱과 다른 Vue 앱 인스턴스로 마운트되며, 그 마운트 지점은 Shadow DOM 안입니다. Shadow 루트의 :host{all:initial}이 모든 상속 스타일을 초기화하므로, 호스트의 전역 CSS(리셋·테마·* { box-sizing } 등)가 디버거 UI를 망가뜨리지 않고, 반대로 디버거 스타일도 Shadow 경계를 넘어 호스트로 새지 않습니다.

CSS import가 필요 없는 이유

디버거 스타일은 별도 .css 산출물이 아니라 JS 문자열로 Shadow DOM에 주입됩니다(src/ui/styles.ts). 그래서 소비 앱은 CSS import 설정 없이도 그대로 동작합니다. 연동에서 스타일시트를 불러올 필요가 없는 것은 우연이 아니라 이 격리 설계의 결과입니다. 연동 절차는 연동을 보세요.

런타임 의존성은 vue 하나

디버거는 pinia·vue-router·axios를 import하지 않습니다. 대신 런타임에 구조적 타입(있어야 할 모양)을 관찰해 발견합니다. 따라서 소비 앱이 그 라이브러리를 쓰지 않으면 해당 패널이 빈 상태로 표시될 뿐 오류가 나지 않고, 소비 앱의 버전 파편화에도 영향을 받지 않습니다. 발견 메커니즘과 타입(PiniaLike/RouterLike)은 옵션 레퍼런스와 함께 패널 레퍼런스에서 다룹니다.

닫혀 있으면 비용 0

패널이 닫혀 있는 동안 디버거는 호스트 컴포넌트 트리를 순회하지 않고 watcher도 만들지 않으며 인스펙션 오버레이도 비용이 없습니다. 다만 네트워크·콘솔·에러·리렌더 수집은 백그라운드에서 계속 기록합니다 — 나중에 패널을 열어도 직전 상황이 남아 있도록 한 의도된 설계입니다(그래서 '완전 0'은 아닙니다). 이 캡처 버퍼는 종류별로 상한이 고정되어 있어(기본 500건, maxEntries로 조정) 장시간 세션에서도 메모리가 무한히 늘지 않습니다.

패널이 열려 있는 동안의 비용도 0.17부터 한 단계 더 줄었습니다. Vue의 KeepAlive는 화면 밖(비활성) 탭의 렌더 이펙트를 정지시키지 않아, 한 번 방문한 탭이 이벤트마다 오프스크린 재계산을 계속하는 문제가 있었습니다(대형 스토어·수 MB 스토리지에서 실측 최대 ×4.5). 0.17은 방문한 비활성 탭의 계산을 동결하고 그 탭으로 돌아올 때 1회만 따라잡습니다 — 활성 탭의 체감과 데이터 최신성은 그대로입니다.

0-런타임-의존성 설계 원리

폐쇄망 반입을 *.tgz 하나로 끝내고 소비 앱 빌드를 절대 깨지 않으려면, 디버거의 dist가 외부 런타임 의존성 없이 자기 완결적이어야 합니다. 이를 위해 두 가지 결정을 의도적으로 유지합니다.

  • CSS를 JS 문자열로 Shadow DOM에 주입(src/ui/styles.ts) — 별도 .css 산출물이 없으므로 소비 앱에 CSS 로더/import 설정이 없어도 동작합니다.
  • source-map-js를 번들에 인라인 — 스택 원본 복원에 쓰는 이 라이브러리는 외부화하지 않고 dist/index.js에 인라인 번들됩니다. 그래서 소비자의 런타임 dependencies0개이고, source-map-js는 디버거의 devDependencies로만 남습니다.

검증 가능한 사실

타르볼의 런타임 의존성이 실제로 비어 있는지는 반입 전에 기계로 확인할 수 있습니다.

bash
tar -xzO -f platform-vue-debugger-0.17.0.tgz package/package.json \
  | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const p=JSON.parse(s);console.log('dependencies:',p.dependencies||'(없음)');console.log('peer:',p.peerDependencies)})"
# → dependencies: (없음)   peer: { vue: '^3.4.0' }

폐쇄망 반입 절차 전체는 에어갭 설치를 보세요.

이 0-런타임-의존성 + dist 자체 완결(번들·CSS 인라인) 원칙을 깨지 않는 한, 폐쇄망 반입은 계속 tgz 하나로 끝납니다. 유지보수자가 이 원칙을 지키며 빌드·게시하는 방법은 빌드 & 게시에 정리되어 있습니다.

Vue 버전 호환

디버거는 값의 출처를 알아내기 위해 Vue의 일부 내부 표면(공식 공개 API가 아닌 부분)을 들여다봅니다. 내부 표면은 마이너 버전업에서도 바뀔 수 있으므로, 그 의존을 한 파일에 가두고 계약 테스트로 회귀를 자동 적발하는 전략을 씁니다.

  • Vue 내부 표면 의존부는 전부 src/core/vueInternals.ts 한 파일에 격리되어 있습니다.
  • test/provenance.spec.ts가 그 계약 테스트입니다 — Vue 메이저/마이너를 올린 뒤 이 테스트가 깨지면, 손볼 곳은 vueInternals.ts 그 파일 하나뿐입니다.

peerDependency는 vue ^3.4.0입니다. 회사 표준인 vue 3.5.2도 이 범위에 포함되므로 그대로 충족되며, peer라서 디버거가 vue를 중복 설치하지 않고 소비 앱의 단일 인스턴스를 그대로 사용합니다.

버전업 시 점검 한 줄

Vue를 올렸다면 npm run test -w @platform/vue-debuggerprovenance.spec(내부 표면 계약)이 통과하는지 확인하세요. 통과하면 호환되는 것이고, 깨지면 깨진 곳이 곧 고칠 곳을 가리킵니다.

알려진 한계 (정직 고지)

좋은 디버거는 자신이 모르는 것을 안다고 말하지 않습니다. 아래 한계들은 도구가 회피하지 않고 정보 탭의 "알려진 한계"에 직접 고지하는 항목들입니다.

소스맵이 없으면 위치 복원이 안 됩니다

디버거는 번들 위치(chunk-abc.js:1:2345)를 소스맵으로 원본(LessonShell.vue:33)으로 되돌립니다. 따라서:

  • build.sourcemap: false(기본)면 .map이 없어 스택이 chunk:line로만 보입니다(원본 줄 불명).
  • .map을 빌드 산출물에서 빼고 배포하면(흔한 보안 정책) 디버거가 fetch하지 못해 마찬가지로 복원이 안 됩니다.

검증계에서 원본 위치를 보려면 sourcemap: 'hidden'이 필요합니다. 설정 방법과 보안 주의는 환경별 설정을 보세요.

minify는 함수명을 종종 소실시킵니다

소스맵은 보통 파일:줄은 복원해도 함수명 매핑은 불완전합니다. 그래서 디버거는 위치를 파일:줄 위주로 보여주고, 함수명이 없을 수 있음을 정보 탭에 고지합니다. "어느 함수인지"보다 "어느 줄인지"를 신뢰하세요.

연쇄 경보는 시간 인과 "추정"입니다

🔗 연쇄 패널은 한 번의 상태 변경이 부른 후속 효과(fetch·또 다른 변경·라우팅)를 묶어 보여줍니다. 깊은 연쇄에 붙는 "연쇄 경보"는 정적 의존성 분석이 아니라 시간 인과에 기반한 추정입니다 — 시간상 뒤따랐다고 해서 반드시 인과가 있는 것은 아닙니다. 단서로 쓰되 단정의 근거로 쓰지 마세요.

provenance 스토어 게터 분류 한계

값의 출처 칩(prop / pinia / computed / local)은 정확한 경우가 대부분이지만, Pinia 스토어 게터가 특정 형태일 때 분류가 어긋날 수 있는 알려진 경계가 있습니다. 객체·배열을 돌려주는 게터는 값-참조로 스토어에 귀속하지만, 원시값(숫자·문자열 등)을 돌려주는 게터는 안전을 위해 보수적으로 computed로 표시될 수 있습니다(스토어 게터인데 computed로 보일 수 있다는 뜻). 출처 칩은 강력한 힌트지만 모든 경우에 100% 단정적이지는 않습니다.

prop 출처는 직계 부모까지만 표시됩니다

prop의 출처 칩(prop ← 부모이름)은 값을 내려준 직계 부모의 이름만 보여줍니다. 그 부모가 다시 더 위에서 받아 내려준 값(prop drilling)이라면, 디버거는 그 윗단까지 거슬러 올라가 추적하지 못합니다. 마찬가지로 provide/inject로 주입된 값은 prop 출처로 표시되지 않습니다. "어디서 왔나"의 한 단계만 확실하다고 보세요.

전역(window/promise) 에러는 컴포넌트에 귀속되지 않습니다

디버거는 app.config.errorHandler로 잡힌 Vue 렌더/라이프사이클 에러는 어느 컴포넌트에서 났는지 함께 기록하지만, **window 'error' 이벤트와 처리되지 않은 Promise 거부(unhandledrejection)**는 발생 컴포넌트를 알 수 없습니다 — 브라우저가 그 정보를 주지 않기 때문입니다. 이런 에러는 메시지·스택만 기록되고 컴포넌트 귀속 없이 표시됩니다. 컴포넌트를 알려면 스택의 '내 코드' 프레임을 단서로 쓰세요.

읽기 의존성 맵은 getItem() 호출만 봅니다 0.17

스토리지 패널의 '읽기 의존성 맵'은 Storage.prototype.getItem 후킹 기반입니다. HTML 스펙상 storage['k']·storage.k 같은 괄호/점 접근은 getItem을 경유하지 않아 관측되지 않습니다. 또한 관측 기반(정적 분석 아님)이라 실행되지 않은 경로도 보이지 않습니다. 그래서 **"출처 0곳 ≠ 아무도 안 읽음"**입니다 — 키 삭제/마이그레이션 결정의 단독 근거로 쓰지 마세요(패널에도 같은 고지가 표시됩니다). 팀 차원에서는 스토리지 접근을 getItem 경유(또는 단일 유틸)로 강제하는 lint 규칙을 권장합니다.

브리지 rejection은 unhandledrejection으로 나가지 않습니다 0.17

디버거가 브리지 호출 결과를 기록하려고 반환 프라미스에 핸들러를 붙이는 순간, 그 rejection은 'handled'로 간주되어 앱의 전역 unhandledrejection(Sentry류 포함)이 발화하지 않습니다. 0.17부터는 이를 에러 패널에 재보고해 디버거 안에서는 잃지 않지만(앱이 자체 catch를 붙였는지는 알 수 없음을 문구로 고지), dev/검증 환경의 전역 리스너 기준으로는 관측이 달라질 수 있다는 점을 알아두세요.

리렌더 수집(mixin)은 destroy로 완전히 제거되지 않습니다

리렌더 추적은 Vue의 renderTriggered 훅을 app.mixin으로 모든 컴포넌트에 붙여 동작합니다. 그런데 Vue에는 한번 등록한 mixin을 제거하는 API가 없습니다. 그래서 destroy()를 호출해도 mixin 자체는 호스트 앱에 잔존하며, 디버거는 내부 enabled 플래그로 **무력화(기록 중단·라이브 리스트 비움)**만 합니다. 잔존 mixin은 비활성 상태라 기록을 남기지 않지만, 완전한 제거가 아니라는 점은 정직하게 고지합니다.

ESM 전용 — 동적 import()로만 소비됩니다

이 패키지는 ESM 전용("type": "module", exportsimport 조건만 노출)이라 require()로는 불러올 수 없습니다. 연동 예시가 동적 import()를 쓰는 것은 프로덕션 dead-code 제거를 위한 의도이기도 하지만, 동시에 ESM 전용이라는 제약이기도 합니다. CommonJS 전용 환경이라면 그대로는 소비할 수 없습니다.

다른 도구가 이미 console/fetch를 감쌌을 때

APM 등 다른 도구가 디버거보다 먼저 console·fetch를 래핑했다면, 디버거는 정보 탭에 '래핑 충돌'을 정직하게 고지합니다(호출 위치·타이밍이 어긋날 수 있다는 뜻). 노이즈를 줄이려면 ignoreUrls로 APM 비콘을 거르세요 — 환경별 설정옵션 레퍼런스를 참고하세요.

유지보수자에게: 이 한계들은 왜 그대로 두는가

이 한계들은 버그가 아니라 인페이지 도구의 본질적 비용입니다(번들 후 위치 복원, 시간 기반 인과 추론 등). 숨기는 대신 정보 탭에 고지하는 것이 설계 결정이며, 회귀를 막는 방법은 계약 테스트(provenance.spec.ts)와 패키징 회귀 게이트입니다. 빌드 원칙과 검증 게이트는 빌드 & 게시, 버전 정책은 버전 정책을 보세요.

관련 문서

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