Zenithium 제작기

마크다운 불러오기 1탄

mdx로 작성된 문서를 불러오는 방법을 소개합니다

2024-10-04 작성

평균 9분 소요

91회 조회

#Blog#Next.js#MDX
cover

개발 블로그에서는 빠질 수 없는 부분이죠. 마크다운 문법으로 작성된 내용을 어떻게 블로그로 불러오는지 알아보려고 합니다.

content.mdx 의 구성

Zenithium에서는 content.mdx 라는 이름의 파일로 포스트를 관리하고 있습니다. 그럼 이 내부에는 어떤 구조로 정보가 담겨있을까요?

Front Matter

개별 포스트 파일의 최상단에는 포스트의 기본 정보가 담겨있는 영역이 있습니다. 이 부분을 Metadata 혹은 Front Matter 라고 합니다.

Front Matter에 담긴 정보를 활용해 각 페이지에서 제목, 설명, 작성일, 시리즈 등을 사용자에게 보여줄 수 있는 것이죠.

---
title: "Header & Footer"
description: "테마 전환 토글 만들기 및 svg 이미지를 컴포넌트로 사용하기"
date: 2024-10-03 09:59:48
updated: ""
series: "Zenithium 제작기"
image: "/postAssets/header-footer/cover.jpg"
tags:
  - Blog
  - Next.js
---
Header & Footer 포스트의 Front Matter

담고 싶은 정보의 key를 정하고 이에 해당하는 value를 우측에 적으면 됩니다. 이 때, Typescript의 경우 미리 Front Matter의 타입을 정의해주면 이후 데이터를 가공할 때 굉장히 편해집니다.

Content

MDX에서의 Front Matter가 포스트의 기본 정보라면 Content는 본문 내용입니다. 실제로 Front Matter는 포스트의 내용에 담기지 않지만 Content는 적혀있는 내용이 모두 화면에 나타나게 됩니다.

---
title: "Front Matter 영역"
---
// Content 영역

Front Matter가 끝나는 지점부터 Content의 시작이니 이 부분부터 내용을 적어나가면 됩니다.
그런데 Front Matter를 매 포스팅마다 일일이 적어야 하는게 번거롭다구요? 물론 좋은 방법이 있습니다.

추천 코드 스니펫

다양한 코드 편집기에서는 코드 프리셋을 등록하고 사용할 수 있는 코드 스니펫을 제공하고 있습니다. 이 포스트를 보는 분들 중에도 여러가지 코드 스니펫을 등록해서 사용중인 분들도 계실텐데요.

MDX도 이 코드 스니펫을 사용해서 Front Matter를 더욱 편하게 작성할 수 있습니다. 저는 김평안님의 블로그 소스 코드에서 코드 스니펫을 참고해 제 방식으로 수정해서 사용했어요.

"Post Matter": {
    "prefix": "post",
    "body": [
      "---",
      "title: \"$1\"",
      "description: \"$2\"",
      "date: $CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND",
      "updated: \"$3\"",
      "series: \"$4\"",
      "image: \"/postAssets/${TM_DIRECTORY/^.+\\\\(.*)$/$1/}/cover.jpg\"",
      "tags:",
      "  - $5",
      "---",
      "",
      "$0"
    ],
    "description": "Front Matter 작성"
  },
vscode MDX 코드 스니펫에 추가

이렇게 코드 스니펫을 사용하면 prefix로 설정한 문구와 tab 키로 빠르게 Front Matter를 작성할 수 있습니다.

MDX 파일 불러오기

MDX로 포스팅을 작성했다면 불러와야겠죠?
이 때의 과정은 MDX 파일을 어디에 담아두고 쓰고 있는지에 따라서 방법이 달라지게 됩니다.

외부 DB 등을 이용하는 경우 API나 DB Query를 사용해 불러오면 되지만 저는 프로젝트 내부에 위치시켜 사용하기 위해 fs 모듈을 이용했습니다.

fs 모듈은 서버에서만 사용할 수 있기 때문에 서버 컴포넌트에서 사용해야 합니다.

const readMDXFile = (filePath: string) => {
  const fileContent = fs.readFileSync(filePath, "utf-8");
  const mdxData = matter(fileContent);
 
  return mdxData;
};
개별 MDX 파일을 파싱해 반환하는 함수

fsreadFileSync는 파일명과 확장자를 포함한 파일 경로를 인자로 받아 파일의 내용을 불러오게 됩니다. 파일의 내용을 불러왔으니 이제 사용하면 될까요?

현재 이 데이터는 Buffer의 형태로 반환되기 때문에 이를 MDX를 파싱할 수 있는 파서를 사용해 가공할 수 있는 형태의 문자열로 변환해야 합니다. 저는 이 과정에 gray-matter 라이브러리의 matter 함수를 사용했습니다.

그럼 이 함수를 사용해 모든 포스트를 불러옵시다.
저는 포스트를 /blog-contents/{slug}/content.mdx 의 경로에서 관리하고 있으니 blog-contents 디렉토리의 모든 slug 디렉토리를 순회하며 content.mdxreadMDXFile 함수로 파싱하면 되겠죠?

/blog/utils.ts
// 포스트들을 모아둔 경로
const directoryPath = path.join(process.cwd(), "blog-contents");
 
const getMDXDatas: (dir: string) => Mdx[] = (dir: string) => {
  const directoryNames = fs.readdirSync(dir);
 
  const allPosts = directoryNames.map((dirName) => {
    const filePath = path.join(directoryPath, dirName, "content.mdx");
    const { data: frontMatter, content } = readMDXFile(filePath);
    const slug = dirName;
 
    return { frontMatter: frontMatter as FrontMatter, content, slug, dirName };
  });
 
  return allPosts;
};

fsreaddirSync는 해당 경로에 있는 모든 디렉토리 이름을 배열에 담게 되는데, 이 배열을 순회하며 content.mdx가 담긴 경로를 만들면 됩니다. filePath가 바로 그 경로이죠.
이 경로를 아까 만들어 둔 readMDXFile함수에 담으면 gray-mattermatter 함수가 파싱한 데이터를 받게 됩니다.

이 데이터는 정말 친절하게도 Front Matter와 Content로 잘 나누어서 전달해 주게 되고 Front Matter의 경우 객체의 형태로 반환되기 때문에 바로 꺼내서 사용할 수 있습니다.

추가로 필요한 데이터인 slug와 dirName을 포함해서 모든 포스트 데이터를 만들어 반환하도록 했습니다.

Front Matter와 Content 사용하기

앞에서 만든 로직으로 MDX를 파싱했다면 그대로 사용할 수 있을까요?

Front Matter는 가능하지만 Content는 아직 아닙니다.

const allPosts = getAllPosts();
const post = allPosts.find((post) => post.slug === params.slug);
 
const {
  slug,
  dirName,
  frontMatter: { title, description, image, date, updated, series, tags },
} = post;
Content를 제외한 나머지 데이터는 바로 사용할 수 있다.

matter로 파싱한 Content는 아직 마크다운 문자열 형태이기 때문이죠. 이를 마크업의 형태로 한번 더 파싱해야 합니다.

matter 대신 다음 포스트에서 언급할 next-mdx-remote/rsc 패키지의 compileMDX라는 비동기 파서를 사용하면 Content를 바로 사용할 수 있습니다.

콘솔로 확인한 Content

지금은 마크다운 문법이 그대로 문자열로 넘어온 형태를 가지고 있는데 다음 포스트에서는 이 문자열을 마크업의 형태로 파싱하는 라이브러리인 next-mdx-remote/rsc에 대해서 다뤄보겠습니다.

추가로 mattercompileMDX로 전환하는 과정도 생각중인데 만일 리팩토링을 하게 되면 이 부분도 기록으로 남겨볼 생각입니다.