diff --git a/docs/markdown-charts.md b/docs/markdown-charts.md index 8f26350..e9945f1 100644 --- a/docs/markdown-charts.md +++ b/docs/markdown-charts.md @@ -33,6 +33,7 @@ - 서버 HTML과 최초 hydration에는 제목·단위·수치 표가 동일하게 렌더된다. 검색엔진과 스크립트를 사용할 수 없는 환경도 값을 읽을 수 있다. - 기본 선택은 차트다. 화면 200px 근처에 도달했을 때 차트 모듈을 가져온다. 차트 로딩 중·실패 시 표를 유지하고 실패 시 재시도 버튼을 제공한다. +- 코드·다이어그램과 기존 benchmark 모듈의 다운로드 실패도 글 전체 오류로 전파하지 않는다. 원본 코드와 본문을 유지하고 해당 블록에 재시도 버튼을 표시한다. - `표`·`차트` 전환은 shadcn ToggleGroup을 사용한다. 키보드로 조작 가능하며 툴팁을 사용하지 않아도 표에서 모든 수치와 조건을 확인할 수 있다. - 막대 축은 0부터 시작한다. 계열 색은 라이트·다크 모드에 맞춰 바뀐다. 불완전 주차의 별표·사선 표시는 표와 차트 모두 유지한다. - 초기 차트 애니메이션은 사용하지 않는다. 차트와 표는 좁은 본문 너비에 맞춰 표시하며 넓은 표만 자체 가로 스크롤을 허용한다. diff --git a/src/components/CodeBlock.tsx b/src/components/CodeBlock.tsx index 952ac1f..7b6b1b6 100644 --- a/src/components/CodeBlock.tsx +++ b/src/components/CodeBlock.tsx @@ -1,12 +1,26 @@ -import { lazy, Suspense, type ComponentPropsWithoutRef, useCallback, useEffect, useId, useRef, useState } from "react" +import { type ComponentPropsWithoutRef, useCallback, useEffect, useId, useRef, useState } from "react" import { Copy, Check } from "lucide-react" import { Button } from "@/components/ui/button" import { useT } from "@/i18n" import { useTheme } from "@/hooks/useTheme" import mermaid from "mermaid" import { readMarkdownCode } from "@/lib/markdown-code" +import { createRetryableLoader } from "@/lib/async-loader" +import { useDeferredModule } from "@/hooks/useDeferredModule" -const BenchmarkChart = lazy(() => import("@/components/BenchmarkChart").then((module) => ({ default: module.BenchmarkChart }))) +const loadBenchmark = createRetryableLoader(() => import("@/components/BenchmarkChart")) + +function DeferredBenchmark({ code, children, ...props }: ComponentPropsWithoutRef<"pre"> & { code: string }) { + const { value: module, error, retry } = useDeferredModule(loadBenchmark) + const t = useT() + if (module) return + return <> + {error &&
+

{t.common.chartLoadError}

+
} +
{children}
+ +} function getCssHex(varName: string): string { const temp = document.createElement("div") @@ -311,7 +325,7 @@ export function CodeBlock({ children, ...props }: ComponentPropsWithoutRef<"pre" } if (language === "benchmark") { - return {children}}> + return {children} } return {children} diff --git a/src/i18n/translations.ts b/src/i18n/translations.ts index 57416f8..aeaeb2f 100644 --- a/src/i18n/translations.ts +++ b/src/i18n/translations.ts @@ -208,6 +208,7 @@ export interface Translations { last30Days: string tableOfContents: string copyCode: string + codeLoadError: string chartView: string chartTable: string chartViewLabel: string @@ -444,6 +445,7 @@ export const ko: Translations = { last30Days: "최근 30일", tableOfContents: "목차", copyCode: "코드 복사", + codeLoadError: "코드·다이어그램을 불러오지 못했습니다. 원본 내용을 표시합니다.", chartView: "차트", chartTable: "표", chartViewLabel: "보기 방식", @@ -680,6 +682,7 @@ export const en: Translations = { last30Days: "Last 30 Days", tableOfContents: "Table of Contents", copyCode: "Copy code", + codeLoadError: "Could not load the code or diagram viewer. Showing the original content.", chartView: "Chart", chartTable: "Table", chartViewLabel: "View", diff --git a/src/pages/PostPage.tsx b/src/pages/PostPage.tsx index 1b39b75..dd52c8f 100644 --- a/src/pages/PostPage.tsx +++ b/src/pages/PostPage.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense, type ComponentPropsWithoutRef, useEffect, useSyncExternalStore } from "react" +import { type ComponentPropsWithoutRef, useEffect } from "react" import { useParams, Link } from "react-router-dom" import ReactMarkdown from "react-markdown" import remarkGfm from "remark-gfm" @@ -50,10 +50,10 @@ import { StructuredData } from "@/components/StructuredData" import { postStructuredData } from "@/lib/structured-data" import { MarkdownChart } from "@/components/MarkdownChart" import { readMarkdownCode } from "@/lib/markdown-code" +import { createRetryableLoader } from "@/lib/async-loader" +import { useDeferredModule } from "@/hooks/useDeferredModule" -const LazyCodeBlock = lazy(() => - import("@/components/CodeBlock").then((module) => ({ default: module.CodeBlock })) -) +const loadCodeBlock = createRetryableLoader(() => import("@/components/CodeBlock")) function localizeMarkdownLink(href: string, language: Language) { if (/^\/(posts|tags|series|search|analytics|about)(\/|[?#]|$)/.test(href)) { @@ -62,19 +62,6 @@ function localizeMarkdownLink(href: string, language: Language) { return href } -function subscribeHydration(callback: () => void) { - const timer = window.setTimeout(callback, 0) - return () => window.clearTimeout(timer) -} - -function getClientSnapshot() { - return true -} - -function getServerSnapshot() { - return false -} - function CodeBlockFallback({ children, ...props }: ComponentPropsWithoutRef<"pre">) { return (
) {
 }
 
 function HydratedCodeBlock(props: ComponentPropsWithoutRef<"pre">) {
-  const isClient = useSyncExternalStore(subscribeHydration, getClientSnapshot, getServerSnapshot)
-
-  if (!isClient) {
-    return 
-  }
-
+  const { value: module, error, retry } = useDeferredModule(loadCodeBlock)
+  const { t } = useLanguage()
+  if (module) return 
   return (
-    }>
-      
-    
+    <>
+      {error && 
+

{t.components.codeLoadError}

+ +
} + + ) }