본문 바로가기

Frontend/Next

[Next.js] 마크다운 블로그 만들기 (feat. getStaticProps, getStaticPaths)

반응형

개요

이번 달 초, Next.js 원티드 프리온보딩에 참여하며 진행했던 과제를 정리해보고자 한다. 우선 next.js 12기준으로 pages 라우팅으로 마크다운 블로그를 만들어보고, 이를 13버전의 app 라우팅으로 마이그레이션하는 과정을 정리한 글이 될 것이다.

 

1. 프로젝트 생성

간단하게 CNA를 사용했다.

npx create-next-app --typescript

 

 

2. styled-components 설정

Next.js에서 styled-components을 사용하려면, 추가 설정이 필요하다. CSS-in-JS 라이브러리는 자바스크립트 런타임 시 적용이 되기 때문에, SSR에서 스타일 적용이 즉시 되지 않기 때문이다. 자바스크립트가 적용되기 전의 HTML 파일이 먼저 렌더링 되면서 스타일이 먹히지 않는 것처럼 보이는 것이다.

 

설치

yarn add styled-components@latest
npm i -D @types/styled-components

 

pages/_document.tsx

  • 정상적인 동작을 위해 _document.tsx를 생성하고 아래 코드를 넣어야 한다.
  • 이는 HTML head에 style태그를 주입하는 것으로 볼 수 있다. (참고)
import React from 'react';
import Document, { DocumentContext } from 'next/document';
import { ServerStyleSheet } from 'styled-components';

class MyDocument extends Document {
  static async getInitialProps(ctx: DocumentContext) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;
    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: (App) => (props) =>
            sheet.collectStyles(<App {...props} />),
        });
      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles} {sheet.getStyleElement()}
          </>
        ),
      };
    } finally {
      sheet.seal();
    }
  }
}

export default MyDocument;

 

 

3. SSG 구조 잡기

이제 블로그의 포스트 리스트를 보여주는 홈 페이지를 만들 것이다. 일단 포스트는 정적으로 가지고 있다고 가정하고, 모든 포스트를 SSG로 가져와 사용자가 접근했을 때 데이터가 바로 보여지도록 할 것이다.

 

pages/index.tsx

  • 홈 페이지에서 블로그의 포스트 리스트를 보여줄 것이다.
  • getStaticProps에서 API로 posts를 가져와 반환한다.
  • 그렇게 반환된 posts는 페이지 컴포넌트의 props로 넘어온다.
export const getStaticProps = () => {
  const posts = getAllPosts(); //API 가정
  return { props: { posts } };
};

const Home: NextPage<{ posts: Post[] }> = ({ posts }) => {
  return (
    <PostBoxes>
      {posts.map((post, index) => (
        <PostBox key={`${post.title}-${index}`} post={post} />
      ))}
    </PostBoxes>
  );
};

export default Home;

 

 

4. 정적 마크다운 포스트 가져오기

위에서 있다치고 넘어간 부분인 정적 포스트를 만들고, 이를 가져오는 API를 만들어보자.

 

정적 마크다운 포스트 생성

  • 우선 이 정적 리스트를 담아두기 위한 src/_posts 폴더를 생성한다.
  • 포스트의 형식은 Front Matter를 참고하면 된다.

 

예시) src/_posts/test.md

  • 상단에 ---로 구분되어 있는 부분은 현재 포스트에 접근할 때 사용될 변수들이라고 생각하면 된다. (메타 데이터 느낌)
  • 이후 내용은 마크다운으로 작성하면 된다.
---
title: 제목
author: zeo
preview: Lorem Ipsum
date: '2023-07-09'
---

## What is Lorem Ipsum?

Lorem Ipsum is dummy text.

 

API 만들기

마크다운 파일 가져오기위한 코드를 찾아보면 다양한듯 비슷한 코드를 많이 발견할 수 있다. 그리고 대표적으로 Next에서 제공하는 blog-starter에서 아래 코드를 확인할 수 있다. 해당 코드를 기준으로 조금씩 변형해서 사용하면 된다. (나는 그냥 가져다 씀 😀)

 

src/api/index.ts

import fs from 'fs';
import { join } from 'path';
import matter from 'gray-matter';

const postsDirectory = join(process.cwd(), 'src/_posts'); //루트를 기준으로 _posts폴더 위치 알리기

export function getPostSlugs() {
  return fs.readdirSync(postsDirectory);
}

export function getPostBySlug(slug: string, fields: string[] = []) {
  const realSlug = slug.replace(/\.md$/, ''); //파일명
  const fullPath = join(postsDirectory, `${realSlug}.md`); //해당 파일의 위치 찾기
  const fileContents = fs.readFileSync(fullPath, 'utf8'); //파일 가져오기
  const { data, content } = matter(fileContents); //마크다운 to 자바스크립트

  type Items = {
    [key: string]: string;
  };

  const items: Items = {}; //파일 데이터

  //---에서 설정한 변수 중 인자로 들어온 값만 데이터에 추가
  fields.forEach((field) => {
    if (field === 'slug') {
      items[field] = realSlug;
    }
    if (field === 'content') {
      items[field] = content;
    }
    if (typeof data[field] !== 'undefined') {
      items[field] = data[field];
    }
  });

  return items;
}

export function getAllPosts(fields: string[] = []) {
  const slugs = getPostSlugs();
  const posts = slugs.map((slug) => getPostBySlug(slug, fields));
  return posts;
}

 

src/utils/index.ts

import { remark } from 'remark';
import html from 'remark-html';

//이름에서 알 수 있듯, 마크다운을 HTML로 변환해주는 함수
export default async function markdownToHtml(markdown: string) {
  const result = await remark()
    .use(html as any)
    .process(markdown);
  return result.toString();
}

 

홈페이지에 API 제대로 연결

  • getAllPosts 부분만 수정하면 된다.
  • 필요한 정보(필드)만 인자로 넘겼다.
export const getStaticProps = () => {
  const posts = getAllPosts(['slug', 'title', 'author', 'preview', 'date']);
  return { props: { posts } };
};

홈페이지 결과

cf) 레이아웃 및 스타일 참고

 

 

5. 정적 경로 설정하기

홈 페이지에서 포스트 하나를 클릭했을 때, /[slug] 경로를 가지는 상세 페이지를 만들자.

 

pages/[slug].tsx

  • 정적 포스트 파일을 이용해 미리 경로를 생성할 수 있다. getStaticPaths를 활용해 각 포스트에 대응하는 경로를 만든다.
  • getStaticPaths에서 반환한 값은 getStaticProps로 넘어간다. 
  • getStaticProps에서는 현재 경로(slug)를 받아, 해당 포스트를 가져온다.
  • 마크다운인 content를 HTML에 삽입할 때는 dangerouslySetInnerHTML을 사용한다.
import type { NextPage } from 'next';
import { getAllPosts, getPostBySlug } from '../api';
import markdownToHtml from '../utils';

export const getStaticPaths = async () => {
  const posts = getAllPosts(['slug', 'title']);

  return {
    paths: posts.map((post) => ({ params: { slug: post.slug } })),
    fallback: false,
  };
};

export const getStaticProps = async ({ params: { slug } }: Parmas) => {
  const post = getPostBySlug(slug, ['slug', 'title', 'content']);
  const content = await markdownToHtml(post.content);

  return { props: { post: { ...post, content } } };
};

const Post: NextPage<{ post: Post }> = ({ post }) => {
  return (
    <>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </>
  );
};

export default Post;

 

 

결과 확인

  • test.md 파일이 /test경로로 보여지는 것을 확인할 수 있다.
  • 마크다운도 잘 보인다!

 

상세 페이지 결과

 

 

Reference

 

 

반응형