본격적으로 마크다운을 사용해 보자 

이번에 사용할 라이브러리중 핵심이 되는 라이브러리는 다음과 같다

 

라이브러리 및 기반지식

 

1. next-mdx-remote

https://github.com/hashicorp/next-mdx-remote

 

GitHub - hashicorp/next-mdx-remote: Load mdx content from anywhere through getStaticProps in next.js

Load mdx content from anywhere through getStaticProps in next.js - hashicorp/next-mdx-remote

github.com

 

2. rehype-pretty-code

https://rehype-pretty-code.netlify.app/

 

Rehype Pretty Code

rehype-pretty-code is a Rehype plugin powered by the shiki syntax highlighter that provides beautiful code blocks for Markdown or MDX. It works on both the server at build-time (avoiding runtime syntax highlighting) and on the client for dynamic highlighti

rehype-pretty-code.netlify.app

3. remark-gfm

https://github.com/remarkjs/remark-gfm

 

GitHub - remarkjs/remark-gfm: remark plugin to support GFM (autolink literals, footnotes, strikethrough, tables, tasklists)

remark plugin to support GFM (autolink literals, footnotes, strikethrough, tables, tasklists) - remarkjs/remark-gfm

github.com

 

 

그리고 기반 지식으로 필요한건 

 

1. 마크다운이란 무엇인가 ? 

https://www.markdownguide.org/getting-started/#what-is-markdown

 

Getting Started | Markdown Guide

An overview of Markdown, how it works, and what you can do with it.

www.markdownguide.org

 

2. frontmatter

https://mystmd.org/guide/frontmatter

 

Frontmatter - MyST Markdown Guide

Frontmatter can be set at the top of your documents to change the look and feel of your articles.

mystmd.org

 

당연하지만 nextjs로 작성할 것이기 때문에 nextjs의 기본 개념과 약간의 js/ts지식이 필요하다 

 

흐름 및 파일 구성

 

흐름은 간단하다 

 

1. 소스파일을 얻어온다

2. 소스파일을 내가 커스터마이징한 mdx compile 함수로 html을 렌더링 한다 

3. 렌더링한 소스를 page에 넣어 그대로 렌더링 한다

 

 

그럼 여기서 필요한 것은 정해지게 되는데 

 

1. 소스는 어디서 얻어 올 것인가?

- 이번 프로젝트는 fs를 이용하여(서버사이드에서는 fs사용가능) 파일을 읽어 올 것이다

2. mdx compile함수는 어떻게 커스터마이징 하는가?

- 이후 글에서 설명한다 

3. 함수로 렌더링 한 소스는 어떻게 사용되는가?

- 이 또한 글에서 설명한다 

 

 

소스를 얻어오자

간단하게  fs모듈을 이용하여 지정된 path.. 더욱더 간단하게 하기 위해서 아예 public폴더에 mdx파일들을 몰아넣기로 한다

기준은 정해졌으니 함수를 하나 짠다

const getPostSource = async (postName: string) => {
    const filePath = path.join(process.cwd(), 'public', 'post', `${postName}.mdx`)
    try {
        return await fs.promises.readFile(filePath, 'utf8')
    } catch (error) {
        const cookieStore = cookies()
        const supabase = createClient(cookieStore)
        supabase.from('errors').insert({ error: JSON.stringify(error) })
    }
}

 

path로 public/post의 경로를 생성하고 postName기준으로 파일을 긁어온다

 

postName은 어디서나왔느냐고 ? 당연히 url에서 떼왔다. 다음과 같다

const RemoteMdxPage = async ({ params }: { params: { post: string } }) => {
    const source = (await getPostSource(params.post)) as MDXRemoteProps['source']
    // (...)
}

 

자연스럽게 params에서 떼왔다.

 

 

소스를 컴파일링 할 함수를 작성하자

이제 CustomMdx라는 함수를 하나 작성 할 것이다

왜 하냐..라고 생각하지 말자 이게 있어야 마크다운 파일을 콘텐츠로 뽑아낼 수 있다

 

난 이번에 code섹션이랑  gfm으로 오토링크정도 적용 시켜주기 위해서 다음과 같이 작성했다

export const CustomMdx = async (opts: MDXRemoteProps) => {
    const { source } = opts
    const { content, frontmatter } = await compileMDX<Partial<FrontmatterProps>>({
        source,
        components: CustomComponents,
        options: {
            parseFrontmatter: true,
            mdxOptions: {
                remarkPlugins: [remarkGfm],
                rehypePlugins: [
                    [
                        // @ts-ignore
                        rehypePrettyCode,
                        {
                            theme: 'dark-plus',
                        },
                    ],
                ],
            },
        },
    })

    return { content, frontmatter }
}

(@ts-ignore는 무슨 짓을 해도 타입을 맞출 수가 없어서 걍 걸어버렸다)

 

remarkPlugins에는 초장에 준비한 remarkGfm를 넣어주고

rehypePlugins또한 초장에 준비한 rehypePrettyCode를 넣어주고 테마정도만 설정해준다

 

각 플러그인의 홈페이지에 가서 설명을 보면 바로 알 수 있다.

 

또한 components도 조금 커스터마이징 하고 싶어서 custom-component를 따로 작성했다 

heading태그와 code정도만 바꿔줬는데 다음과 같다 

 

import { LinkIcon } from 'lucide-react'
import { MDXComponents } from 'mdx/types'
import Link from 'next/link'
import { DetailedHTMLProps, HTMLAttributes, createElement } from 'react'

const HeaderCompoenet = (level: number) => {
    const HeaderComponent = (props: DetailedHTMLProps<HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>) => {
        const Tag = `h${level}`
        const id = props.children?.toString()?.replaceAll(' ', '-').toLowerCase()

        return (
            <Link className='no-underline heading-url' id={id} href={`#${id}`}>
                {createElement(
                    Tag,
                    { ...props },
                    <section className='flex items-center gap-2 group'>
                        <LinkIcon className='w-0 h-3.5 group-hover:w-3.5 transition-width duration-300' />
                        {props.children}
                    </section>,
                )}
            </Link>
        )
    }
    HeaderComponent.displayName = `h${level}`
    return HeaderComponent
}

const codeComponent = (props: DetailedHTMLProps<HTMLAttributes<HTMLElement> & { 'data-language'?: string }, HTMLElement>) => {
    return (
        <code className='flex flex-col relative'>
            <span className='absolute top-0 right-0 px-1.5 rounded border capitalize'>{props['data-language']}</span>
            {props.children}
        </code>
    )
}

export const CustomComponents: MDXComponents = {
    h1: HeaderCompoenet(1),
    h2: HeaderCompoenet(2),
    h3: HeaderCompoenet(3),
    h4: HeaderCompoenet(4),
    h5: HeaderCompoenet(5),
    h6: HeaderCompoenet(6),
    code: codeComponent,
}

 

Heading태그들은 뭐.. 반복되는 내용이기도 해서 class작성해서 뽑을까.. 하다

그냥 함수로 통일하자 해서 함수로 하나씩 찍어내줬고

 

code에는 해당 코드의 언어를 표시하고 싶어서 저런 식으로 작성해줬다 

 

마지막으로 CustomComponents로 export해서 이 위의 코드처럼 customcomponent에 넣어주면 끝이다

 

 

 

작성이 끝났다면 마지막으로 compile된 mdx에서

content와 frontmatter 를 추출해서 리턴해주면 또 하나의 함수 생성 끝 

 

다시 article/[post]/page로 돌아가서 페이지를 마저 작성해 주자

 

 

 

const RemoteMdxPage = async ({ params }: { params: { post: string } }) => {
    const source = (await getPostSource(params.post)) as MDXRemoteProps['source']
    const viewCnt = await manageViewCnt(params.post)
    const comments = await getCommentList(params.post)
    const { content, frontmatter } = await CustomMdx({ source })

    return (
        <>
            <MdxPage content={content} frontmatter={{ ...frontmatter, viewCnt }} />
            <Comments comments={comments} post={params.post} />
        </>
    )
}
export default RemoteMdxPage

 

viewCnt와  comment는 supabase에서 가져오는데, 함수가 너무 길어져서 하나하나 빼서 위에 놔둔 상태이다

 

이번 포스팅의 목적은 단순하게 마크다운이 목적이기에 생략하고..  자세히 볼 것은 content와 frontmatter가 생성된 부분이다

 

이렇게 생성하고 페이지 틀에 부어줄텐데 그게 이제 MdxPage컴포넌트이다 

 

import dayjs from 'dayjs'
import { JSXElementConstructor, ReactElement } from 'react'
import { Badge } from '../ui/badge'
import { Separator } from '../ui/separator'
import { FrontmatterProps } from './custom-mdx'
import Tags from '../post/tags'
interface MdxPageProps {
    content: ReactElement<any, string | JSXElementConstructor<any>>
    frontmatter: Partial<FrontmatterProps>
}
const MdxPage = async ({ frontmatter, content }: MdxPageProps) => {
    return (
        <section className='prose prose-neutral dark:prose-invert container max-w-screen-lg py-7 bg-neutral-50 dark:bg-neutral-900 rounded my-5'>
            <section className='flex justify-between items-center flex-wrap'>
                <section className='flex items-center space-x-2 h-5'>
                    <Badge variant={'outline'}>{frontmatter.category}</Badge>
                    <Separator orientation='vertical' />
                    <span className='text-xl font-bold'>{frontmatter?.title}</span>
                </section>

                <section className='flex items-center space-x-2 h-5'>
                    <span>{dayjs(frontmatter?.date).format('YYYY-MM-DD')}</span>
                    <Separator orientation='vertical' />
                    <span>{frontmatter.viewCnt} views</span>
                </section>
            </section>
            <Separator className='my-2' />
            <section className='flex flex-wrap gap-2 py-3 justify-end'>
                <Tags tags={frontmatter?.tags} />
            </section>
            <section>{content}</section>
        </section>
    )
}
export default MdxPage

 

사실 frontmatter를 화면에 표시하기 위한! 컴포넌트라 해도 무방하다

content자체는 이미 compile된 상태여서

그냥 컴포넌트로 뿌려주면 자기가 알아서 렌더링 된 상태로 나타난다

 

코드 기준으로 저런 식으로 나온다 

 

사실 코드블럭도 보여주고 싶은데.. 이미 올라가버린 글이라서 새로 쓰긴 귀찮고.. 요건 유지보수하면서 

'이러한 케이스가 있었다' 하면서 또 포스팅을 작성해 보도록 하겠다.

 

마무리

어려울 수 있다 생각할 수도 있는데 전혀 어렵지 않다

그냥 삽질이 좀 많을 수도 있는데 그건 어쩔 수 없는 숙명이라 생각하고..

 

하나 작성해서 보면 매우 이쁘기 때문에.. 이 글을 읽고 있다면 마크다운으로 홈페이지 하나 작성해 보는 것을 추천한다

 

덤으로 nextjs에서의 기능들도 계속 손에 익힐 수가 있으니 매우 강력 추천..

 

 

 

 

복사했습니다!