Next.js 블로그 제작기 (3)

user profile img

신현호

TypeScript
Next.js

블로그 제작기

Post Thumbnail

목차

    시작하기에 앞서

    저는 Next.js 13버전의 App Router를 사용중이기때문에, 12버전까지의 Page Router에서의 사용법과는 다를 수 있습니다.
    또한 TypeScript를 사용하였기때문에, 설명이 TS위주이기때문에 JS로 만드실 분들은 해당 부분 감안하여 참고하시면 되겠습니다!

    MDX를 파싱해보자

    이전 포스트에서는 .md파일을 파싱하는 방법에 대해서 말씀드렸는데요, 저번에도 말한것처럼 현재 저는 mdx 파일로 포스트를 작성중입니다.
    이번 포스트에서는 이전 포스트에서 조금 부족했던 내용들과 더불어 mdx 파일을 파싱하는 방법에 대해서 이야기 할 예정입니다.

    STEP 1. Contentlayer 소개

    Contentlayer

    저는 Contentlayer라는 도구를 사용하여 mdx 파일을 파싱 할 겁니다.
    Contentlayer는 정적 콘텐츠 관리 도구 인데요, 현재까지는 Next.js에서만 사용합니다. (RemixSvelteKit은 지원 고려중이라고 합니다.)

    기본적으로 .md.mdx 파일들을 읽어 데이터 객체로 변환시켜 사용할 수 있습니다.
    타입스크립트와 함께 쓴다면 더 좋은데, 마크 다운 파일에 대한 정적 타입을 제공하기 때문입니다.

    Next.js 공식 문서에도 추천 도구로 Contentlayer를 소개중입니다.

    그러나, mdx를 파싱하는데 꼭 Contentlayer를 사용해야만 하는 것은 아닙니다.
    비슷한 도구인 next-mdx-remote 사용하셔도 좋고, @next/mdx 사용하셔도 좋습니다만 Contentlayer가 다른 툴 대비 Next.js와 연동이 간단하고 툴이 가벼워서 선택했습니다.

    그러니, 빠르게 Contentlayer를 세팅해봅시다.

    STEP 2. Contentlayer 세팅

    bash

    yarn add contentlayer next-contentlayer --dev
    

    설치가 완료되었다면, next.config.ts에 설정을 해줘야합니다.
    어려운 건 아니고 withContentlayer를 로드해서 nextConfig를 감싸주면 됩니다.

    ts

    const { withContentlayer } = require('next-contentlayer')
    ...
    module.exports = withContentlayer(nextConfig)
    

    그 다음에는, tsconfig의 설정이 필요합니다.

    json

    "paths": {
        "@/*": ["./src/*"],
        "@/contentlayer/generated": ["./.contentlayer/generated"],
        "@/contentlayer/generated/*": ["./.contentlayer/generated/*"]
    }
    

    겸사겸사 .gitignore 설정도 해주면 좋습니다. (권장)

    text

    # contentlayer
    *.contentlayer
    

    STEP 3. contentlayer.config.ts 생성하기

    여기서부터 중요해지는데, contentlayer.config.ts는 읽어올 파일의 형식을 정의하는 공간입니다.
    루트폴더에 contentlayer.config.ts파일을 생성하고 다음 내용을 작성해주세요

    ts

    export const Post = defineDocumentType(() => ({
      name: 'Post',
      filePathPattern: `**/*.mdx`,
      contentType: 'mdx',
      fields: {
        title: {
          type: 'string',
          required: true,
        },
        description: {
          type: 'string',
          required: true,
        },
        date: {
          type: 'string',
          required: true,
        },
        thumbnail: {
          type: 'string',
          required: true,
        },
        category: {
          type: 'list',
          of: {
            type: 'string',
          },
          required: true,
        },
      },
      computedFields: {
        url: {
          type: 'string',
          resolve: (post) => `/posts/${post._raw.flattenedPath}`,
        },
      },
    }))
    

    name영역은 커스텀 타입의 이름입니다. 앞으로 지속적으로 사용될 타입이므로 반드시 정의해야합니다
    아래에 있는 field의 내용들은 front-matter에 기재될 내용들입니다.

    예시로 저는 다음과 같이 front-matter를 작성하고 있습니다.

    ---
    title: Next.js 블로그 제작기 (3)
    description: Next.js로 나만의 블로그를..
    date: 2023-10-21
    thumbnail: /blogImages/image02.jpg
    category:
      - TypeScript
      - Next.js
      - Blog
    ---
    

    그 다음으로는 contentlayer 관련 파일들을 생성해주는 로직이 필요합니다.

    ts

    export default makeSource({
      contentDirPath: 'public/blog',
      documentTypes: [Post],
      mdx: {
        remarkPlugins: [remarkGfm, remarkBreaks],
        rehypePlugins: [
          rehypeSlug,
          rehypeHighlight as Pluggable<any>,
          [
            rehypeAutolinkHeadings,
            {
              properties: {
                className: ['anchor'],
                ariaLabel: 'anchor',
              },
            },
          ],
          [rehypeExternalLinks, { target: '_blank', rel: ['noopener noreffer'] }],
        ],
      },
    })
    

    contentDirPath는 작성할 mdx 파일들이 어떤 경로에 있는지를 기재해주면 되고, documentTypes는 앞서 생성한 커스텀 타입을 넣어주면 됩니다.
    이후 아래에 있는 remarkPluginsrehypePlugins 내용들에 대해서 간략하게 말씀드려보자면

    remark는 마크다운을 처리하는 라이브러리입니다. 이전에 사용한 remarkGfmremark의 플러그인입니다.
    rehype은 html을 처리하는 라이브러리입니다. mdx는 html 문법과 마크다운 문법이 혼용되기 때문에 필요합니다.

    저는 remark에서는 remarkGfmremarkBreaks를 사용했으며, rehype에서는 rehypeSlug, rehypeHighlight, rehypeAutolinkHeadings, rehypeExternalLinks를 사용했습니다.
    (rehypeHighlight에 들어간 anyTypeError가 발생해서 임시 조치 해놓은 것이니 좋은 방법 있으면 공유해주세요 ㅠㅠ)

    해당 플러그인을 어떤 이유로 사용했는지 간략하게 정리하자면 다음과 같습니다.

    • remark

      • remarkGfm / 깃허브 마크다운 문법을 사용하기위해서
      • remarkBreaks / 간편한 개행 처리를 위해서
    • rehype

      • rehypeSlug / h1 ~ h6 태그들에 id를 부여하기 위해서
      • rehypeAutolinkHeadings / rehypeSlug로 부여된 id로 화면 스크롤을 이동시키기 위해서 (목차 바로가기 기능)
      • rehypeHighlight / 코드 스페이스에 하이라이팅을 적용하기 위해서
      • rehypeExternalLinks / 외부 링크를 첨부하기 위해서

    STEP 4. Contentlayer를 사용하여 파일 읽어오기

    ts

    import { allPosts, type Post } from '@/contentlayer/generated'
    
    export function getAllPost(): Array<[number, Post]> {
      return allPosts
        .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
        .map((post, idx) => [idx, post])
    }
    

    설정이 완료되었다면, allPosts를 사용하여 파일을 불러오면 됩니다.
    이전 마크다운때는 readdirSync를 사용하여 파일을 읽어온 후 처리했는데, 훨씬 간단해진 모습입니다.

    이렇게 하면 파일을 객체 배열 형태로 받아오는 데 까지는 성공한겁니다.
    그럼 그 다음은 읽어온 파일들을 렌더링 시켜야겠죠?

    STEP 5. Contentlayer를 사용하여 MDX 렌더링하기

    ts

    import { type Post } from '@/contentlayer/generated'
    import { type MDXComponents } from 'mdx/types'
    import { useMDXComponent } from 'next-contentlayer/hooks'
    import Link from 'next/link'
    import '../style/reset.css'
    
    interface MdxRendererProps {
      post: Post
    }
    
    const mdxComponents: MDXComponents = {
      a: ({ href, children }) => (
        <Link
          href={href as string}
          className="text-lg text-gray-500 no-underline transition duration-150 ease-in hover:text-black">
          {children}
        </Link>
      ),
      h1: ({ children }) => (
        <h1 className="hover:underline-offset-3 hover:underline">{children}</h1>
      ),
    }
    
    export default function MdxRenderer({ post }: MdxRendererProps) {
      const MDXContent = useMDXComponent(post.body.code)
    
      return (
        <div className="prose prose-neutral max-w-[75ch]">
          {post.body.code !== undefined && (
            <MDXContent components={mdxComponents} />
          )}
        </div>
      )
    }
    

    front-matter를 제외한 내부 내용은 ~.body.code를 참조하면 얻을 수 있으며, useMDXComponent를 사용하면 ~.body.code의 내용들을 JSX 컴포넌트로 변환할 수 있습니다.
    MDXContent의 인자로 들어간 mdxComponents는 커스텀 스타일링 태그들입니다.

    이번에도 tailwindcss-typographyprose를 사용했습니다. 이렇게하면 간단하게 스타일링을 적용할 수 있습니다.

    자주 발생하는 이슈

    TypeError: Cannot read properties of undefined (reading 'inTable')

    지금 remarkGfmcontentlayer간의 충돌이 있어, remarkGfm을 최신버전으로 받으시면 오류가 발생합니다.

    remarkGfm의 버전을 3.0.1 로 다운그레이드하면 정상적으로 작동합니다.


    렌더링된 mdx 컴포넌트의 스타일을 수정하고 싶습니다. 어떻게 하면 될까요?

    거의 대부분의 이유는 tailwindcss-typographyprose속성 때문에 그런거므로, 해당 스타일을 따로 정의하여 로드하시면 됩니다.


    특정 컴포넌트에서 allPosts를 호출하면 mdx 파일을 읽어오지 못합니다.

    next.js 13버전부터는 클라이언트 컴포넌트와 서버 컴포넌트의 개념이 생겼습니다.
    기존 12버전을 사용하던 분들에는 낯선 개념일 수 있습니다. next.js 의 컴포넌트는 기본적으로 서버 컴포넌트이지만, 로직에 use client를 삽입해주면 클라이언트 컴포넌트로 동작합니다.

    contentlayer측에서는 contentlayer의 기능을 서버 컴포넌트에서 사용하기를 권장하고 있습니다.
    그렇기에 contentlayer의 기능을 클라이언트 컴포넌트에서 사용하여 오류가 발생했다면, 서버 컴포넌트로 변경하여 사용하면 됩니다.


    hook 사용을 하지 못하는데 어떻게 파라미터의 값을 받아오는지 궁금합니다.

    • url경로에 있는 파라미터들의 값이 필요한데, 이거 받아오려면 usePathName 써야하는거 아닌가요?
    • 클라이언트 컴포넌트에서는 contentlayer 사용을 권장하지 않는다고 하시지 않았나요?

    서버 컴포넌트에서 hook을 사용하지 않고 파라미터를 받아오는 방법이 있습니다.

    ts

    export default function posts({ params }: { params: { category: string } }) {
    ...
    }
    

    이런 방식으로 params를 받아올 수 있습니다. useRouter() 혹은 usePathname() 을 사용하실 필요없으니 꼭 저 방식을 사용하시길 바랍니다.


    이렇게 해서 mdx 파일들을 contentlayer를 사용하여 파싱하는 방법에 대해서 설명 해 드렸는데요
    제가 한 방법이 100% 다 맞는 방법이 아닐 수 있기 때문에, 부족한 부분은 댓글로 피드백 주시면 되겠습니다!

    Profile Image

    신현호

    Frontend Developer

    프론트엔드 개발자를 꿈꾸고 있는 대학생입니다. 끊임없이 배우고 성장하는 개발자가 되기 위해 노력하고 있습니다.

    블로그 제작기

    총 11개의 포스트가 존재합니다.