Next.js로 마크다운 블로그 만들기

임동현·2023년 7월 11일
0

원티드 온보딩 수업중 과제가 주어졌다.

Next.js로 마크다운으로 작성한 블로그를 정적 페이지(SSG)로 작성해주세요.

사실 나는 이미 나만의 블로그를 만들기 위해 따로 Next.js + Nest.js 로 구성된
웹블로그를 만들고 있는 와중에 내가 전혀 경험해보지 못한 , 블로그 이기에 관심을 갖고서
이 과제를 진행했다.

처음에는 markdown 이 뭐지 하면서 찾아봤다.
[출저] 허니몬 github

  1. 마크다운이란?
  • Markdown 은 텍스트 기반의 마크업언어로 2004년 존 그루버에 의해 만들어졌으며 쉽게 쓰고 읽을 수 있으며 html로 변환이 가능하다. 특수기호와 문자를 이용한 매우 간단한 구조의 문법을 사용하여 웹에서도 보다 빠르게 컨텐츠를 작성하고 보다 직관적으로 인식할수 있다.

    1) 장점

  • 간결하다.

  • 별도의 도구없이 작성이 가능하다.

  • 다양한 형태로 변환이 가능하다.

  • 텍스트로 저장되기 때문에 용량이 적어 보관이 용이하다.

  • 텍스트 파일이기 때문에 버전관리 시스템을 이용하여 변경이력을 관리할 수 있다.

  • 지원하는 프로그램과 플랫폼이 다양하다.

2) 단점

  • 표준이 없다.
  • 표준이 없기 때문에 도구에 따라서 변환방식이나 생성물이 다르다.
  • 모든 HTML 마크업을 대신하지 못한다.

간단한 장단점에 대해서만 보고 우리가 알고있는 github ReadMe 와 사용방법이 같구나
생각이 들었다.

⚙️ 블로그 만들기

npx create-next-app --typescript

프로젝트를 생성

Next.js 는 기본적으로 pre-render 를 한다.

pre-render 에는 SSG(Static Site Generation)와 SSR(Server Side Rendering), 2가지 방식이 있는데 , 둘의 차이점은 SSG 는 빌드 시에 데이터를 가져와서 HTML 파일을 미리 만들어두고 , SSR 은 서버에 요청이 있을 때, 데이터를 가져오고 HTML 파일을 만들어서 반환합니다 .

이번 과제에서는 SSG로 만드는 과제이기에 Next.js 에서 SSG 를 getStaticProps 메서드를
이용해서 손쉽게 할 수 있습니다.

? Post Page 를 만들지 않고 .md 마크다운 파일을 html 을 어떻게 그려줘야 하지 ? 생각이 들어 검색을 하다보니

[github]

대부분의 오픈소스 프로젝트들의 깃허브를 들어가면 , simple 이나 expample 폴더를 찾아 볼 수 있는데 거기서 다양한 프로젝트의 예제 코드를 찾아볼 수 있습니다.
Next.js 의 github example 폴더에 들어가보면 blog-starter 예제 코드가 있습니다.

import { PostData, PostMeta } from "@/types/types";
import fs from "fs";
import matter from "gray-matter";
import path from "path";
import { remark } from "remark";
import remarkHtml from "remark-html";

// 변수에 현재 작업 디렉토리의 경로와 '__posts' 폴더를 합쳐서 할당합니다.

const postsDir = path.join(process.cwd(), "__posts");

// getAllSortedPostsData()  함수는 '__posts' 폴더에 모든 파일의 이름을 가져와서 '.md' 확장자를 제거한후
// 각 파일의 내용을 읽어 옵니다. matter 패키지를 사용하여 파일 내용을 파싱하고 , 필요한 데이터와 함께 PostMeta 객체로 반환합니다.
// 이렇게 모든 포스트 데이터는 날짜를 기준으로 정렬되어 리턴됩니다.

export function getAllSortedPostsData(): PostMeta[] {
  const fileNames = fs.readdirSync(postsDir);
  const allPostsData = fileNames.map((fileName) => {
    const id = fileName.replace(/\.md$/, "");

    const fullPath = path.join(postsDir, fileName);
    const fileContents = fs.readFileSync(fullPath, "utf8");

    const matterResult = matter(fileContents);

    return { id, ...matterResult.data } as PostMeta;
  });

  return allPostsData.sort((a, b) => (a.date < b.date ? 1 : -1));
}

// getPostData 함수는 주어진 ID 를 기반으로 해당 포스트 데이터를 가져옵니다.
// ID 를 사용하여 해당 포스트의 '.md' 파일의 전체 경로를 결정하고 , 파일 내용을 읽어옵니다.
// 그리고 matter 패키지를 사용하여 파일 내용을 파싱하고 , remark , remarkHtml 을 사용하여 Markdown 파일 내용을 HTML 로 변환합니다.
// 이렇게 변환된 HTML 내용과 필요한 데이터를 포함한 PostData  객체로 반환합니다.

export async function getPostData(id: string): Promise<PostData> {
  const fullPath = path.join(postsDir, `${id}.md`);
  const fileContents = fs.readFileSync(fullPath, "utf8");

  const matterResult = matter(fileContents);

  const processedResult = await remark()
    .use(remarkHtml)
    .process(matterResult.content);
  const contentHtml = processedResult.toString();

  return {
    id,
    contentHtml,
    ...matterResult.data,
  } as PostData;
}

PostData, PostMeta Types

export type PostMeta = {
  id: string;
  title: string;
  date: string;
  author: string;
  description?: string;
  tags?: string[];
  categories?: string[];
};

export type PostData = PostMeta & {
  contentHtml: string;
};

pages/index.tsx

// getStaticProps 함수는 정적 생성을 위해 서버측에 호출되는 함수입니다. 
// get AllSortedPostsData 함수를 사용하여 모든 포스트 데이터를 가져와서 
// 변수 allPostsData 로 할당합니다. 그리고 이를 props 객체에 담아서 반환합니다. 
export const getStaticProps: GetStaticProps = () => {
  const allPostsData = getAllSortedPostsData();

  return {
    props: {
      allPostsData,
    },
  };
};
export default function Home({ allPostsData }: Props) {
console.log(allPostsData)
  return (
    <>
      <Head>
        <title>Create Next App11111</title>
        <meta name="description" content="Generated by create next app" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <div className={styles.main}>
        {allPostsData.map((post, index) => (
          <Link href={`/${post.id}`} key={index} className={styles.card}>
            <h2>{post.title}</h2>
            <p>{post.author}</p>
            <p>{post.date}</p>
            <p>Read More</p>
          </Link>
        ))}
      </div>
    </>
  );
}

Home 컴포넌트에 allPostsData 를 매개변수로 받아 map함수를 사용하여 뿌려줍니다.

console.log(allPostsData)

pages/[id].tsx

import { PostData } from "@/types/types";
import { getAllSortedPostsData, getPostData } from "@/utils/posts";
import { GetStaticProps, GetStaticPaths } from "next";
import React from "react";
import styles from "@/styles/sass/styles.module.scss";

interface BlogPostPageProps {
  postData: PostData;
}

// <div className={styles.content}>는 포스트의 내용을 나타내는 부분입니다. dangerouslySetInnerHTML 속성을 사용하여
//  HTML 문자열을 동적으로 설정합니다. 이는 해당 포스트의 HTML 내용을 그대로 출력하기 위해 사용됩니다.
export default function BlogPostPage({ postData }: BlogPostPageProps) {
  console.log(postData, " postData : 는 현재 뭐가 나오고있어 ");
  return (
    <div className={styles.container}>
      <h1 className={styles.title}>{postData.title}</h1>
      <div
        className={styles.content}
        dangerouslySetInnerHTML={{ __html: postData.contentHtml }}
      />
    </div>
  );
}

// getStaticPaths 함수를 통해서 , 이 함수는 동적 라우팅을 위해 필요한 경로를 생성합니다.
// getAllSotrtedPostsData 함수를 통해 모든 포스트 데이터를 가져옵니다.
// map 함수를 사용하여 , 각 포스트의 id 를 사용하여 경로 객체를 생성합니다. 이렇게 생성된  경로들은 동적 라우팅에 사용됩니다.
// { paths, fallback: false }; 를 반환하며 생성된 경로와 fallback 값을 설정합니다. 여기서는 fallback 을 false 로 설정하여
// 없는 경로로 접근시 404 페이지로 처리 되도록 합니다.

export const getStaticPaths: GetStaticPaths = async () => {
  const allPostsData = await getAllSortedPostsData();
  const paths = allPostsData.map((data) => ({
    params: { id: data.id }, // 각 페이지에 필요한 파라미터를 지정
  }));

  return { paths, fallback: false };
};

// getStaticProps 함수는 빌드 시점에 페이지에 필요한 데이터를 가져옵니다. params 를 사용하여 요청된 포스트의 id 를 추출합니다.
// getPostData 함수를 사용하여 id 에 해당하는 포스트 데이터를 가져옵니다.
// postData 를 props 객체에 담아 반환합니다.

export const getStaticProps: GetStaticProps<BlogPostPageProps> = async ({
  params,
}) => {
  const id = params?.id as string;
  const postData = await getPostData(id);

  return {
    props: {
      postData,
    },
  };
};

profile
프론트엔드 공부중

0개의 댓글