블로그 작성 글을 마크다운으로 결정하고서, 어떻게 렌더링하는게 좋을지 고민했다. Nextjs는 기본적으로 mdx를 지원해주고 있어서 .mdx파일로 읽을 수가 있다.
근데 고정된 페이지가 아니라 동적으로, 원격으로 작성할 수 있어야하고 수정도 해야하는데, 그러면 결국 서버에 작성해야하고, 내용을 fetch해서 렌더링 해야한다.
그래서 mdx는 접어두고, 공식문서에 작성된 next-mdx-remote/rsc를 적용했었다.
근데, 23년 3월 업데이트 이후로 현재(23년 10월)까지 업데이트가 이루어 지지 않았다. remark-gfm 플러그인은 에러가 나고 불안정한 모습을 보여준다.
직접 플러그인을 짤까 하다가 다행히 react-markdown가 업데이트(23년 9월 말, react18버전에 대응) 되어서 사용하게 되었다.
Strapi에서 필드를 Rich Text(Markdown)으로 추가해서 글을 작성 할 수 있다. 이걸로 등록한다고 한들 다른 처리가 이루어지는건 아니고 본질은 그냥 text이다.
Markdown을 처리하는 과정은 text의 원본 텍스트를 추상 구문 트리(Abstract Syntax Tree)로 변환 후 원하는 형태로 출력하는 과정이 필요하다.
해당 과정을 처리해주는 라이브러리로 unified가 유명한데,
그중에 특히 마크다운에 특화된 라이브러리로는 remark(unified), marked, markdown-it등이 있다.
- remark를 사용해서 컨텐츠 내용을 400자 이하로 줄이는 기능의 예제
import { remark } from "remark";
import unlink from "remark-unlink"; // 링크 제거
import strip from "strip-markdown"; // 마그다운 스타일 형식 제거
function stripMarkdownText(markdownText: string) {
markdownText = markdownText.replace(/\[.*\]/g, "[]");
markdownText = remark()
.use(unlink)
.use(strip as any) // 플러그인이 typescript에 대응하지 못하는게 많다.
.processSync(markdownText)
.toString();
markdownText =
markdownText.length > 400 ? markdownText.slice(0, 400) : markdownText;
return markdownText;
}
interface Props {
text: string;
}
export default function MDXSummary({ text }: Props) {
return <div className="line-clamp-4">{stripMarkdownText(text)}</div>;
}
위와 같이 마크다운에서 원하는 형태로 편집하거나,
const ast = remark().parse(markdownText);
AST(추상 구문 트리)를 추출한 후에 원하는 형태로 가공해서 쓸 수 있다.
서론에 상술했듯 react-markdown을 이용한다.
markdown으로 렌더링 된 html은 @tailwindcss/typography를 설치하여 적용했다.
remark-gfm은 깃헙에서 사용하는 markdown확장 형식 플러그인이다.
markdown 관련 모듈들은 대부분 remark를 사용하며, remark 플러그인과 rehype플러그인을 붙일 수 있다.
- 모듈 설치
npm i -D @tailwindcss/typography react-markdown remark-gfm
- 마크다운 변환 예제
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
interface Props {
text: string;
}
export default async function MDXContent({ text }: Props) {
return (
<Markdown
remarkPlugins={[remarkGfm]}
className={`prose prose-sm prose-slate w-full max-w-full md:prose-base lg:prose-lg`}
>
{text}
</Markdown>
);
}
이제 코드를 이쁘게 보여 주기 위해서 syntax-highlighter를 설정 했다. remark-prism 같은 remark 플러그인으로 처리할 수도 있지만,
좀더 제어가 용이한 react-syntax-highlighter을 이용한다.
- Code를 감싸는 부분, 어떤 코드인지 여부와, 코드 복사 기능
import { getIcon } from "@/utils/syntax";
import { ClassAttributes, HTMLAttributes, ReactElement } from "react";
import { BiClipboard } from "react-icons/bi";
import { ExtraProps } from "react-markdown";
export default function Pre(
props: ClassAttributes<HTMLPreElement> &
HTMLAttributes<HTMLPreElement> &
ExtraProps,
) {
const language = (props?.children as ReactElement)?.props?.className?.replace(
"language-",
"",
);
const Icon = getIcon(language!);
if (!language) return <pre {...props} />;
return (
<div data-code="" className="flex flex-col rounded-lg bg-[#1e1e1e]">
<div className="flex items-center justify-between px-4 py-2">
<div className="flex items-center gap-4 text-gray-400">
<span className="text-lg">{Icon}</span>
<span className="text-xs">{language}</span>
</div>
<button
data-clipboard=""
className="text-lg text-gray-400 active:text-green-500"
>
<BiClipboard />
</button>
</div>
{props.children}
</div>
);
}
- react-syntax-highlighter를 적용, 테마는 prism형식을 적용.
import { getCurrentLanguage } from "@/utils/syntax";
import { ClassAttributes, HTMLAttributes } from "react";
import { ExtraProps } from "react-markdown";
import SyntaxHighlighter from "react-syntax-highlighter/dist/esm/prism"; // react-syntax-highlighter는 패키지 사이즈가 커져서 나중에는 다른걸로 바꿨다.
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
export default function Code(
props: ClassAttributes<HTMLElement> &
HTMLAttributes<HTMLElement> &
ExtraProps,
) {
const currentLanguage = getCurrentLanguage(
props.className?.replace("language-", ""),
);
if (!currentLanguage) return <code {...props} />;
return (
<SyntaxHighlighter
language={currentLanguage}
showLineNumbers={true}
style={vscDarkPlus}
lineNumberStyle={{
color: "rgba(255, 255, 255, 0.5)",
fontSize: "0.8rem",
marginRight: "1rem",
}}
>
{String(props.children)}
</SyntaxHighlighter>
);
}
코드 시작 구문에 다음과 같이 입력하면
```typescript
props.className에 language-typescript로 받을 수 있다.
이렇게 만들어둔 컴포넌트를 Markdown의 components속성으로 넘기면 된다.
\\ 중략
<Markdown
remarkPlugins={[remarkGfm]}
className={`prose prose-sm prose-slate w-full max-w-full md:prose-base lg:prose-lg`}
components={{
pre: Pre,
code: Code,
}}
>
{text}
</Markdown>