Skip to content

Commit bb1e955

Browse files
committed
fix(blog): 코드 모듈 로딩 실패 시 본문 보존
1 parent 1502db2 commit bb1e955

4 files changed

Lines changed: 35 additions & 29 deletions

File tree

‎docs/markdown-charts.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
- 서버 HTML과 최초 hydration에는 제목·단위·수치 표가 동일하게 렌더된다. 검색엔진과 스크립트를 사용할 수 없는 환경도 값을 읽을 수 있다.
3535
- 기본 선택은 차트다. 화면 200px 근처에 도달했을 때 차트 모듈을 가져온다. 차트 로딩 중·실패 시 표를 유지하고 실패 시 재시도 버튼을 제공한다.
36+
- 코드·다이어그램과 기존 benchmark 모듈의 다운로드 실패도 글 전체 오류로 전파하지 않는다. 원본 코드와 본문을 유지하고 해당 블록에 재시도 버튼을 표시한다.
3637
- `표`·`차트` 전환은 shadcn ToggleGroup을 사용한다. 키보드로 조작 가능하며 툴팁을 사용하지 않아도 표에서 모든 수치와 조건을 확인할 수 있다.
3738
- 막대 축은 0부터 시작한다. 계열 색은 라이트·다크 모드에 맞춰 바뀐다. 불완전 주차의 별표·사선 표시는 표와 차트 모두 유지한다.
3839
- 초기 차트 애니메이션은 사용하지 않는다. 차트와 표는 좁은 본문 너비에 맞춰 표시하며 넓은 표만 자체 가로 스크롤을 허용한다.

‎src/components/CodeBlock.tsx‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,26 @@
1-
import { lazy, Suspense, type ComponentPropsWithoutRef, useCallback, useEffect, useId, useRef, useState } from "react"
1+
import { type ComponentPropsWithoutRef, useCallback, useEffect, useId, useRef, useState } from "react"
22
import { Copy, Check } from "lucide-react"
33
import { Button } from "@/components/ui/button"
44
import { useT } from "@/i18n"
55
import { useTheme } from "@/hooks/useTheme"
66
import mermaid from "mermaid"
77
import { readMarkdownCode } from "@/lib/markdown-code"
8+
import { createRetryableLoader } from "@/lib/async-loader"
9+
import { useDeferredModule } from "@/hooks/useDeferredModule"
810

9-
const BenchmarkChart = lazy(() => import("@/components/BenchmarkChart").then((module) => ({ default: module.BenchmarkChart })))
11+
const loadBenchmark = createRetryableLoader(() => import("@/components/BenchmarkChart"))
12+
13+
function DeferredBenchmark({ code, children, ...props }: ComponentPropsWithoutRef<"pre"> & { code: string }) {
14+
const { value: module, error, retry } = useDeferredModule(loadBenchmark)
15+
const t = useT()
16+
if (module) return <module.BenchmarkChart data={code} />
17+
return <>
18+
{error && <div className="not-prose flex flex-wrap items-center gap-3 text-sm text-muted-foreground" role="alert">
19+
<p>{t.common.chartLoadError}</p><Button variant="outline" size="sm" onClick={retry}>{t.common.retry}</Button>
20+
</div>}
21+
<pre {...props}>{children}</pre>
22+
</>
23+
}
1024

1125
function getCssHex(varName: string): string {
1226
const temp = document.createElement("div")
@@ -311,7 +325,7 @@ export function CodeBlock({ children, ...props }: ComponentPropsWithoutRef<"pre"
311325
}
312326

313327
if (language === "benchmark") {
314-
return <Suspense fallback={<pre {...props}>{children}</pre>}><BenchmarkChart data={code} /></Suspense>
328+
return <DeferredBenchmark code={code} {...props}>{children}</DeferredBenchmark>
315329
}
316330

317331
return <ShikiBlock code={code} language={language} preProps={props}>{children}</ShikiBlock>

‎src/i18n/translations.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ export interface Translations {
208208
last30Days: string
209209
tableOfContents: string
210210
copyCode: string
211+
codeLoadError: string
211212
chartView: string
212213
chartTable: string
213214
chartViewLabel: string
@@ -444,6 +445,7 @@ export const ko: Translations = {
444445
last30Days: "최근 30일",
445446
tableOfContents: "목차",
446447
copyCode: "코드 복사",
448+
codeLoadError: "코드·다이어그램을 불러오지 못했습니다. 원본 내용을 표시합니다.",
447449
chartView: "차트",
448450
chartTable: "표",
449451
chartViewLabel: "보기 방식",
@@ -680,6 +682,7 @@ export const en: Translations = {
680682
last30Days: "Last 30 Days",
681683
tableOfContents: "Table of Contents",
682684
copyCode: "Copy code",
685+
codeLoadError: "Could not load the code or diagram viewer. Showing the original content.",
683686
chartView: "Chart",
684687
chartTable: "Table",
685688
chartViewLabel: "View",

‎src/pages/PostPage.tsx‎

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { lazy, Suspense, type ComponentPropsWithoutRef, useEffect, useSyncExternalStore } from "react"
1+
import { type ComponentPropsWithoutRef, useEffect } from "react"
22
import { useParams, Link } from "react-router-dom"
33
import ReactMarkdown from "react-markdown"
44
import remarkGfm from "remark-gfm"
@@ -50,10 +50,10 @@ import { StructuredData } from "@/components/StructuredData"
5050
import { postStructuredData } from "@/lib/structured-data"
5151
import { MarkdownChart } from "@/components/MarkdownChart"
5252
import { readMarkdownCode } from "@/lib/markdown-code"
53+
import { createRetryableLoader } from "@/lib/async-loader"
54+
import { useDeferredModule } from "@/hooks/useDeferredModule"
5355

54-
const LazyCodeBlock = lazy(() =>
55-
import("@/components/CodeBlock").then((module) => ({ default: module.CodeBlock }))
56-
)
56+
const loadCodeBlock = createRetryableLoader(() => import("@/components/CodeBlock"))
5757

5858
function localizeMarkdownLink(href: string, language: Language) {
5959
if (/^\/(posts|tags|series|search|analytics|about)(\/|[?#]|$)/.test(href)) {
@@ -62,19 +62,6 @@ function localizeMarkdownLink(href: string, language: Language) {
6262
return href
6363
}
6464

65-
function subscribeHydration(callback: () => void) {
66-
const timer = window.setTimeout(callback, 0)
67-
return () => window.clearTimeout(timer)
68-
}
69-
70-
function getClientSnapshot() {
71-
return true
72-
}
73-
74-
function getServerSnapshot() {
75-
return false
76-
}
77-
7865
function CodeBlockFallback({ children, ...props }: ComponentPropsWithoutRef<"pre">) {
7966
return (
8067
<pre
@@ -93,16 +80,17 @@ function MarkdownCodeBlock(props: ComponentPropsWithoutRef<"pre">) {
9380
}
9481

9582
function HydratedCodeBlock(props: ComponentPropsWithoutRef<"pre">) {
96-
const isClient = useSyncExternalStore(subscribeHydration, getClientSnapshot, getServerSnapshot)
97-
98-
if (!isClient) {
99-
return <CodeBlockFallback {...props} />
100-
}
101-
83+
const { value: module, error, retry } = useDeferredModule(loadCodeBlock)
84+
const { t } = useLanguage()
85+
if (module) return <module.CodeBlock {...props} />
10286
return (
103-
<Suspense fallback={<CodeBlockFallback {...props} />}>
104-
<LazyCodeBlock {...props} />
105-
</Suspense>
87+
<>
88+
{error && <div className="not-prose flex flex-wrap items-center gap-3 text-sm text-muted-foreground" role="alert">
89+
<p>{t.components.codeLoadError}</p>
90+
<Button variant="outline" size="sm" onClick={retry}>{t.common.retry}</Button>
91+
</div>}
92+
<CodeBlockFallback {...props} />
93+
</>
10694
)
10795
}
10896

0 commit comments

Comments
 (0)