Published On

Next.js로 블로그 제작하기 1


블로그 템플릿을 사용하지 않고 Next.js로 직접 제작한 이유

블로그의 기획부터 배포까지 전 과정에 직접 참여함으로써 제 취향의 블로그를 생성해보고 싶었습니다.

맞춤형 레이아웃, mdx 파일 파싱, 자동 배포 구축, SEO 최적화 및 검색 엔진 등록 등을 직접 해보며 생성하였습니다.

Next.js의 강력한 SEO 지원과 효율적인 디렉토리 파싱라우팅 기능이 이러한 결정에 크게 기여하였습니다.

패키지 매니저로 pnpm을 선택한 이유

프로젝트에서 사용할 패키지 매니저 선택은 개발의 효율성과 안정성에 중대한 영향을 미칩니다.

초기에는 bun을 고려했으나, bun이 npm의 최신 버전을 지원하지 않아 여러 호환성 문제가 발생했습니다.

이 문제를 해결하기 위해, 저는 pnpm을 선택했습니다.

pnpm은 뛰어난 안정성과 호환성을 제공하며, 특히 프로젝트의 의존성을 효율적으로 관리할 수 있도록 도와줍니다.

mdx 파일 파싱으로 velite를 선택한 이유

기존에 가장 많이 사용하는 contentlayer는 더 이상 업데이트가 없어서 다른 파싱 라이브러리를 찾다가 velite를 선택하였습니다.

직접 mdx파일을 일일이 파싱하는것보다 효율적으로 파싱을 해주고 타입 안전성빌드 시간 최적화, 유연한 콘텐츠 쿼리와 통합

콘텐츠 쿼리란?

콘텐츠 쿼리는 데이터를 검색하고 추출하는 데 사용되는 요청입니다.

이는 일반적으로 데이터베이스에서 정보를 검색하거나, 컨텐츠 관리 시스템(CMS)에서 특정 조건에 맞는 컨텐츠를 불러올 때 사용됩니다.

// 예시
// ./.velite에서 데이터 검색하고 추출
import { posts } from "#site/content";

const tags = getAllTags(posts);

블로그 제작 프로세스

제가 프로젝트를 효과적으로 진행하기 위해 준비한 단계별 계획은 다음과 같습니다.

  1. 벤치마킹: 경쟁력 있는 블로그들을 조사하여, 우수한 디자인과 기능을 참고하였습니다.
  2. 디자인 초안: 블로그의 대략적인 디자인 및 레이아웃 구상을 피그마를 통해 진행하였습니다.
  3. 프로젝트 생성: pnpm을 사용하여 Next.js 프로젝트를 생성하였습니다.
  4. 코드 품질 관리: ESLint와 Prettier를 설정 및 vscode의 setting을 변경하고 적용하였습니다.
  5. 콘텐츠 파싱: velite를 활용해 MDX 혹은 마크다운 파일로부터 정적 HTML을 생성하였습니다.
  6. 설정 파일 수정: velite.config.ts 파일을 세부적으로 조정하여 컨텐츠 파싱을 최적화하였습니다.
  7. 스타일링: CSS를 수정하여 블로그의 시각적 요소와 스타일링 하였습니다.
  8. 메타데이터 설정: 페이지별 메타데이터를 설정하여 SEO 친화적인 블로그를 구축하였습니다.
  9. SEO 및 유틸리티 페이지 작성: 검색엔진 최적화를 위해 sitemap과 robots.txt를 생성하고, 사용자 경험을 개선하기 위해 404 Not Found 및 Error 페이지를 생성하였습니다.

환경 설정하기

  • pnpm package manager를 이용하여 nextjs 생성
# nextjs 설치
pnpm create next-app
# dark모드를 위한 theme 라이브러리 설치
pnpm i next-themes
# tailwindcss의 3rd party로 사용할 `typography`, `forms`를 설치
pnpm i -D @tailwindcss/forms @tailwindcss/typography
# mdx를 html로 parsing할 라이브러리 설치
pnpm i -D velite
# html parsing할 rehype 라이브러리들 설치
pnpm i -D rehype-slug rehype-pretty-code rehype-autolink-headingspnpm rehype-prism-plus
# 기본 ui로 shadcn-ui 설치
pnpm dlx shadcn-ui@latest init\n
pnpm dlx shadcn-ui@latest add button sheet dropdown-menu badge pagination card
# 코드 문법을 위한 shiki, 경로를 설정을 위한 slugger 설치
pnpm i shiki github-slugger
  • tailwind config 설정

forms 태그의 초기값의 일관화를 위해 forms를 설치하고, 블로그 상세페이지의 css를 위해 typography를 설치하였습니다.

tailwind.config.jsjavascript
plugins: [require("@tailwindcss/forms"), require('@tailwindcss/typography')],
...
// 기타 tailwind 설정
  • velite를 사용하면 mdx파일을 ./.velite에 파싱해주기 때문에 자주 사용되는 paths를 설정했습니다.
tsconfig.jsonjson
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "#site/content": ["./.velite"],
    }
  • Next.js 프로젝트에서 Velite 웹팩 플러그인을 설정
next.config.jsjavascript
import { build } from "velite";

class VeliteWebpackPlugin {
  // 플러그인이 이미 시작되었는지 확인
  static started = false;
  constructor(options = {}) {
    this.options = options;
  }
  apply(compiler) {
    // executed three times in nextjs !!!
    // twice for the server (nodejs / edge runtime) and once for the client
    compiler.hooks.beforeCompile.tapPromise("VeliteWebpackPlugin", async () => {
      if (VeliteWebpackPlugin.started) return;
      VeliteWebpackPlugin.started = true;
      const dev = compiler.options.mode === "development";
      this.options.watch = this.options.watch ?? dev;
      this.options.clean = this.options.clean ?? !dev;
      await build(this.options); // start velite
    });
  }
}

/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack: (config) => {
    // 웹팩 설정에 VeliteWebpackPlugin 플러그인을 추가
    config.plugins.push(new VeliteWebpackPlugin());
    return config;
  },
};

export default nextConfig;

제가 설정한 .eslintrc.json, tsconfig.json, vscode setting은 아래와 같습니다.

.eslintrc.jsonjson
{
  "extends": "next/core-web-vitals",
  "rules": {
    "semi": ["error", "always"], // 코드 끝에 세미콜론이 항상 있어야 함을 나타냅니다.
    // "no-unused-vars": "warn", // 사용되지 않는 변수가 있을 경우 경고를 표시합니다.
    "eqeqeq": ["error", "always"], // 항상 === 또는 !==를 사용하여 등가 비교를 수행하도록 강제합니다.
    "indent": ["error", 2] // 들여쓰기를 스페이스 2개로 설정합니다.
  }
}
tsconfig.jsonjson
{
  "compilerOptions": {
    // "baseUrl": ".",
    "target": "ES6",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"],
      "#site/content": ["./.velite"],
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

vscode-setting.json
{
  "mdx.validate.validateReferences": "ignore",
  "typescript.updateImportsOnFileMove.enabled": "always", // mdx []링크 경고 무시
  "[mdx]": {"editor.quickSuggestions": false }, //mdx 자동완성 비활성화
}

Velite를 이용한 MDX에서 HTML로의 파싱 과정에 대해서는 다음 블로그 글에서 자세히 다루겠습니다.



연관된 포스트 구경가기

간략히