You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
apps/blog/web은 Next.js 16 + React 19로 굳어 있습니다. 소스 191개, 글 70편, React 전용 런타임 의존만 8종(@ssgoi/react·@giscus/react·react-markdown·recharts·@tanstack/react-query·react-medium-image-zoom·react-syntax-highlighter·lucide-react)입니다.
여기서 궁금한 것이 넷입니다.
같은 사이트를 컴파일 기반 프레임워크로 지으면 번들이 실제로 얼마나 줄어드는가
이 블로그의 저작 문법(커스텀 태그 15종)이 React 없이도 같은 모양으로 서는가
@blog/content·check-seo·Panda blog-preset이 정말 프레임워크 중립인가 — 지금까지 소비자가 React 하나뿐이라 검증된 적이 없습니다
이 저장소의 저작 스킬(blog-design-system·blog-components·blog-diagrams·tech-blog-writer)이 React를 전제하고 있는가
넷 다 읽어서는 답이 안 나오고, 지어 봐야 나옵니다. 그래서 운영 블로그는 그대로 두고 SvelteKit으로 같은 사이트를 한 벌 더 짓습니다.
무엇을 만드는가
apps/blog/web-svelte(@blog/web-svelte) 워크스페이스를 새로 만들어, apps/blog/web과 같은 콘텐츠·같은 토큰·같은 URL 계약으로 블로그 전체를 SvelteKit + adapter-static으로 재구현합니다. 기존 React 앱은 지우지 않고 그대로 운영합니다.
결정 요약
축
결정
근거
프레임워크
SvelteKit + adapter-static
선정 기준은 문법 직관성 + 번들 최소. Solid 스파이크 없이 Svelte로 확정
대상 범위
공개 6개 라우트 + Admin 4개 = 전체
부분만 지으면 번들 비교가 사과와 오렌지가 됨
React 앱
유지
3중 비교가 아니라 2중 비교(React vs Svelte). 되돌리기가 항상 가능
워크스페이스
apps/blog/web-svelte / @blog/web-svelte
나중에 진짜 교체할 때 web으로 개명하는 경로가 자연스럽다
콘텐츠 소스
apps/blog/posts공유
원고를 복제하면 두 사이트가 조용히 갈라진다
콘텐츠 파이프라인
@blog/content그대로 재사용
자체 content.config.mts + content.values.mts만 새로 둔다
스타일
Panda CSS + blog-preset 그대로
색 토큰의 단일 출처가 blog-preset.ts라는 금지선을 유지. @design-system/ui의 React 컴포넌트만 재구현
Supabase
로컬 인스턴스만
프로덕션 post_views·post_view_logs에 쓰지 않는다 → 운영 조회수·Analytics 오염이 구조적으로 불가능
배포
별도 Worker, wrangler versions upload 프리뷰 URL만
custom_domain 없음. 최종 배포 전환은 이 이슈 범위 밖
기능 플래그
해당 없음
별도 워크스페이스라 기존 블로그 사용자에게 노출되는 경로가 없다. 워크스페이스의 존재 여부가 곧 스위치
마감
없음
실험·학습이 목적. 스택 PR을 틈날 때 쌓는다
목적 (성공 기준의 출처)
학습·비교 실험 — 반응성 모델·컴파일 전략·DX 차이를 직접 겪는다
번들 크기·성능 개선 여지 확인
블로그 글 소재 확보
저작 스킬 4종의 사용성·성능 점검 — React 전제가 얼마나 박혀 있는지
대시보드 (2026-09-13 3차 갱신)
구현은 전부 릴리즈 브랜치(claude/react-svelte-solid-migration-dkj247)에 머지됐습니다. main에는 아직 아무것도 들어가지 않았습니다.
머지 커밋으로 내렸습니다. 스쿼시하면 PR마다 새 SHA로 들어가 다음 PR의 merge-base가 옛 커밋에 남고, 그 PR의 diff가 앞 PR의 내용까지 끌어안습니다(실측 10 → 29파일)
머지 경로. REST 머지 엔드포인트는 스택 PR을 403(Merging stacked PRs via this endpoint is not supported)으로 거절합니다. gh stack merge <스택 번호> --yes --merge로 내렸습니다 — 메서드를 빼면 마지막에 쓴 방식을 따르므로 --merge를 명시해야 합니다
base는 미리 못 돌립니다. GitHub이 스택 멤버의 base 변경을 막습니다(Cannot change the base branch because the pull request is part of a stack)
#408은 GitHub 스택 UI에 묶여 있지 않았습니다. 생성 시점에 REST API가 POST /pulls를 500/502로 거절해 웹 UI로 만들었고, base가 올바라 머지 순서에는 영향이 없었습니다
preview-blog.yml 매트릭스의 두 줄(preview = Next.js, preview-svelte = SvelteKit)이 자기 앱 경로나 두 앱이 함께 읽는 경로(원고·packages/@blog·이 워크플로)가 바뀐 PR에서만 빌드·업로드합니다. 잡 if에서는 matrix를 쓸 수 없어 첫 스텝이 pulls/{n}/files로 판정하고 뒤 13스텝을 건너뜁니다. 옮긴 파일은 옛 경로도 보고, API 실패나 3000개 상한에서는 건너뛰지 않습니다.
레일 폭(maxW·px)을 페이지가 직접 쓴 곳이 있는가 — Rail.svelte만 압니다
인라인 eslint-disable이 있는가 — 0건이어야 합니다(noInlineConfig로 애초에 안 먹습니다)
화면 문자열이 React 판과 같은가 — 라벨·placeholder·aria-label. Svelte를 몰라도 두 파일을 나란히 놓으면 보입니다
결정이 든 파일 일곱
나머지는 기계적인 이식입니다. 논쟁할 값이 있는 것은 이 일곱입니다.
파일
무엇을 정하나
apps/blog/web-svelte/content.config.mts
경로 앵커와 배선. __CONTENT_ROOT__로 앵커를 덮는 이유가 주석에
apps/blog/web-svelte/content.values.mts
이 앱의 번들 규칙 12개. 마커를 왜 React 판과 다르게 골랐는지가 핵심
apps/blog/web-svelte/panda.config.ts
구문 강조 토큰 매핑. React는 서드파티 테마의 hex를 치환하고, 여기는 Prism 클래스에 역할을 직접 잇습니다
src/lib/server/markdown/index.ts + 형제들
커스텀 태그를 빌드 타임 HAST 변환으로. React는 런타임 컴포넌트 맵입니다
src/lib/client/archiveParams.ts
/posts URL 계약. nuqs를 안 쓰고 순수 함수 둘로
src/lib/shared/tocRail.ts
차례 레일 좌표 + 활성 구간 판정. 데스크톱·모바일이 같은 함수를 씁니다
src/lib/admin/charts/geometry.ts
차트 좌표. 영역 차트는 0→max, 스파크라인은 min→max로 스케일이 다르고, 그 이유가 주석에 있습니다(React 판이 그렇게 나뉘어 있습니다)
게이트가 대신 봐 주는 것
check-seo — 산출물 HTML의 SEO 계약 99페이지. React 판과 같은 검사기를 수정 없이 통과합니다
check-bundle — 청크 마커 규칙 12개. admin 코드·mermaid·구문 강조 파서·차트 곡선 계산이 공개 페이지 첫 로드에 없다는 것
check-parity — 두 산출물의 클래스 어휘·헤딩·링크·요소 수·아이콘 선언 대조
계약 테스트 116개 / 파일 15개 — URL 계약, 공개 판정, 커스텀 태그 구조, 좌표 계산, 차트 기하 스냅샷
lint --max-warnings=0, check-types
게이트는 전부 JS가 돌기 전의 산출물만 봅니다. 브라우저에서 JS가 DOM을 바꾸거나 읽는 조각(mermaid·이미지 줌·코드 복사·차례)은 어느 게이트에도 걸리지 않습니다. mermaid 글 5편의 본문이 사라진 채로 #401부터 #412까지 통과한 이유입니다 — 아래 「배포 뒤 발견한 결함」
측정 결과 (2026-09-13 재측정, 양쪽 새로 빌드)
blog-content measure-bundle — gzip 첫 로드 전송량(HTML + CSS + JS), 프로덕션 빌드.
페이지
React
Svelte
비율
차이
/
313.6 KB
87.4 KB
27.9%
−226.2 KB
/about/
313.0 KB
86.9 KB
27.8%
−226.1 KB
/posts/ (중앙값)
339.7 KB
114.8 KB
33.8%
−224.9 KB
/series/
255.1 KB
87.5 KB
34.3%
−167.6 KB
/privacy/
249.9 KB
85.2 KB
34.1%
−164.7 KB
/404/
248.1 KB
82.4 KB
33.2%
−165.7 KB
/admin/ (중앙값)
447.9 KB
146.1 KB
32.6%
−301.8 KB
산출물 전체: 921개 파일 96.5 MB → 529개 파일 64.0 MB
어디서 갈리는가 — 분해
페이지
HTML
CSS
JS
/
React
9.3 KB
30.3 KB
274.0 KB (13개)
Svelte
5.8 KB
29.5 KB
52.1 KB (19개)
/404/
React
5.2 KB
30.3 KB
212.7 KB (9개)
Svelte
3.3 KB
29.5 KB
49.6 KB (18개)
/admin/
React
5.6 KB
30.3 KB
412.1 KB (17개)
Svelte
3.4 KB
29.5 KB
113.2 KB (25개)
CSS가 30.3 대 29.5로 사실상 같습니다. 파리티 전에는 30.3 대 10.3이었습니다. 그 격차는 프레임워크가 아니라 기능 수였다는 것이 이걸로 확인됩니다 — 같은 Panda 프리셋이라 화면이 같아지면 유틸리티도 같아집니다. 이 결론은 라이브러리 선택과 무관합니다.
JS 수치를 어디까지 믿을 수 있나
JS는 페이지마다 오염 정도가 다릅니다. 가장 깨끗한 것은 /404/ 입니다 — 차트도 쿼리도 URL 파라미터도 애니메이션도 없어서, 논쟁 중인 라이브러리가 하나도 걸리지 않습니다.
/posts/·/admin/은 여전히 라이브러리 선택이 섞여 있습니다 — 아래 절을 보세요.
라이브러리 비용 — 실측 (2026-09-13)
「손으로 만들기 전에 대안을 찾지 않았다」는 지적에 대한 조사 결과입니다. Vite + Svelte 5.57 프로브를 만들어 실제로 번들했습니다(gzip, Svelte 런타임 베이스라인 16.1 KB 제외).
후보
추가
렌더
Panda 토큰
비고
d3-shape (곡선만)
+2.3 KB
SVG
✅
채택 — #408에서 들어감
@tanstack/svelte-query
+6.8 KB
—
—
미적용
d3-shape + d3-scale
+10.6 KB
SVG
✅
layercake 8.4
+20.9 KB
SVG(마크는 직접)
✅
유일한 진지한 대안
uPlot 1.x
+22.5 KB
Canvas
❌
구조적 탈락
chart.js (tree-shaken)
+58.4 KB
Canvas
❌
구조적 탈락
@observablehq/plot
+130.0 KB
SVG
△
layerchart 2.5 (/svg 서브패스)
+148 KB
SVG
❌ Tailwind
recharts보다 큼
recharts (React 판 현재)
~129 KB
SVG
✅
마커를 가진 청크 3개의 합(상한)
두 가지가 뒤집혔습니다.
layerchart는 배럴 탓이 아니었습니다. 처음 +132.5 KB가 나왔을 때 export * 트리셰이킹 실패로 보고 넘겼는데, layerchart/svg 서브패스로 좁혀도 오히려 더 큽니다(148 KB, 정적 클로저 32청크). 게다가 스타일이 @layerstack/tailwind에 묶여 Panda strictTokens 위에 얹을 수 없습니다
Canvas 계열은 크기와 무관하게 탈락입니다. 색을 css()로 못 주니 CSS 변수를 읽어 주입하고 테마 전환마다 다시 그려야 하는데, 이 저장소는 다이어그램 SVG까지 전부 토큰에 연결하는 금지선이 있습니다. check-seo·check-bundle도 canvas 내용은 못 봅니다
recharts 129 KB가 이 대시보드에서 실제로 하는 계산은 2.3 KB입니다. recharts의 victory-vendor가 d3-shape/d3-scale을 벤더링한 것이고, type="monotone"은 curveMonotoneX 그 자체입니다.
라이브러리 교체 비용의 분해
recharts를 쪼개면 셋으로 갈립니다. 이 축이 「무엇이 비쌌는가」를 설명합니다.
조각
프레임워크 종속?
이 화면의 비용
계산 (d3-shape·d3-scale)
무관
2.3 KB — 그대로 가져옴
DOM 측정 (ResponsiveContainer)
무관 (브라우저 API)
0 KB — bind:clientWidth
컴포넌트 조립 (<XAxis/>·<Area/>)
React 전용
나머지 전부
셋째가 가치는 가장 작고 번들은 가장 큽니다. 같은 분해를 nuqs에 대면 셋째가 거의 0이라 73줄로 끝났고, motion은 셋째가 아예 없어서 패키지가 그대로 갈 수 있었습니다. 분해가 곧 비용 예측입니다. 전문은 이 코멘트.
남은 오염 — 무엇이 정리됐고 무엇이 아닌가
라이브러리
상태
recharts ↔ 직접 그린 SVG
정리됨(다른 방식으로). 대안 8종을 실측했고 공정한 교체 대상이 없음을 확인했습니다(위 표). 동시에 Svelte 판이 빠뜨리고 있던 반응형 사이징과 곡선 보간을 #408에서 채워 기능이 동등해졌습니다. 「같은 라이브러리」는 아니지만 「같은 기능, 근거 있는 선택」입니다
@tanstack/react-query ↔ 69줄 스토어
미정리. svelte-query 비용만 쟀습니다(+6.8 KB). 넣어서 재지는 않았습니다
nuqs ↔ 73줄 순수 함수
미정리.sveltekit-search-params 4.0.0을 재지 않았습니다
motion ↔ svelte/transition
미정리.motion은 프레임워크 중립이라 같은 패키지를 쓸 수 있는데 쓰지 않았습니다. 현재 svelte/transition의 fly·fade·scale을 씁니다 — 스프링과 다른 움직임입니다
HTML에서도 갈립니다 — 하이드레이션 페이로드
글 상세 HTML을 페이로드와 마크업으로 갈라 재면:
전체(gzip)
페이로드 제외
페이로드 원문
React
34.4 KB
13.2 KB
153.5 KB
Svelte
20.1 KB
12.0 KB
49.3 KB
마크업 자체는 13.2 대 12.0으로 거의 같습니다 — 픽셀 파리티가 다른 각도에서 한 번 더 확인됩니다. 차이 14 KB는 전부 RSC flight 페이로드입니다.
#408이 d3-shape(+2.3 KB)와 스파크라인 채움 경로를 넣은 결과입니다. admin의 +3.3 KB가 그 실제 비용이고, 공개 페이지도 0.8 KB씩 늘었습니다 — d3-shape가 샌 것은 아닙니다(check-bundle 규칙 12번이 curveMonotoneX가 어느 페이지 첫 로드에도 없음을 강제하고 통과합니다). 청크가 하나 늘면서 SvelteKit 매니페스트가 커진 것으로 보이나 확인하지 않은 추정입니다.
아직 못 잰 것
query·url-params·motion의 라이브러리 비용 분리 — 위 표의 미정리 셋
LCP · 하이드레이션 시간 — 측정 항목에 있지만 재지 못했습니다. 번들 크기만 있습니다
빌드 시간 — React 49초는 기록돼 있으나 같은 조건의 Svelte 수치가 없습니다
프레임워크 중립성 — 가정이 어떻게 판명됐나
자산
가정
판명
@blog/content 로더·공개 판정·URL 계약
중립
참. 다만 배럴이 node:fs를 함께 열어, 클라이언트 그래프에서 빈 스텁으로 externalize됐습니다(빌드는 성공하고 런타임에만 깨집니다). 클라이언트 문 @blog/content/client를 새로 냈습니다
@blog/content 빌드 스크립트
중립
참. 진입점을 blog-content 하나로 모았습니다
check-seo
중립
참 — 수정 없이 통과합니다. 이 실험에서 가장 강한 중립성 증거
check-bundle
반쯤 중립
거짓이었습니다./_next/static/chunks/가 경로에 박혀 있어 SvelteKit 산출물에서 청크를 하나도 못 찾았습니다. 음성 검사만 있었다면 규칙 전부가 "누수 없음"으로 통과했겠지만, 양성 대조(requiredIn)가 #328(2026-08-27)부터 규칙마다 필수라 전 규칙이 marker-dead로 실패하며 드러났습니다. 경로 관례를 걷어냈습니다(#399). 정정: 이전 기록의 "규칙마다 양성 대조를 필수로 만들었다"는 틀렸습니다 — 필수화는 이 실험 전의 일입니다
Panda blog-preset
중립
참.jsxFramework 없이 css() 함수만 씁니다
@design-system/ui 컴포넌트
비중립
예상대로. Svelte로 재구현
React 전용 런타임 의존 8종
전부 다시 만들어야 한다
거짓. 셋(react-markdown·react-syntax-highlighter·@giscus/react)은 React 바인딩일 뿐이라 벗겨 내면 같은 엔진(unified·refractor·giscus 스크립트)을 그대로 씁니다. 넷(react-query·recharts·nuqs·motion)은 Svelte 대안이 있는데 찾아보지 않고 손으로 만들었습니다 — 뒤늦게 전부 실측했고(위 표), 그중 recharts만 "대안이 더 나쁘다"가 확인됐습니다
예상 밖의 결과
사이트 값이 중립이 아니었습니다. React 앱의 content.values.mts에 묶여 있어 @blog/site-values 패키지로 뺐습니다
조회수·대시보드 도메인 2,560줄에 react·next/ import가 0이었습니다. 원래 중립이었고 앱에 묶여 있던 것은 env를 읽어 클라이언트를 만드는 일 하나였습니다 → @blog/analytics
다이어그램 좌표 계산기도 중립이었습니다 → @blog/diagram. 두 앱이 같은 좌표를 쓰므로 같은 원고가 픽셀 단위로 같은 그림이 됩니다
"React 전용 의존"의 절반은 React 전용이 아니었습니다. 위 표의 마지막 줄
Svelte 앱 11,093줄 중 3,053줄이 프레임워크 import가 0입니다(lib/server 2,248 + lib/shared 654 + lib/domain·platform 151). boundaries 린트가 강제한 결과고, 그만큼은 다음 프레임워크로 그대로 갑니다
이 실험이 실제로 가르쳐 준 것
계획에 없던 발견들입니다. 깨진 가정 자체가 산출물이라는 방침에 따라 남깁니다.
기계가 세는 게이트는 화면을 보지 않습니다.check-seo·check-bundle·테스트가 전부 초록인 채로 여섯 페이지가 자리만 잡은 화면이었습니다. 12개 PR 동안 드러나지 않았습니다
완료 기준에 없는 것은 만들어지지 않습니다. 원장의 18개 기준 중 "화면이 같다"고 말하는 줄이 없었고, 그래서 화면이 같아지지 않았습니다
"계약·회귀 테스트만 이식"이 시각 계약 테스트를 정확히 제외했습니다. React 판 Rail.test.tsx의 test('기본 폭은 text다')를 이식했다면 레일 폭 버그는 없었을 것입니다
웹폰트 누락은 박스로는 안 잡힙니다. JetBrains Mono를 안 싣고 있었는데 모노는 0.6em 고정폭이라 글자 상자가 픽셀 단위로 같았습니다. 크롬의 실제 렌더 폰트 조회(CSS.getPlatformFontsForNode)로만 드러났습니다. Pretendard 때는 한글 폴백의 폭이 달라 줄바꿈이 어긋나며 드러났습니다
번들 수치에는 유효성 전제가 필요합니다. 기능이 다른 두 산출물의 크기 비교는 아무것도 말해 주지 않습니다
대체물을 만들기 전에 대안을 찾아야 합니다. 라이브러리 넷을 손으로 만들면서 Svelte 대안을 찾아보지 않았고, 그 선택이 주 지표를 오염시켰습니다. 「찾아봤는데 없어서」와 「안 찾아봐서」는 글로 적으면 똑같이 읽힙니다 — 전자였는지 확인할 방법이 기록에 없으면 후자로 보는 편이 안전합니다
애니메이션은 정지 마크업 대조로 못 잡습니다.motion의 스프링을 svelte/transition의 280ms fly로 바꿔 놓고 픽셀 파리티를 달성했다고 적었습니다. 다른 움직임입니다
게이트에 사각지대가 있으면 거기에 결함이 고입니다./admin은 로그인 게이트 뒤라 배포된 프리뷰에서 열리지 않고, check-parity의 PARITY_PAGES 5개에도 없습니다 — 두 검증 장치 어느 쪽도 보지 않는 화면입니다. 그 틈에서 AreaChart가 preserveAspectRatio="none"으로 viewBox를 가로로만 늘려 축 글자가 뭉개지고 커서 점이 타원인 채로 살아남았습니다. 빌드도 check-seo도 check-bundle도 전부 통과하고 있었습니다(#408에서 수정 + 좌표 스냅샷으로 잠금)
손으로 만든 코드가 실패한 자리는 계산이 아니라 렌더링 환경 대응이었습니다. 차트 428줄을 쪼개면 geometry.ts 101줄(순수 함수 + 테스트)에서도, 마크업 250줄에서도 버그가 안 났습니다. 난 곳은 실제 폭 측정·곡선 보간·라벨 겹침 — 전부 브라우저에서만 드러나고 순수 함수로 안 떨어지는 축입니다. 라이브러리가 파는 것의 상당 부분이 사실 이쪽이고, 직접 만들면 이 축이 구조적으로 빕니다
라이브러리 교체 비용은 "대응 패키지가 있나"가 아니라 "렌더 트리에 얼마나 붙어 있나"로 결정됩니다. 위 「라이브러리 교체 비용의 분해」 절
본문을 빌드 타임 HTML로 구우면, 상호작용은 "마운트 뒤 DOM 고치기"가 되고 그 코드는 게이트 밖에 놓입니다. React 판은 react-markdown이 렌더할 때 코드 블록 노드를 컴포넌트로 바꿔(CodeBlock.tsx — language === 'mermaid'면 <MermaidLazy>) 그림이 트리의 제자리에 들어갑니다. 무엇을 바꿀지 고르는 코드가 없습니다. Svelte 판은 본문을 {@html} 문자열로 넣어 그 안에 컴포넌트를 둘 수 없으므로, Mermaid.svelte가 마운트 뒤 DOM을 찾아 replaceWith로 바꿉니다. 바꿀 대상은 다른 파일(codeBlock.ts)이 정한 마크업 모양에 대한 가정이었고, 그 가정이 틀려 본문 전체가 그림 하나로 대체됐습니다(fix(blog-web-svelte): mermaid 그림이 글 본문 전체를 대신하지 않게 한다 #413). 그림 그리기는 두 판 모두 같은 mermaid.render입니다 — 라이브러리의 문제가 아니라 10번 분해의 컴포넌트 조립 조각을 손으로 대신한 자리입니다. 빌드 타임 굽기를 택한 이유(load 데이터가 하이드레이션용으로 HTML에 한 번 더 직렬화된다)는 apps/blog/web-svelte/README.md에 있습니다
서버 HTML(curl)에는 본문이 전부 있다(h2 6 · p 33). 브라우저에서 JS가 돈 뒤에는 #post-content가 없고, 그 자리에 mermaid 그림 상자 하나만 남는다
원인
codeBlock.ts는 mermaid 펜스를 상자로 감싸지 않는다(<pre><code class="language-mermaid">가 본문 바로 아래). Mermaid.svelte는 <div><pre><code> 모양을 가정하고 closest('pre')?.parentElement를 바꿨다 → 그 부모가 #post-content
기간
Mermaid.svelte가 들어온 #401(4bf2cae)부터 #413 머지(f3340b2)까지
영향
mermaid 펜스가 있는 글 5편 — payment-system-architecture · next-js-ecs-deploy · 번들러 2·3·4편. React 판은 무관
수정
<pre> 하나만 바꾼다. 서버 쪽 테스트에 "mermaid 펜스는 상자 없이 <pre>로 나간다"를 잠갔다 — 이 단언은 수정 전에도 통과한다. 클라이언트 버그는 이 앱의 node 테스트 환경에서 재현되지 않는다
검증
JS가 돈 뒤 #post-content의 h2·p 수를 서버 HTML과 대조 — 로컬 빌드에서 글 44편 전부, PR 프리뷰 배포본에서 mermaid 글 5편. 그림 8개가 서버의 mermaid <pre> 수와 같다
마운트 뒤 DOM을 만지거나 읽는 다른 조각은 동작을 확인하지 않았습니다. 이번 대조는 "본문이 남는가"만 봤습니다.
src/lib/client/CopyCode.svelte — closest('figure')로 코드를 찾는다
src/lib/client/ImageZoom.svelte
src/lib/client/Toc.svelte · MobileToc.svelte — 헤딩 id로 요소를 찾는다
스택 PR 계획 → 실제 진행
원래 6단계를 계획했으나 14개로 쪼개졌습니다.
계획 #
내용
상태
1
기준선 측정 + 워크스페이스 뼈대
✅
2
라우팅 + 정적 페이지
✅
3
마크다운 렌더 + 커스텀 태그
✅ 13종(원고가 실제로 쓰는 전부)
4
Mermaid · 구문 강조 · 이미지 줌 + check-bundle 규칙
✅ 규칙 12개
5
런타임 기능
✅ 페이지 전환 애니메이션만 제외
6
Admin + 배포 + CI + 문서 + 글 초안
✅ 스킬 점검 보고서 완료 · 비교 글 초안은 보류
—
화면 파리티 (계획에 없던 단계)
✅
—
라이브러리 대안 실측 (계획에 없던 단계)
✅ 8종 측정 완료
—
라이브러리 비용 분리 (계획에 없던 단계)
🔄 차트 축만 정리됨. query·url-params·motion 남음
성공 기준
목표 수치는 지금 정하지 않습니다. 1번 PR에서 React 쪽 기준선을 먼저 재고, 그 숫자를 본 뒤 목표를 정해 이 이슈에 추가합니다. 지금 지어낸 숫자보다 정직합니다.
측정 항목(양쪽 동일 조건, 프로덕션 빌드 기준):
글 상세 페이지 초기 JS gzip 크기 — 주 지표 → 273.2 KB → 64.4 KB (⚠️ query·url-params 오염 잔존)
홈 초기 JS gzip 크기 → 274.0 KB → 52.1 KB (⚠️ 같은 오염)
/404/ 초기 JS gzip 크기 — 오염이 가장 적은 지표 → 212.7 KB → 49.6 KB (23.3%)
이 방침이 시각 계약 테스트를 함께 제외했습니다. 위 「나중에 추가한 기준」이 그 수정입니다.
글 70편 스모크 — 전부 200으로 서고 빌드가 통과한다
커스텀 태그 각각의 렌더 계약 — 필수 속성, 자식 구조, 누락 속성 시 동작
check-seo 규칙 전부 통과 — h1 1개, description 중복·길이, <title> 60자, canonical 자기참조, og 태그, img alt, 산출물↔발행 글 정합성
URL 계약 — 후행 슬래시, .이 든 slug(turborepo-next.js-docker·vue-3.0) 처리
공개 판정 회귀 — draft·scheduled 글이 프로덕션 빌드에서 빠지고 dev에서는 배지와 함께 보인다
지연 로드 경계 — Mermaid·구문 강조·차트 곡선 계산이 글 상세 외 라우트 청크에 없다
테마 — pre-paint 스크립트가 FOUC 없이 html[data-theme]를 세팅한다
조회수 — 6시간 쿠키 쿨다운이 지켜지고 두 탭 레이스에서 중복 카운트가 없다
차트 기하 — 좌표 문자열 스냅샷(720·1152 두 폭, 스파크라인 80×32·96×24). /admin이 시각 검증 경로가 없으므로 계산을 대신 잠근다
비회귀: apps/blog/web의 기존 테스트 전량이 계속 통과한다. pnpm build --filter=@blog/web이 계속 통과한다
범위 밖
apps/blog/web 교체·제거 — 최종 배포 전환은 비교 결과를 보고 별도로 판단합니다. 이 이슈는 비교 재료를 만드는 데까지입니다
프로덕션 Supabase 연결 — 로컬 인스턴스만. 배포된 프리뷰는 동적 기능이 빈 상태입니다
커스텀 도메인 · 색인 허용 — 프리뷰 URL만 냅니다. 중복 콘텐츠 판단은 최종 배포 결정 때 함께 봅니다
Solid 구현 — Svelte 하나로 갑니다
기능 플래그 — 별도 워크스페이스라 필요 없습니다
스킬 파일 자체 수정 — 점검 보고서까지만. 스킬을 고치는 것은 보고서를 보고 별도로 판단합니다
apps/react·apps/next.js 등 다른 실험 앱 — 손대지 않습니다
두 앱의 admin 라우트 구조 정렬 — React는 /admin이 글 관리 화면(509줄)이고 대시보드가 /admin/analytics인데, Svelte는 /admin이 대시보드입니다. 그래서 React TopPostsTable의 행별 스파크라인에 대응하는 자리가 Svelte에 없습니다. 컴포넌트 문제가 아니라 구조 문제라 별도 판단입니다
배경
apps/blog/web은 Next.js 16 + React 19로 굳어 있습니다. 소스 191개, 글 70편, React 전용 런타임 의존만 8종(@ssgoi/react·@giscus/react·react-markdown·recharts·@tanstack/react-query·react-medium-image-zoom·react-syntax-highlighter·lucide-react)입니다.여기서 궁금한 것이 넷입니다.
@blog/content·check-seo·Pandablog-preset이 정말 프레임워크 중립인가 — 지금까지 소비자가 React 하나뿐이라 검증된 적이 없습니다blog-design-system·blog-components·blog-diagrams·tech-blog-writer)이 React를 전제하고 있는가넷 다 읽어서는 답이 안 나오고, 지어 봐야 나옵니다. 그래서 운영 블로그는 그대로 두고 SvelteKit으로 같은 사이트를 한 벌 더 짓습니다.
무엇을 만드는가
apps/blog/web-svelte(@blog/web-svelte) 워크스페이스를 새로 만들어,apps/blog/web과 같은 콘텐츠·같은 토큰·같은 URL 계약으로 블로그 전체를 SvelteKit +adapter-static으로 재구현합니다. 기존 React 앱은 지우지 않고 그대로 운영합니다.결정 요약
adapter-staticapps/blog/web-svelte/@blog/web-svelteweb으로 개명하는 경로가 자연스럽다apps/blog/posts공유@blog/content그대로 재사용content.config.mts+content.values.mts만 새로 둔다blog-preset그대로blog-preset.ts라는 금지선을 유지.@design-system/ui의 React 컴포넌트만 재구현post_views·post_view_logs에 쓰지 않는다 → 운영 조회수·Analytics 오염이 구조적으로 불가능wrangler versions upload프리뷰 URL만custom_domain없음. 최종 배포 전환은 이 이슈 범위 밖목적 (성공 기준의 출처)
대시보드 (2026-09-13 3차 갱신)
구현은 전부 릴리즈 브랜치(
claude/react-svelte-solid-migration-dkj247)에 머지됐습니다. main에는 아직 아무것도 들어가지 않았습니다.9a2082b(맨 위 #408)286b603f3340b2머지 기록
svelte-1-baselinesvelte-2-site-values@blog/site-values로svelte-3-routingcheck-seo통과svelte-4-markdown-componentssvelte-5-diagrams@blog/diagram으로svelte-6-code-blockscheck-bundle에서 경로 관례 제거svelte-7-code-blockssvelte-8-client-runtimesvelte-9-search-commentssvelte-10-admin@blog/analytics로svelte-11-admin-uisvelte-12-ci-deploysvelte-13-home-paritycheck-parity+ 주석 규칙svelte-14-admin-chartsMerging stacked PRs via this endpoint is not supported)으로 거절합니다.gh stack merge <스택 번호> --yes --merge로 내렸습니다 — 메서드를 빼면 마지막에 쓴 방식을 따르므로--merge를 명시해야 합니다Cannot change the base branch because the pull request is part of a stack)POST /pulls를 500/502로 거절해 웹 UI로 만들었고, base가 올바라 머지 순서에는 영향이 없었습니다#409 — 프리뷰를 앱별로 조건부 빌드
preview-blog.yml매트릭스의 두 줄(preview= Next.js,preview-svelte= SvelteKit)이 자기 앱 경로나 두 앱이 함께 읽는 경로(원고·packages/@blog·이 워크플로)가 바뀐 PR에서만 빌드·업로드합니다. 잡if에서는matrix를 쓸 수 없어 첫 스텝이pulls/{n}/files로 판정하고 뒤 13스텝을 건너뜁니다. 옮긴 파일은 옛 경로도 보고, API 실패나 3000개 상한에서는 건너뛰지 않습니다.preview-svelte잡이 70초 → 6초workflowPreviewGate.test.ts(구조) +workflowPreviewGateScript.test.ts(워크플로의run:블록을 꺼내 가짜gh로 실행, 27개)스벨트를 몰라도 리뷰하는 법
코드 구조는 알지만 Svelte 문법이 처음일 때를 위한 안내입니다.
문법 대응표 — 이 스택에 실제로 나오는 것만
useStatelet x = $state(0)useMemo/ 파생값const y = $derived(...), 여러 줄이면$derived.by(() => ...)useEffect$effect(() => { ...; return () => 정리 })function C({ a, b })const { a, b } = $props()children{@render children()}, 타입은SnippetonClick={fn}onclick={fn}{cond && <X/>}{#if cond}<X/>{/if}xs.map(x => <Row key={x.id}/>){#each xs as x (x.id)}<Row/>{/each}ref={el}bind:this={el}useRef+ResizeObserverbind:clientWidth={w}<ResponsiveContainer>자리에 씀파일 배치도 대응됩니다.
app/posts/page.tsxroutes/posts/+page.svelteroutes/posts/+page.server.ts의load()generateStaticParams+page.server.ts의entries()app/layout.tsxroutes/+layout.svelte'use client'src/lib/server/아래 모듈을 화면이 import하면 빌드가 실패한다마지막 줄이 실제로 도움이 된 차이입니다. React 판은 "이 모듈은 서버 전용"을 주석과 리뷰로 지키는데(
src/content.ts), 여기서는 도구가 강제합니다.문법을 몰라도 볼 수 있는 것 — 체크리스트 여섯
Svelte 코드 한 줄도 읽지 않고 이것만 확인해도 위험은 거의 덮입니다.
@blog/content배럴을 여는 곳이 있는가 — 있으면 안 됩니다. 배럴은node:fs를 함께 열어서, 클라이언트 그래프에 들어가면 Vite가 빈 스텁으로 바꿉니다(빌드는 성공하고 런타임에만 깨집니다). 화면은@blog/content/client만 씁니다→
grep -rn "from '@blog/content'" apps/blog/web-svelte/src/lib/components apps/blog/web-svelte/src/lib/client@blog/site-values, 앱 경로는src/lib/shared/routes.ts, 글 URL은postPath→
grep -rn "#[0-9a-fA-F]\{6\}" apps/blog/web-svelte/srcmaxW·px)을 페이지가 직접 쓴 곳이 있는가 —Rail.svelte만 압니다eslint-disable이 있는가 — 0건이어야 합니다(noInlineConfig로 애초에 안 먹습니다)결정이 든 파일 일곱
나머지는 기계적인 이식입니다. 논쟁할 값이 있는 것은 이 일곱입니다.
apps/blog/web-svelte/content.config.mts__CONTENT_ROOT__로 앵커를 덮는 이유가 주석에apps/blog/web-svelte/content.values.mtsapps/blog/web-svelte/panda.config.tssrc/lib/server/markdown/index.ts+ 형제들src/lib/client/archiveParams.ts/postsURL 계약. nuqs를 안 쓰고 순수 함수 둘로src/lib/shared/tocRail.tssrc/lib/admin/charts/geometry.ts게이트가 대신 봐 주는 것
check-seo— 산출물 HTML의 SEO 계약 99페이지. React 판과 같은 검사기를 수정 없이 통과합니다check-bundle— 청크 마커 규칙 12개. admin 코드·mermaid·구문 강조 파서·차트 곡선 계산이 공개 페이지 첫 로드에 없다는 것check-parity— 두 산출물의 클래스 어휘·헤딩·링크·요소 수·아이콘 선언 대조--max-warnings=0,check-types측정 결과 (2026-09-13 재측정, 양쪽 새로 빌드)
blog-content measure-bundle— gzip 첫 로드 전송량(HTML + CSS + JS), 프로덕션 빌드.//about//posts/(중앙값)/series//privacy//404//admin/(중앙값)산출물 전체: 921개 파일 96.5 MB → 529개 파일 64.0 MB
어디서 갈리는가 — 분해
//404//admin/CSS가 30.3 대 29.5로 사실상 같습니다. 파리티 전에는 30.3 대 10.3이었습니다. 그 격차는 프레임워크가 아니라 기능 수였다는 것이 이걸로 확인됩니다 — 같은 Panda 프리셋이라 화면이 같아지면 유틸리티도 같아집니다. 이 결론은 라이브러리 선택과 무관합니다.
JS 수치를 어디까지 믿을 수 있나
JS는 페이지마다 오염 정도가 다릅니다. 가장 깨끗한 것은
/404/입니다 — 차트도 쿼리도 URL 파라미터도 애니메이션도 없어서, 논쟁 중인 라이브러리가 하나도 걸리지 않습니다./404/JS차이 163.1 KB 중 확실하게 귀속되는 것:
react-dom+unstable_scheduleCallback+createRootMinified React error)/posts/·/admin/은 여전히 라이브러리 선택이 섞여 있습니다 — 아래 절을 보세요.라이브러리 비용 — 실측 (2026-09-13)
「손으로 만들기 전에 대안을 찾지 않았다」는 지적에 대한 조사 결과입니다. Vite + Svelte 5.57 프로브를 만들어 실제로 번들했습니다(gzip, Svelte 런타임 베이스라인 16.1 KB 제외).
d3-shape(곡선만)@tanstack/svelte-queryd3-shape+d3-scalelayercake8.4uPlot1.xchart.js(tree-shaken)@observablehq/plotlayerchart2.5 (/svg서브패스)두 가지가 뒤집혔습니다.
layerchart는 배럴 탓이 아니었습니다. 처음 +132.5 KB가 나왔을 때export *트리셰이킹 실패로 보고 넘겼는데,layerchart/svg서브패스로 좁혀도 오히려 더 큽니다(148 KB, 정적 클로저 32청크). 게다가 스타일이@layerstack/tailwind에 묶여 PandastrictTokens위에 얹을 수 없습니다css()로 못 주니 CSS 변수를 읽어 주입하고 테마 전환마다 다시 그려야 하는데, 이 저장소는 다이어그램 SVG까지 전부 토큰에 연결하는 금지선이 있습니다.check-seo·check-bundle도 canvas 내용은 못 봅니다recharts 129 KB가 이 대시보드에서 실제로 하는 계산은 2.3 KB입니다. recharts의
victory-vendor가 d3-shape/d3-scale을 벤더링한 것이고,type="monotone"은curveMonotoneX그 자체입니다.라이브러리 교체 비용의 분해
recharts를 쪼개면 셋으로 갈립니다. 이 축이 「무엇이 비쌌는가」를 설명합니다.
d3-shape·d3-scale)ResponsiveContainer)bind:clientWidth<XAxis/>·<Area/>)셋째가 가치는 가장 작고 번들은 가장 큽니다. 같은 분해를
nuqs에 대면 셋째가 거의 0이라 73줄로 끝났고,motion은 셋째가 아예 없어서 패키지가 그대로 갈 수 있었습니다. 분해가 곧 비용 예측입니다. 전문은 이 코멘트.남은 오염 — 무엇이 정리됐고 무엇이 아닌가
recharts↔ 직접 그린 SVG@tanstack/react-query↔ 69줄 스토어nuqs↔ 73줄 순수 함수sveltekit-search-params4.0.0을 재지 않았습니다motion↔svelte/transitionmotion은 프레임워크 중립이라 같은 패키지를 쓸 수 있는데 쓰지 않았습니다. 현재svelte/transition의fly·fade·scale을 씁니다 — 스프링과 다른 움직임입니다HTML에서도 갈립니다 — 하이드레이션 페이로드
글 상세 HTML을 페이로드와 마크업으로 갈라 재면:
마크업 자체는 13.2 대 12.0으로 거의 같습니다 — 픽셀 파리티가 다른 각도에서 한 번 더 확인됩니다. 차이 14 KB는 전부 RSC flight 페이로드입니다.
#406 시점 대비 변화
//posts//404//admin/#408이 d3-shape(+2.3 KB)와 스파크라인 채움 경로를 넣은 결과입니다. admin의 +3.3 KB가 그 실제 비용이고, 공개 페이지도 0.8 KB씩 늘었습니다 — d3-shape가 샌 것은 아닙니다(
check-bundle규칙 12번이curveMonotoneX가 어느 페이지 첫 로드에도 없음을 강제하고 통과합니다). 청크가 하나 늘면서 SvelteKit 매니페스트가 커진 것으로 보이나 확인하지 않은 추정입니다.아직 못 잰 것
프레임워크 중립성 — 가정이 어떻게 판명됐나
@blog/content로더·공개 판정·URL 계약node:fs를 함께 열어, 클라이언트 그래프에서 빈 스텁으로 externalize됐습니다(빌드는 성공하고 런타임에만 깨집니다). 클라이언트 문@blog/content/client를 새로 냈습니다@blog/content빌드 스크립트blog-content하나로 모았습니다check-seocheck-bundle/_next/static/chunks/가 경로에 박혀 있어 SvelteKit 산출물에서 청크를 하나도 못 찾았습니다. 음성 검사만 있었다면 규칙 전부가 "누수 없음"으로 통과했겠지만, 양성 대조(requiredIn)가 #328(2026-08-27)부터 규칙마다 필수라 전 규칙이 marker-dead로 실패하며 드러났습니다. 경로 관례를 걷어냈습니다(#399). 정정: 이전 기록의 "규칙마다 양성 대조를 필수로 만들었다"는 틀렸습니다 — 필수화는 이 실험 전의 일입니다blog-presetjsxFramework없이css()함수만 씁니다@design-system/ui컴포넌트react-markdown·react-syntax-highlighter·@giscus/react)은 React 바인딩일 뿐이라 벗겨 내면 같은 엔진(unified·refractor·giscus 스크립트)을 그대로 씁니다. 넷(react-query·recharts·nuqs·motion)은 Svelte 대안이 있는데 찾아보지 않고 손으로 만들었습니다 — 뒤늦게 전부 실측했고(위 표), 그중recharts만 "대안이 더 나쁘다"가 확인됐습니다예상 밖의 결과
content.values.mts에 묶여 있어@blog/site-values패키지로 뺐습니다react·next/import가 0이었습니다. 원래 중립이었고 앱에 묶여 있던 것은 env를 읽어 클라이언트를 만드는 일 하나였습니다 →@blog/analytics@blog/diagram. 두 앱이 같은 좌표를 쓰므로 같은 원고가 픽셀 단위로 같은 그림이 됩니다lib/server2,248 +lib/shared654 +lib/domain·platform151). boundaries 린트가 강제한 결과고, 그만큼은 다음 프레임워크로 그대로 갑니다이 실험이 실제로 가르쳐 준 것
계획에 없던 발견들입니다. 깨진 가정 자체가 산출물이라는 방침에 따라 남깁니다.
check-seo·check-bundle·테스트가 전부 초록인 채로 여섯 페이지가 자리만 잡은 화면이었습니다. 12개 PR 동안 드러나지 않았습니다Rail.test.tsx의test('기본 폭은 text다')를 이식했다면 레일 폭 버그는 없었을 것입니다CSS.getPlatformFontsForNode)로만 드러났습니다. Pretendard 때는 한글 폴백의 폭이 달라 줄바꿈이 어긋나며 드러났습니다motion의 스프링을svelte/transition의 280msfly로 바꿔 놓고 픽셀 파리티를 달성했다고 적었습니다. 다른 움직임입니다/admin은 로그인 게이트 뒤라 배포된 프리뷰에서 열리지 않고,check-parity의PARITY_PAGES5개에도 없습니다 — 두 검증 장치 어느 쪽도 보지 않는 화면입니다. 그 틈에서AreaChart가preserveAspectRatio="none"으로 viewBox를 가로로만 늘려 축 글자가 뭉개지고 커서 점이 타원인 채로 살아남았습니다. 빌드도check-seo도check-bundle도 전부 통과하고 있었습니다(#408에서 수정 + 좌표 스냅샷으로 잠금)geometry.ts101줄(순수 함수 + 테스트)에서도, 마크업 250줄에서도 버그가 안 났습니다. 난 곳은 실제 폭 측정·곡선 보간·라벨 겹침 — 전부 브라우저에서만 드러나고 순수 함수로 안 떨어지는 축입니다. 라이브러리가 파는 것의 상당 부분이 사실 이쪽이고, 직접 만들면 이 축이 구조적으로 빕니다react-markdown이 렌더할 때 코드 블록 노드를 컴포넌트로 바꿔(CodeBlock.tsx—language === 'mermaid'면<MermaidLazy>) 그림이 트리의 제자리에 들어갑니다. 무엇을 바꿀지 고르는 코드가 없습니다. Svelte 판은 본문을{@html}문자열로 넣어 그 안에 컴포넌트를 둘 수 없으므로,Mermaid.svelte가 마운트 뒤 DOM을 찾아replaceWith로 바꿉니다. 바꿀 대상은 다른 파일(codeBlock.ts)이 정한 마크업 모양에 대한 가정이었고, 그 가정이 틀려 본문 전체가 그림 하나로 대체됐습니다(fix(blog-web-svelte): mermaid 그림이 글 본문 전체를 대신하지 않게 한다 #413). 그림 그리기는 두 판 모두 같은mermaid.render입니다 — 라이브러리의 문제가 아니라 10번 분해의 컴포넌트 조립 조각을 손으로 대신한 자리입니다. 빌드 타임 굽기를 택한 이유(load데이터가 하이드레이션용으로 HTML에 한 번 더 직렬화된다)는apps/blog/web-svelte/README.md에 있습니다배포 뒤 발견한 결함
mermaid가 있는 글의 본문이 사라졌다 — #413에서 수정
/posts/payment-system-architecture/가 헤더와 썸네일만 보인다는 제보#post-content가 없고, 그 자리에 mermaid 그림 상자 하나만 남는다codeBlock.ts는 mermaid 펜스를 상자로 감싸지 않는다(<pre><code class="language-mermaid">가 본문 바로 아래).Mermaid.svelte는<div><pre><code>모양을 가정하고closest('pre')?.parentElement를 바꿨다 → 그 부모가#post-contentMermaid.svelte가 들어온 #401(4bf2cae)부터 #413 머지(f3340b2)까지payment-system-architecture·next-js-ecs-deploy· 번들러 2·3·4편. React 판은 무관<pre>하나만 바꾼다. 서버 쪽 테스트에 "mermaid 펜스는 상자 없이<pre>로 나간다"를 잠갔다 — 이 단언은 수정 전에도 통과한다. 클라이언트 버그는 이 앱의 node 테스트 환경에서 재현되지 않는다#post-content의 h2·p 수를 서버 HTML과 대조 — 로컬 빌드에서 글 44편 전부, PR 프리뷰 배포본에서 mermaid 글 5편. 그림 8개가 서버의 mermaid<pre>수와 같다마운트 뒤 DOM을 만지거나 읽는 다른 조각은 동작을 확인하지 않았습니다. 이번 대조는 "본문이 남는가"만 봤습니다.
src/lib/client/CopyCode.svelte—closest('figure')로 코드를 찾는다src/lib/client/ImageZoom.sveltesrc/lib/client/Toc.svelte·MobileToc.svelte— 헤딩 id로 요소를 찾는다스택 PR 계획 → 실제 진행
원래 6단계를 계획했으나 14개로 쪼개졌습니다.
check-bundle규칙성공 기준
목표 수치는 지금 정하지 않습니다. 1번 PR에서 React 쪽 기준선을 먼저 재고, 그 숫자를 본 뒤 목표를 정해 이 이슈에 추가합니다. 지금 지어낸 숫자보다 정직합니다.
측정 항목(양쪽 동일 조건, 프로덕션 빌드 기준):
/404/초기 JS gzip 크기 — 오염이 가장 적은 지표 → 212.7 KB → 49.6 KB (23.3%)완료 기준
apps/blog/web-svelte가pnpm build --filter=@blog/web-svelte로 정적 export 된다apps/blog/posts의 글 70편이 원고 수정 없이 그대로 렌더된다code-tabs는 상호작용이 필요해 제외blog-content check-seo가 수정 없이 통과한다check-bundle이 이 앱의BUNDLE_GUARDS선언으로 통과한다 — 규칙 12개pnpm lint --filter=@blog/web-svelte가--max-warnings=0으로 통과하고 인라인 disable이 0건이다$lib/server강제가eslint-plugin-boundaries를 대신합니다(도구가 막으므로 빌드가 실패합니다)blog-preset토큰 경유)lint·check-types·test에 편입돼 PR CI가 두 앱을 함께 검사한다README.md·CLAUDE.md·AGENTS.md에 새 워크스페이스가 반영된다apps/blog/web은 한 줄도 바뀌지 않는다 — 예외 조항대로 중립화 수정만 들어갔고(@blog/site-values·@blog/analytics·@blog/diagram추출), React 앱 테스트 전량이 통과합니다나중에 추가한 기준 (이번 실험이 가르쳐 준 것)
blog-content check-parity. 클래스 어휘·헤딩·링크·요소 수·아이콘 선언 다섯 축을 HTML만 읽고 대조한다/admin이 로그인 게이트와PARITY_PAGES양쪽의 사각지대였습니다. 차트 좌표를 인라인 스냅샷으로 잠가(snapshot.test.ts) 회귀가 조용히 지나가지 않게 했습니다테스트 시나리오
계약·회귀 테스트만 이식합니다. 유닛 테스트 전량 이식은 하지 않습니다.
check-seo규칙 전부 통과 — h1 1개, description 중복·길이,<title>60자, canonical 자기참조, og 태그, img alt, 산출물↔발행 글 정합성.이 든 slug(turborepo-next.js-docker·vue-3.0) 처리draft·scheduled글이 프로덕션 빌드에서 빠지고 dev에서는 배지와 함께 보인다html[data-theme]를 세팅한다/admin이 시각 검증 경로가 없으므로 계산을 대신 잠근다apps/blog/web의 기존 테스트 전량이 계속 통과한다.pnpm build --filter=@blog/web이 계속 통과한다범위 밖
apps/blog/web교체·제거 — 최종 배포 전환은 비교 결과를 보고 별도로 판단합니다. 이 이슈는 비교 재료를 만드는 데까지입니다apps/react·apps/next.js등 다른 실험 앱 — 손대지 않습니다/admin이 글 관리 화면(509줄)이고 대시보드가/admin/analytics인데, Svelte는/admin이 대시보드입니다. 그래서 ReactTopPostsTable의 행별 스파크라인에 대응하는 자리가 Svelte에 없습니다. 컴포넌트 문제가 아니라 구조 문제라 별도 판단입니다