원티드 온보딩 수업중 과제가 주어졌다.
Next.js로 마크다운으로 작성한 블로그를 정적 페이지(SSG)로 작성해주세요.
사실 나는 이미 나만의 블로그를 만들기 위해 따로 Next.js + Nest.js 로 구성된
웹블로그를 만들고 있는 와중에 내가 전혀 경험해보지 못한 , 블로그 이기에 관심을 갖고서
이 과제를 진행했다.
처음에는 markdown 이 뭐지 하면서 찾아봤다.
[출저] 허니몬 github
Markdown 은 텍스트 기반의 마크업언어로 2004년 존 그루버에 의해 만들어졌으며 쉽게 쓰고 읽을 수 있으며 html로 변환이 가능하다. 특수기호와 문자를 이용한 매우 간단한 구조의 문법을 사용하여 웹에서도 보다 빠르게 컨텐츠를 작성하고 보다 직관적으로 인식할수 있다.
1) 장점
간결하다.
별도의 도구없이 작성이 가능하다.
다양한 형태로 변환이 가능하다.
텍스트로 저장되기 때문에 용량이 적어 보관이 용이하다.
텍스트 파일이기 때문에 버전관리 시스템을 이용하여 변경이력을 관리할 수 있다.
지원하는 프로그램과 플랫폼이 다양하다.
2) 단점
간단한 장단점에 대해서만 보고 우리가 알고있는 github ReadMe 와 사용방법이 같구나
생각이 들었다.
npx create-next-app --typescript
프로젝트를 생성
pre-render 에는 SSG(Static Site Generation)와 SSR(Server Side Rendering), 2가지 방식이 있는데 , 둘의 차이점은 SSG 는 빌드 시에 데이터를 가져와서 HTML 파일을 미리 만들어두고 , SSR 은 서버에 요청이 있을 때, 데이터를 가져오고 HTML 파일을 만들어서 반환합니다 .
이번 과제에서는 SSG로 만드는 과제이기에 Next.js 에서 SSG 를 getStaticProps 메서드를
이용해서 손쉽게 할 수 있습니다.
대부분의 오픈소스 프로젝트들의 깃허브를 들어가면 , 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,
},
};
};