요약
Issue #111로 자산 경로는 해결됐지만, 앱이 라우터 basename을 배포용 base로 고정하면 자산이 전부 200으로 로드돼도 라우터가 아무것도 렌더하지 않아 화면이 빈 채로 남는다. 자산 로딩과 무관한 별개 층이며, 콘솔에 에러조차 남지 않아 진단이 어렵다.
메커니즘
GitHub Pages용 Vite 프로젝트의 정석 레시피가 원인이다:
<BrowserRouter basename={import.meta.env.BASE_URL}>
import.meta.env.BASE_URL은 빌드 타임에 base 값(/my-todo-app/)으로 인라인된다. 그런데 프리뷰에서 브라우저의 location.pathname은 /api/v1/previews/{sessionId}/{accessToken}/…이다.
react-router(6.30.4 dist/index.js:1212-1231 기준):
let trailingPathname = stripBasename(pathname, basename);
if (trailingPathname == null) return null;
…
if (locationContext == null) { return null; }
basename이 pathname의 접두사가 아니면 <Router>가 NOTHING을 렌더한다. 즉 빈 페이지.
basename을 지우는 것으로도 해결되지 않는다 — 이번엔 라우트 정의(path="/")와 실제 pathname이 어긋난다.
왜 구조적인가
프리뷰 URL은 /api/v1/previews/{sessionId}/{accessToken}/ 형태이고 accessToken은 매 열람마다 회전한다(Issue #77 G4). 따라서 앱의 basename을 프리뷰 URL에 맞추는 것은 불가능하다(빌드 타임에 알 수 없고, 회전할 때마다 재빌드해야 한다). 게이트웨이 프록시나 HTML rewrite로도 못 고친다 — 라우터는 서버 응답이 아니라 location.pathname을 보기 때문이다.
현재 영향 범위
2026-08-15 운영에서 관측된 앱은 해당 없음(실측: 번들 191KB에 my-todo-app 문자열도 import.meta 흔적도 없어 BASE_URL을 쓰지 않음). 즉 지금 당장 깨진 사용자는 확인되지 않았고, LLM이 라우터를 넣으면서 이 레시피를 따라 쓰는 순간 발현된다.
해결 후보
- 프리뷰 빌드에서 base를 중립화 — 배포가 하는 것과 같은 방식(
npx vite build --base=/)을 프리뷰에도 적용하면 BASE_URL이 /가 되어 basename도 /가 된다. 근본 해법이지만 제약이 있다:
- 프리뷰 빌드 경로는
npm run build 하드코딩(PreviewWorkspaceService.java:133-134)이고, npm run build -- --base=/는 체인 스크립트에서 위험하다 — npm은 인자를 스크립트 문자열 끝에 이어붙이므로 "vite build && cp dist/index.html dist/404.html" 같은 스크립트에서는 cp로 인자가 가서 빌드가 exit 1로 깨진다(실측 확인).
- Agent CODE 경로에는 서버가 통제하는 빌드 호출 자체가 없다(LLM이 직접
npm run build 실행) — 이 경로는 이 대책으로 커버되지 않는다.
- 서빙 계층에서 base 경로를 실재화 — dist 안에
ln -s . <base> 자기참조 심볼릭 링크. 컨테이너 실측에서 문서/JS/CSS/SPA 라우트 4케이스가 모두 올바른 MIME으로 200이 됐고 CapDrop ALL에서도 링크 생성이 성공했다. 다만 이것도 pathname 접두사(/api/v1/previews/...)를 없애주지는 못하므로 basename 문제 자체는 해결하지 못한다 — 자산 문제의 대안일 뿐이다.
- 프리뷰 전용 오리진 분리(§2.29부터 예고된 백로그) + base 중립화 조합이 유일한 완전 해법이다.
- 최소 조치: CODE 프롬프트에 "라우터 basename을 배포 경로로 고정하지 말 것"을 명시하고, 프리뷰가 비어 있을 때 이 원인을 안내한다.
출처
다중 에이전트 조사(2026-08-15, Issue #111 해법 판정 과정)의 react-router 소스 실측 및 컨테이너 실측. 관측 앱의 번들 분석은 직접 수행.
요약
Issue #111로 자산 경로는 해결됐지만, 앱이 라우터 basename을 배포용 base로 고정하면 자산이 전부 200으로 로드돼도 라우터가 아무것도 렌더하지 않아 화면이 빈 채로 남는다. 자산 로딩과 무관한 별개 층이며, 콘솔에 에러조차 남지 않아 진단이 어렵다.
메커니즘
GitHub Pages용 Vite 프로젝트의 정석 레시피가 원인이다:
import.meta.env.BASE_URL은 빌드 타임에 base 값(/my-todo-app/)으로 인라인된다. 그런데 프리뷰에서 브라우저의location.pathname은/api/v1/previews/{sessionId}/{accessToken}/…이다.react-router(6.30.4
dist/index.js:1212-1231기준):basename이 pathname의 접두사가 아니면
<Router>가 NOTHING을 렌더한다. 즉 빈 페이지.basename을 지우는 것으로도 해결되지 않는다 — 이번엔 라우트 정의(
path="/")와 실제 pathname이 어긋난다.왜 구조적인가
프리뷰 URL은
/api/v1/previews/{sessionId}/{accessToken}/형태이고 accessToken은 매 열람마다 회전한다(Issue #77 G4). 따라서 앱의 basename을 프리뷰 URL에 맞추는 것은 불가능하다(빌드 타임에 알 수 없고, 회전할 때마다 재빌드해야 한다). 게이트웨이 프록시나 HTML rewrite로도 못 고친다 — 라우터는 서버 응답이 아니라location.pathname을 보기 때문이다.현재 영향 범위
2026-08-15 운영에서 관측된 앱은 해당 없음(실측: 번들 191KB에
my-todo-app문자열도import.meta흔적도 없어 BASE_URL을 쓰지 않음). 즉 지금 당장 깨진 사용자는 확인되지 않았고, LLM이 라우터를 넣으면서 이 레시피를 따라 쓰는 순간 발현된다.해결 후보
npx vite build --base=/)을 프리뷰에도 적용하면BASE_URL이/가 되어 basename도/가 된다. 근본 해법이지만 제약이 있다:npm run build하드코딩(PreviewWorkspaceService.java:133-134)이고,npm run build -- --base=/는 체인 스크립트에서 위험하다 — npm은 인자를 스크립트 문자열 끝에 이어붙이므로"vite build && cp dist/index.html dist/404.html"같은 스크립트에서는cp로 인자가 가서 빌드가 exit 1로 깨진다(실측 확인).npm run build실행) — 이 경로는 이 대책으로 커버되지 않는다.ln -s . <base>자기참조 심볼릭 링크. 컨테이너 실측에서 문서/JS/CSS/SPA 라우트 4케이스가 모두 올바른 MIME으로 200이 됐고 CapDrop ALL에서도 링크 생성이 성공했다. 다만 이것도 pathname 접두사(/api/v1/previews/...)를 없애주지는 못하므로 basename 문제 자체는 해결하지 못한다 — 자산 문제의 대안일 뿐이다.출처
다중 에이전트 조사(2026-08-15, Issue #111 해법 판정 과정)의 react-router 소스 실측 및 컨테이너 실측. 관측 앱의 번들 분석은 직접 수행.