Nextra 블로그 시작

Nextra 블로그 시작

Next.js (opens in a new tab) 그리고 MDX (opens in a new tab) 기반의 문서와 함께 사이트를 구축할 수 있는 Nextra (opens in a new tab) 블로그를 시작하게 되면서 구축한 내용에 대해 정리를 해보았다.

Nextra 사이트를 구축하기 이전에는 첫 번째로 Hexo 프레임워크를 사용해서 정적 사이트를 빌드 후에 Github에 배포하는 형태로 블로그를 운영했었다.

두 번째로는 국내에서 유명한 Tistory 플랫폼을 이용해서 블로그를 운영했었다.

Hexo, Tistory 에서 블로그를 운영해오면서 내 입맛대로 블로그를 커스터마이징 하고싶다는 욕심이 있었다.

하지만 Hexo의 경우 사용법 미숙으로 한계를 느꼈었고, 편리함을 추구해서 Tistory로 옮겼지만 블로그 플랫폼 특성상 정해진 도구내에서 커스터마이징을 진행하다보니 한계가 느껴졌었다.

Nextra를 발견하고 내가 현재 사용하는 기술들로 조금 더 유연하게 커스텀할 수 있다는것에 매력을 느껴 새로운 블로그 구축을 시작하게 되었다.

Nextra 는?

NextraNext.js를 만든 Vercel 팀에서 시작된 프로젝트로 Next.js 기반으로 사이트를 쉽고 유연하게 만들 수 있는 프레임워크이다.

Nextra 문서 (opens in a new tab)

Nextra 2.x.x 버전을 기준으로 사용하게 되면 기본적으로 제공하는 것들은 아래와 같다.

나는 Nextra에서 제공하는 문서 테마가 마음에 들어서 해당 테마를 적용시키고 블로그를 시작했다.

Nextra 프로젝트 시작해보기

Nextra 프로젝트 생성

Next.js 기반이기 때문에 사전에 node.js가 준비되어야 한다.

node.js 설치해야 한다면 node.js (opens in a new tab) 에서 직접 안정화된 LTS 버전으로 설치할 수 있다.
node.js를 직접 설치해도 되지만 node.js는 꾸준히 업데이트가 되고 있어서 버전을 변경해야 한다면 재설치가 귀찮기 때문에 언제든지 Node.js 버전을 변경해서 설치할 수 있는 nvm으로 설치하는 것을 추천한다.
https://github.com/nvm-sh/nvm (opens in a new tab)

우선 Nextra를 시작할 작업 폴더 루트를 생성하고 해당 루트에서 아래 명령어를 통해 프로젝트를 생성한다.

bash
npm i next react react-dom nextra nextra-theme-docs

실행 명령어 설정

프로젝트에 설치가 완료되었다면 프로젝트를 실행하기 위해서 package.json 파일을 아래처럼 scripts 명령어를 추가해서 수정한다.

package.json
{
	"scripts": {
		"dev": "next",
		"build": "next build",
		"start": "next start"
	},
	"dependencies": {
		"next": "^14.1.0",
		"react": "^18.2.0",
		"react-dom": "^18.2.0",
		"nextra": "^2.13.2",
		"nextra-theme-docs": "^2.13.2"
	}
}

next.config.js 설정

Next.js에서 Nextra Docs 테마를 적용시키기 위해 next.config.js 파일을 루트에 추가하고 아래처럼 설정한다.

next.config.js
const withNextra = require('nextra')({
  theme: 'nextra-theme-docs',
  themeConfig: './theme.config.tsx',
});
 
module.exports = withNextra();

Typescript 설정

Javascript 만으로도 작업이 가능하지만 Typescript를 사용해서 타입과 함께 작업할 수 있도록 한다.

프로젝트 루트경로에 tsconfig.json 파일을 생성하고 관련 설정한다.

tsconfig.json
{
    "compilerOptions": {
        "target": "es5",
        "lib": [
            "dom",
            "dom.iterable",
            "esnext"
        ],
        "allowJs": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "noEmit": true,
        "incremental": true,
        "esModuleInterop": true,
        "module": "esnext",
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "jsx": "preserve"
    },
    "include": [
        "next-env.d.ts",
        "**/*.ts",
        "**/*.tsx"
    ],
    "exclude": [
        "node_modules"
    ]
}

theme.config.tsx 테마 설정

Nextra 테마를 사용하게되면 테마에서 설정할 수 있는 config 값들을 제공한다.

나는 Docs Theme를 사용했기 때문에 아래 문서를 참고하면 Docs 테마와 관련된 설정을 변경할 수 있다.

https://nextra.site/docs/docs-theme/theme-configuration (opens in a new tab)

프로젝트 루트경로에 theme.config.tsx 파일을 추가하고 아래처럼 설정한다.

theme.config.tsx
import { DocsThemeConfig } from 'nextra-theme-docs';
 
const config: DocsThemeConfig = {
    logo: <span>My Nextra Documentation</span>,
    project: {
        link: 'https://github.com/shuding/nextra'
    }
    // ... other theme options
}
 
export default config;

첫 페이지 만들기

이제 pages/index.mdx 디렉토리에 파일을 추가하고 내용을 작성한다.

pages/index.mdx
# Nextra 사이트 홈
 
Hello, Word!

폴더구조는 아래와 같다.

    • index.mdx
  • next.config.js
  • package.json
  • theme.config.tsx
  • tsconfig.json
  • 프로젝트 실행

    이제 아래 명령어를 실행시켜 프로젝트 개발환경을 실행한다.

    npm run dev

    아래 주소창으로 접속해서 들어가서 내용이 보인다면 기본적인 세팅은 끝났다.

    http://localhost:3000 (opens in a new tab)

    커스터마이징

    Nextra에서 제공하는 기본 테마로 글을 작성할 수도 있었지만 여기서 자동화 할 수 있는 부분을 처리하고 내가 커스텀 하고싶은 부분은 변경해서 적용시켰다.

    커스터마이징을 시작하기에 앞서 내가 필요하다고 생각한 목록을 정리했다.

    • 컴포넌트 작성 시 스타일을 편리하게 사용
    • 날짜 데이터 포맷팅
    • MDX 파일 작성시 front-matter 정보를 입력해서 정적 데이터로 만들고 활용
    • 최신글 데이터 생성
    • 빌드시점에 자동화 코드 실행시키기

    생각한 목록내에서 필요한 패키지들을 선정했다.

    설치 명령어

    npm i clsx dayjs
    npm i -D tailwindcss autoprefixer postcss front-matter ts-node

    Theme Config

    우선 Nextra에서 Docs 테마를 사용했기 때문에 theme.config.tsx 파일에서 내가 설정하고 싶은 부분을 변경할 수 있다.

    Docs Theme Configuration (opens in a new tab)

    theme.config.tsx
    const config: DocsThemeConfig = {
      logo: "왼쪽 상단 로고 내용 React Component로 구성 가능",
      project: {
        link: "오른쪽 상단 Github 링크",
      },
      /**
       * @see https://www.npmjs.com/package/next-seo?activeTab=readme
       */
      useNextSeoProps() {
        // Nextra 내부에서 next-seo 패캐지를 사용하는데 검색엔진 최적화 관련해서 동적으로 설정할 수 있다.
      },
      head: "head 태그에 들어갈 내용 주로 meta 태그들을 설정할 수 있다."
      },
      search: {
        placeholder: "검색창 placeholder 속성 문구"
      },
      footer: {
        text: "푸터 문구",
      },
      // 예시) main: ({ children }) => return <div>콘텐츠는 이걸로 변경됩니다.</div>
      main: "메인 콘텐츠 내용을 변경할 수 있다.",
      // https://nextra.site/docs/docs-theme/theme-configuration#theme-color
      primaryHue: "사이트 메인 색상 선택",
      themeSwitch: {
        useOptions() {
          // 테마 변경 문구를 지정 가능
          return {
            light: '라이트',
            dark: '다크',
            system: '시스템'
          }
        }
      }
    }

    위에 설정처럼 테마에서 설정하고 싶은 부분을 내 입맛에 맞게 변경했다.

    해당 설정은 모든 페이지에 들어갈 때마다 실행되는 설정값으로 nextra에서 useConfig (opens in a new tab) 훅을 사용하면 설정값들을 가져와서 페이지가 실행되기 전에 동적으로 설정값을 변경할 수 있다.

    나는 MDX 문서를 작성할 때 상단에 front matter (opens in a new tab) 정보를 정해진 포맷으로 작성하기 때문에 해당 부분을 가져와서 동적으로 head부분에서 meta 정보를 추가로 넣어주는 부분을 추가했다.

    글을 작성할 때 항상 Front Matter 포맷을 아래처럼 가져가고 themeConfig 파일에서 useConfig 훅을 통해 해당 정보를 참고할 수 있기 때문에 동적으로 적용시켜보았다.

    ---
    title: Nextra 블로그 시작
    description: Nextra 블로그를 시작하게 된 내용
    keywords:
      - nextra
      - blog
    date: 2024-01-13
    thumbnail: /img/nextjs-icon.svg
    ---
     
    ... 내용

    Front Matter 정보와 함께 테마 설정에서 활용

    theme.config.tsx
    const config: DocsThemeConfig = {
      head: () => {
        const { asPath, defaultLocale, locale } = useRouter();
        const { frontMatter } = useConfig();
        const url =
          `${process.env.NEXT_PUBLIC_URL}${(defaultLocale === locale ? asPath : `/${locale}${asPath}`)}`;
     
        return (
          <>
            <meta property="og:url" content={url} />
            <meta property="og:title" content={frontMatter.title || 'yeobe blog'} />
            <meta
              property="og:description"
              content={frontMatter.description || 'yeobe blog content'}
            />
          </>
        )
      },
      // ...config
    }

    gitTimestamp

    나는 글 작성 시간을 작성하는 문서에 정의해놓고 쓰고 싶은데 git commit 시간으로만 글 하단에 표시하게 되어있어서 해당 부분을 변경하였다.

    https://nextra.site/docs/docs-theme/theme-configuration#last-updated-date (opens in a new tab)

    theme.config.tsx 파일에서 main (opens in a new tab) 속성을 활용하면 children props에 어떤 내용들이 담겨있는지 참조할 수 있는데 아래와 같은 순서로 내려오는 것을 확인할 수 있었다.

    1. MDX 문서 내용
    2. gitTimestamp github commit 시간에 따른 글 작성 시간(정보가 없으면 Text만 null)
    3. Navigation 컴포넌트
    main content

    여기서 문제가 발생했는데 사실 main 속성에서 제공하는 children propsreadonly 전용이여서 변경할 수 없다.

    나는 이부분을 React에서 제공하는 Children.map (opens in a new tab)을 통해서 ReactNode를 복제하는 동시에 순회하면서 변경하는 방법을 택했다.

    gitTimestamp 부분을 내가 MDX 문서로 글을 작성할 때 넣어준 Front Matter -> date 속성의 날짜로 변경하고 내가 원하는 포맷으로 변경했다.

    ⚠️

    여기서 props로 받은 childrenReactNode를 직접 변경하는것은 React 권장사항이 아니다.
    리액트 Children 문서 (opens in a new tab)를 살펴보면 더 나은 대안들을 제공하고 있다.
    하지만 테마 자체에서 제공하는 부모 컴포넌트를 변경하기 어려운 상황이여서 ReactNode를 복사해서 변경 후 내려주는 방법을 택했다.

    theme.config.tsx
    import React, { Children, ReactElement } from 'react';
    import { useRouter } from 'next/router';
    import { DocsThemeConfig, useConfig } from 'nextra-theme-docs';
    import dayjs from 'dayjs';
    import { main } from './src/config/themeConfig';
     
    const config: DocsThemeConfig = {
      main: ({ children}) => {
        const { frontMatter } = useConfig();
        const writeTime = dayjs(frontMatter.date).format('YYYY년 MM월 DD일');
     
        // gitTimestamp 부분의 index를 선택해서 내용을 변경한다.
        const gitTimestampCustomizeChildren = Children.map(Children.toArray(children), (child, index) => {
          child = (child as ReactElement).props.children.map((propChild, propIndex) => {
            if (propIndex === 1) {
              if (!frontMatter.date) return;
     
              propChild = React.createElement(
                "div",
                { className: "nx-mt-12 nx-mb-8 nx-block nx-text-xs nx-text-gray-500 ltr:nx-text-right rtl:nx-text-left dark:nx-text-gray-400" },
                React.createElement("time", { dateTime: frontMatter.date }),
                `작성일 ${writeTime}`
              )
            }
     
            return propChild;
          })
     
          return child;
        });
     
        return <>
          { gitTimestampCustomizeChildren }
        </>
      }
      // ...other config
    }
     
    export default config;

    빌드시점에 파일과 데이터 생성하기

    nextra에서는 pages 폴더를 기준으로 _meta.json 이라는 파일에서 작성한 MDX 문서의 설정값이나 제목등을 어떻게 보여줄건지 설정하는 방법을 제공한다.

    _meta.json 설정법 (opens in a new tab)

    theme.config.tsx 에서 설정한 옵션이 페이지에 기본적으로 적용되고 특정 페이지를 다른 설정값을 적용시키고 싶다면 _meta.json 파일을 MDX파일의 동일 디렉토리에 생성해서 따로 설정하는 방법이다.

    _meta.json 예시모양

      • sample1.mdx
      • sample2.mdx
      • _meta.json
  • _meta.json
    {
        "sample1": {
            "title": "샘플 1입니다"
        }
    }

    위에처럼 파일을 작성했다면 아래 이미지와 같은 메뉴가 나타나게 된다.

    menu sample

    나는 이부분에서 직접 _meta.json 파일을 직접 만들기보다 MDX 문서로 작성할 때 Front Matter 정보를 입력할 것이니 해당 정보로 _meta.json 파일을 빌드시점에 동적으로 생성하면 좋겠다고 판단했다.

    ts-node용 tsconfig.json 적용

    우선 빌드 시점에 파일을 생성할 부분의 코드를 typescript로 작성하고 실행시키기 위해 ts-node 설정을 추가했다.

    tsconfig.json
    {
      // ts-node 부분 설정 추가
      "ts-node": {
        "transpileOnly": true,
        "compilerOptions": {
          "module": "commonjs"
        }
      }
    }

    javascript로 작성된 코드를 Node.js 환경에서 빌드 시점전에 실행시켜 파일을 생성시키는 동작을 구성할 수 있는데 여기서 typescript로 코드를 작성하고 실행시키기 위해서 ts-node 패키지를 사용했다.

    이제 빌드시점에 실행할 코드 파일을 scripts/build-data.ts 경로에 작성하고 실행하기 위해서 명령어를 수정했다.

    package.json
    "scripts": {
        "dev": "npm run build:data && next dev",
        "build": "npm run build:data && next build",
        "start": "next start",
        "build:data": "ts-node ./scripts/build-data.ts"
    }

    빌드 시점에 생성할 파일 정보는 아래와 같다.

    • 카테고리 별로 폴더를 만들고 MDX 문서를 작성했으면 각 카테고리별 폴더안에 _meta.json 파일 생성
    • 카테고리 별로 작성된 모든 MDX 문서를 가지고 최신글 데이터 생성

    위에 목표대로 카테고리별 _meta.json 파일은 각 MDX 파일의 Front Matter 정보에 title 내용으로 메뉴 구조가 생기도록 _meta.json 파일을 생성하는 작업을 진행했다.

    최신글 데이터의 경우 pages 루트 경로를 제외하고 모든 MDX 문서를 모아서 각 Front Matter 정보중 title, description, date, thumbnail 데이터를 활용해서 최신글 관련 json 데이터를 정적으로 생성했다.

    마지막으로 메인 페이지에서는 카드형식의 최신글 컴포넌트를 새로 작성하고 빌드시점에 생성된 json 데이터만큼 메인 페이지에 보여지게 작성했다.

    해당 내용대로 데이터가 생성되도록 작성하고 메인 페이지에 최신글 컴포넌트를 연동한 모습은 아래와 같다.

    main page

    제가 작성한 프로젝트를 테스트 하거나 수정해서 배포하고 싶은 분들을 위해서 템플릿 프로젝트를 생성했습니다.
    템플릿 프로젝트에는 커스터마이징 한 부분들이 모두 적용되어 있습니다.

    https://github.com/KIMSANGYEOB/nextra-blog-start-template (opens in a new tab)

    마무리

    Next.js 를 아는 개발자라면 언제든지 Nextra 프레임워크를 사용해보는 것을 추천한다.

    Nextra는 현재 오픈소스 개발자에 1~2명에 의해 프로젝트가 유지되고 있어서 개발속도가 빠르진 않다.

    현재 Nextra 유지보수 하고 있는 개발자가 2023-12-12 날짜에 Nextra 3 업데이트 내용 및 마이그레이션에 대해서 내용을 정리해서 올렸다. 하지만, 출시 관련해서 정해진 날짜는 없다고 한다. 😲

    https://the-guild.dev/blog/nextra-3 (opens in a new tab)

    내가 생각하기엔 현재 나와있는 Nextra 2 버전에서도 충분히 문서 사이트나 블로그로 구축해서 활용할 수 있으니 앞으로 SEO 최적화, Sitemap, Comment 등의 기능을 추가하면서 운영을 이어나갈 예정이다.

    작성일 2024년 01월 13일