Skip to content

Commit 7729920

Browse files
committed
fix: MDX렌더링이 실패한 후, 외부에서 재시도를 하지 못하던 문제 수정
1 parent 87b6ef0 commit 7729920

File tree

5 files changed

+69
-53
lines changed

5 files changed

+69
-53
lines changed

apps/pyconkr/src/debug/page/mdi_test.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import React from "react";
22

33
import { Box, Button, Card, CardContent, TextField, Typography } from "@mui/material";
4+
import type { MDXComponents } from "mdx/types.js";
45

56
import * as Common from "@frontend/common";
7+
import * as Shop from "@frontend/shop";
68

79
const LOCAL_STEORAGE_KEY = "mdi_test_input";
810
const MDX_TEST_STRING = `\
@@ -74,6 +76,16 @@ HTML 태그 중 일부를 사용할 수 있어요!
7476
*/ }
7577
`
7678

79+
const PyConMDXComponents: MDXComponents = {
80+
Shop__Common__PriceDisplay: Shop.Components.Common.PriceDisplay,
81+
Shop__Common__SignInGuard: Shop.Components.Common.SignInGuard,
82+
Shop__Common__ContextProvider: Shop.Components.Common.ShopContextProvider,
83+
Shop__Feature__CartStatus: Shop.Components.Features.CartStatus,
84+
Shop__Feature__ProductList: Shop.Components.Features.ProductList,
85+
Shop__Feature__OrderList: Shop.Components.Features.OrderList,
86+
Shop__Feature__UserInfo: Shop.Components.Features.UserInfo,
87+
}
88+
7789
const getMdxInputFromLocalStorage: () => string = () => {
7890
const input = localStorage.getItem(LOCAL_STEORAGE_KEY);
7991
return input ? input : "";
@@ -86,20 +98,25 @@ const setMdxInputToLocalStorage: (input: string) => string = (input) => {
8698

8799
export const MdiTestPage: React.FC = () => {
88100
const inputRef = React.useRef<HTMLTextAreaElement>(null);
89-
const [mdxInput, setMdxInput] = React.useState(getMdxInputFromLocalStorage());
101+
const [state, setState] = React.useState<{ text: string, resetKey: string }>({
102+
text: getMdxInputFromLocalStorage(),
103+
resetKey: window.crypto.randomUUID()
104+
});
105+
106+
const setMDXInput = (text: string) => setState({ text: setMdxInputToLocalStorage(text), resetKey: window.crypto.randomUUID() });
90107

91108
return (
92109
<Box sx={{ p: 3 }}>
93110
<Typography variant="h5" gutterBottom>MDX 에디터</Typography>
94-
<TextField inputRef={inputRef} defaultValue={mdxInput} multiline fullWidth minRows={4} sx={{ my: 2 }} />
95-
<Button variant="contained" onClick={() => inputRef.current && setMdxInput(setMdxInputToLocalStorage(inputRef.current.value))}>변환</Button>
111+
<TextField inputRef={inputRef} defaultValue={state.text} multiline fullWidth minRows={4} sx={{ my: 2 }} />
112+
<Button variant="contained" onClick={() => inputRef.current && setMDXInput(inputRef.current.value)}>변환</Button>
96113
&nbsp;
97-
<Button variant="contained" onClick={() => setMdxInput(MDX_TEST_STRING)}>테스트용 Help Text 로딩</Button>
114+
<Button variant="contained" onClick={() => setMDXInput(MDX_TEST_STRING)}>테스트용 Help Text 로딩</Button>
98115
<br />
99116
<br />
100117
<Card>
101118
<CardContent>
102-
<Common.Components.MDXRenderer text={mdxInput} />
119+
<Common.Components.MDXRenderer {...state} components={PyConMDXComponents} />
103120
</CardContent>
104121
</Card>
105122
</Box>

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@rollup/plugin-node-resolve": "^16.0.1",
4747
"@rollup/plugin-replace": "^6.0.2",
4848
"@tanstack/react-query-devtools": "^5.76.1",
49+
"@types/mdx": "^2.0.13",
4950
"@types/node": "^22.15.18",
5051
"@types/react": "^19.1.4",
5152
"@types/react-dom": "^19.1.5",

packages/common/src/components/error_handler.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Suspense } from "@suspensive/react";
66
import CommonContext from '../hooks/';
77

88
const DetailedErrorFallback: React.FC<{ error: Error, reset: () => void }> = ({ error, reset }) => {
9+
console.error(error);
910
const errorObject = Object.getOwnPropertyNames(error).reduce((acc, key) => ({ ...acc, [key]: (error as unknown as { [key: string]: unknown })[key] }), {});
1011
return <>
1112
<Typography variant="body2" color="error">error.message = {error.message}</Typography>

packages/common/src/components/mdx.tsx

Lines changed: 42 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,28 @@ import * as React from "react";
22
import * as runtime from "react/jsx-runtime";
33
import * as R from "remeda";
44

5-
import { evaluate, EvaluateOptions } from "@mdx-js/mdx";
5+
import { evaluate } from "@mdx-js/mdx";
6+
import * as provider from "@mdx-js/react";
67
import { CircularProgress } from "@mui/material";
7-
import { ErrorBoundary, Suspense } from "@suspensive/react";
8-
import { useSuspenseQuery } from "@tanstack/react-query";
9-
import components, { MuiMdxComponentsOptions } from 'mui-mdx-components';
8+
import { ErrorBoundary } from "@suspensive/react";
9+
import type { MDXComponents } from "mdx/types";
10+
import muiComponents from 'mui-mdx-components';
1011

11-
import Components from "../components";
1212
import Hooks from "../hooks";
13+
import { ErrorFallback } from "./error_handler";
1314

14-
const MDXComponents: MuiMdxComponentsOptions = {
15-
overrides: {
16-
'h1': (props) => <h1 {...props} />,
17-
'h2': (props) => <h2 {...props} />,
18-
'h3': (props) => <h3 {...props} />,
19-
'h4': (props) => <h4 {...props} />,
20-
'h5': (props) => <h5 {...props} />,
21-
'h6': (props) => <h6 {...props} />,
22-
'strong': (props) => <strong {...props} />,
23-
'em': (props) => <em {...props} />,
24-
'ul': (props) => <ul {...props} />,
25-
'ol': (props) => <ol {...props} />,
26-
'li': (props) => <li {...props} />,
27-
}
28-
}
29-
30-
const InnerMDXRenderer: React.FC<{ text: string, baseUrl: string }> = ({ text, baseUrl }) => {
31-
const options: EvaluateOptions = { ...runtime, baseUrl };
32-
33-
const { data } = useSuspenseQuery({
34-
queryKey: ["mdx", text],
35-
queryFn: async () => {
36-
const { default: RenderResult } = await evaluate(text, options);
37-
return <div className="markdown-body">
38-
<RenderResult components={components(MDXComponents)} />
39-
</div>
40-
},
41-
});
42-
43-
return <>{data}</>;
15+
const CustomMDXComponents: MDXComponents = {
16+
'h1': (props) => <h1 {...props} />,
17+
'h2': (props) => <h2 {...props} />,
18+
'h3': (props) => <h3 {...props} />,
19+
'h4': (props) => <h4 {...props} />,
20+
'h5': (props) => <h5 {...props} />,
21+
'h6': (props) => <h6 {...props} />,
22+
'strong': (props) => <strong {...props} />,
23+
'em': (props) => <em {...props} />,
24+
'ul': (props) => <ul {...props} />,
25+
'ol': (props) => <ol {...props} />,
26+
'li': (props) => <li {...props} />,
4427
}
4528

4629
const lineFormatterForMDX = (line: string) => {
@@ -55,20 +38,31 @@ const lineFormatterForMDX = (line: string) => {
5538
return `${trimmedLine} \n`;
5639
}
5740

58-
export const MDXRenderer: React.FC<{ text: string }> = ({ text }) => {
59-
// 원래 MDX는 각 줄의 마지막에 공백 2개가 있어야 줄바꿈이 되고, 또 연속 줄바꿈은 무시되지만,
60-
// 편의성을 위해 렌더러 단에서 공백 2개를 추가하고 연속 줄바꿈을 <br />로 변환합니다.
41+
export const MDXRenderer: React.FC<{ text: string; components?: MDXComponents, resetKey?: string }> = ({ text, components, resetKey }) => {
6142
const { baseUrl } = Hooks.Common.useCommonContext();
43+
const [state, setState] = React.useState<{ component: React.ReactNode, resetKey: string }>({
44+
component: <CircularProgress />,
45+
resetKey: window.crypto.randomUUID(),
46+
})
47+
48+
const setRenderResult = (component: React.ReactNode) => setState((prev) => ({ ...prev, component: component }));
49+
const setRandomResetKey = () => setState((prev) => ({ ...prev, resetKey: window.crypto.randomUUID() }))
6250

63-
const processedText = text
64-
.split("\n")
65-
.map(lineFormatterForMDX)
66-
.join("")
67-
.replaceAll("\n\n", "\n<br />\n");
51+
React.useEffect(() => {
52+
(
53+
async () => {
54+
try {
55+
// 원래 MDX는 각 줄의 마지막에 공백 2개가 있어야 줄바꿈이 되고, 또 연속 줄바꿈은 무시되지만,
56+
// 편의성을 위해 렌더러 단에서 공백 2개를 추가하고 연속 줄바꿈을 <br />로 변환합니다.
57+
const processedText = text.split("\n").map(lineFormatterForMDX).join("").replaceAll("\n\n", "\n<br />\n");
58+
const { default: RenderResult } = await evaluate(processedText, { ...runtime, ...provider, baseUrl });
59+
setRenderResult(<RenderResult components={muiComponents({ overrides: { ...CustomMDXComponents, ...components } })} />);
60+
} catch (error) {
61+
setRenderResult(<ErrorFallback error={error as Error} reset={setRandomResetKey} />);
62+
}
63+
}
64+
)();
65+
}, [text, resetKey, state.resetKey]);
6866

69-
return <ErrorBoundary fallback={Components.ErrorFallback}>
70-
<Suspense fallback={<CircularProgress />}>
71-
<InnerMDXRenderer text={processedText} baseUrl={baseUrl} />
72-
</Suspense>
73-
</ErrorBoundary>
67+
return <ErrorBoundary fallback={ErrorFallback} resetKeys={[text, resetKey, state.resetKey]}>{state.component}</ErrorBoundary>
7468
};

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)