본문 바로가기
프로젝트

[블로그 프로젝트] 2. mdx 마크다운으로 게시물 페이지 만들기

by Piva 2024. 2. 11.
  • 이전 기획글에서도 언급했듯, 마크다운을 활용하여 블로그 게시물을 작성하기로 결정했었다.
  • 저번 글에 이어, 마크다운을 통한 게시물 작성을 구현하기 위해 무엇을 하였는지 소개한다.

마크다운(mdx/md) 띄우기

next-mdx-remote + gray-matter 활용하기

  마크다운을 Next.js에서 사용하는 방법으로, 공식 문서에도 안내되어 있는 @next/mdx 가 있다. 이 패키지는 로컬 폴더 내의 mdx 파일을 pages/app 디렉터리 내에서 바로 사용할 수 있게 한다고 설명하고 있다.

 

  공식문서를 살펴보면 소개하는 또 다른 마크다운 패키지가 있는데, 이것이 바로 내가 사용한 next-mdx-remote 이다. 앞서 언급한 @next/mdx와의 차이점이라면 굳이 로컬에 있는 마크다운이 아니어도 사용이 가능하다는 것이다(ex. 서버에 있는 컨텐츠 등). 특히 마크다운 파일들을 서로 다른 로컬 폴더에 저장하거나, DB 혹은 CMS를 사용하는 경우에 유용하다고 언급하고 있다. 이 외에도 다양한 마크다운 관련 라이브러리가 존재하나(react-markdown, mdx-bundler 등...), 우선 나는 공식 문서에서도 언급하는 next-mdx-remote를 사용해서 작업하기로 결정했다.

 

  추가로 사용한 것은 gray-matter 라는 패키지로, 파일에서 Front matter를 파싱하는데 사용된다. Front matter에는 게시물 제목, 날짜, 카테고리 등 포스팅에 관한 각종 정보를 넣을 수 있다.

Front matter: 직역하면 머리말이나 전문(前文). 마크다운의 메타 데이터를 작성하는데 사용된다. _(언더스코어) 3개 사이에 넣고자 하는 정보를 키-값 형태로 기록하면 된다.

 

  마크다운을 처리하는 방법은 아래의 과정을 따른다.

  1. File system(fs)을 통해 로컬 폴더 내의 마크다운 파일들을 불러온다.
  2. gray-matter 등을 이용해 마크다운 내 Front matter 데이터와 컨텐츠를 파싱한다.
  3. 파싱된 마크다운 컨텐츠는 next-mdx-remote 등을 통해 HTML 형태로 변환한다.
import { readFileSync, readdirSync } from 'fs';
import matter from 'gray-matter';

// 파일 시스템을 통해 로컬 내 마크다운 파일을 불러오기 위한 함수
const getFileContent = (fileName: string) => {
  const FILE_PATH = PATH + fileName;
  return readFileSync(FILE_PATH, 'utf-8');
};

// gray-matter를 통해 마크다운의 front matter와 컨텐츠를 파싱한다.
const getPost = (fileName: string) => {
  const source = getFileContent(fileName);
  const { content, data } = matter(source); // 여기서 data가 front matter이다.

  return { content, data };
};

 

 

remark / rehype 등으로 커스터마이징 하기

  이렇게 가져온 마크다운을 커스텀하는 패키지가 존재하는데, 대표적으로 remark/rehype가 있다. 각각 마크다운/HTML을 변환하는 플러그인으로, 다양한 종류의 플러그인을 제공하고 있다. next-mdx-remote에서 이들을 사용하고자 한다면, serialize 함수에 사용할 플러그인들을 넘겨주면 된다. 아래에선 내가 사용한 플러그인을 간략하게 소개한다.

 

※ Tailwind를 사용하고 있다면, 마크다운이 적용된 컴포넌트에 prose prefix를 달아주자. 그래야 마크다운 컨텐츠의 스타일이 유지된 채 제대로 표시된다.

 

import { serialize } from 'next-mdx-remote/serialize';
import remarkGfm from 'remark-gfm';
import rehypePrism from 'rehype-prism-plus';

const serializeMdx = async (source: string) => {
  return serialize(source, {
    mdxOptions: {
      remarkPlugins: [remarkGfm], // remark 플러그인
      rehypePlugins: [rehypePrism], // rehype 플러그인
      format: 'mdx',
    },
  });
};

 

remark-gfm

  Github Flavored Markdown(GFM)을 사용할 수 있도록 하는 플러그인. 일반 마크다운에 추가로 GFM에서 지원하는 테이블, 체크 리스트 등을 사용할 수 있게 해준다. 기능이 많기도 하고, 깃허브를 사용하다 보니 아무래도 GFM을 활용하는 것이 좋겠다 싶어서 추가했다.

 

rehype-prism-plus

  코드 블럭을 꾸밀 수 있는 플러그인. 코드 라인 넘버링이나 라인 하이라이트 기능 등을 제공한다. 참고로 라인 하이라이트나 코드 라인 넘버링이 정상적으로 작동하게 하려면 이것을 추가해야한다고. 추가로, 이곳에서 코드 블럭을 꾸밀 수 있는 다양한 테마 CSS 파일을 공개하고 있다. 마음에 드는 테마를 적절하게 고르면 좋을 것 같다.

 

  위의 플러그인을 사용하여 마크다운의 코드 블럭을 스타일링한 결과는 아래와 같다.

코드 블럭의 스타일링이 완료된 모습

 

 

게시물 페이지 만들기

  현재 블로그 페이지 구조를 간단하게 그리자면 다음과 같다.

 

  post가 게시물 페이지로, 각 마크다운의 파일 이름으로 이루어진 URL을 제공하기 위해 동적 라우팅을 사용한다. 블로그 페이지를 어떻게 렌더링할까 생각했었는데, 아무래도 블로그 게시물을 자주 수정하진 않을 것 같아 정적으로 페이지를 생성하기로(SSG) 결정했다.

 

  이러한 결정 하에 블로그 게시물을 렌더링하는 과정은 아래와 같다.

  1. 블로그 게시물 페이지가 될 [slug].tsx 를 만든다.
  2. getStaticPaths에 slug에 들어갈 수 있는 값들을 넣어준다. 내 경우는 각 마크다운 파일들의 파일명이 된다.
  3. getStaticProps에서 slug를 통해 게시물 내용을 가져오고, 이를 prop으로 전달한다.

※ slug가 무엇인지 몰랐는데, 이번에 블로그 개발에 대해 알아보면서 알았다. 마크다운 파일 이름(ex. test.md)에서 파일 확장자 부분을 제외한 파일명 부분을 slug라 한다는 듯?

// src/pages/post/[slug].tsx

export const getStaticPaths: GetStaticPaths = async () => {
  const paths = getAllPosts().map((post) => ({ params: { slug: post.slug } }));

  return {
    paths,
    fallback: false,
  };
};

export const getStaticProps: GetStaticProps = async (context) => {
  const slug = context.params?.slug ?? '';
  
  // slug를 통해 파일 이름을 가져와 해당 이름을 가진 마크다운 게시물을 가져온다.
  const { content, data } = getPost(`${slug}.mdx`);

  const mdxSource = await serializeMdx(content);

  return {
    props: { content: mdxSource, data },
  };
};

 

마크다운과 스타일이 적용된 모습

 

  이 과정을 거치면 사진처럼 원하는 디자인이 적용된 마크다운 블로그 페이지를 만날 수 있다.