블로그 프로젝트 만들어보기
cna(create-next-app)에 TypeScript 템플릿을 적용해서 설치해주었다.
markdown
Markdown은 텍스트 기반의 마크업 언어로 쉽게 쓰고 읽을 수 있으며 HTML로 변환이 가능하다. 특수기호와 문자를 이용한 매우 간단한 구조의 문법을 사용하여 웹에서도 보다 빠르게 컨텐츠를 작성하고 보다 직관적으로 인식할 수 있다. 마크다운이 최근 각광받기 시작한 이유는 깃헙에서 사용하는 ReadME.md 덕분인데 마크다운을 통해 설치방법, 소스코드 설명, 이슈 등을 간단하게 기록하고 가독성을 높일 수 있다는 강점이 부각되었다.
예제 md파일은 NextJS 블로그 만들기에서 가져왔다.
https://nextjs.org/learn/basics/data-fetching/blog-data
Learn | Next.js
Production grade React applications that scale. The world’s leading companies use Next.js by Vercel to build pre-rendered applications, static websites, and more.
nextjs.org
제일 상단폴더에 posts폴더를 생성 후 md파일을 넣어주었다.
Markdown파일 데이터로 추출하기
lib(library) 폴더를 생성 후 post.ts파일을 생성한다.
import fs from "fs";
import path from "path";
import matter from "gray-matter";
import { remark } from "remark";
import remarkHtml from "remark-html";
const postsDirectory = path.join(process.cwd(), "posts");
export function getSortedPostsData() {
// /posts파일 이름 잡아주기
const fileNames = fs.readdirSync(postsDirectory);
const allPostsData = fileNames.map((fileName) => {
const id = fileName.replace(/\.md$/, "");
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, "utf8");
const matterResult = matter(fileContents);
return {
id,
...matterResult.data as { date: string; title: string },
};
});
// Sorting
return allPostsData.sort((a, b) => {
if (a.date < b.date) {
return 1;
} else {
return -1;
}
});
}
블로그 프로젝트를 완성 후 작성하는 거라 해당 코드를 복사 붙여 넣기 하면 오류가 생길 수도 있다.
먼저 path는 설명하자면 폴더경로 라고 하는 게 조금 쉽게 이해될 수 있을 것 같다.
자세한 설명은 해당 url을 찾아보면 된다. 나도 정확히 알지 못해서 해당 페이지에서 이해를 하였다.
https://developer.mozilla.org/ko/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL
URL이란? - Web 개발 학습하기 | MDN
이 문서에서는 URL(Uniform Resource Locator)이 무엇이며 어떻게 구성되어 있는지 설명합니다.
developer.mozilla.org
코드설명을 이어서 하면
process.cwd는 우리가 작업하고 있는 폴더 경로가 나타난다.
일반적인 웹사이트를 말하면 https://min-webstory.tistory.com/ 이렇게 나온다.
거기에 우리가 만든 posts폴더를 join 즉 합쳐 준다.
process.cwd()는 node.js 명령어라고 한다.
fs.readdirSync 지정된 디렉토리에 내용을 동기화하여 읽은 후 파일 이름을 또는 개체를 포함하는 배열을 반환한다.
우린 위에서 디렉토리의 경로를 구했고 해당 경로를 readdirSync를 통해 해당 폴더에 있는 파일 리스트의 배열을 받는다.
const allPostsData = fileNames.map((fileName) => {
const id = fileName.replace(/\.md$/, "");
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, "utf8");
const matterResult = matter(fileContents);
return {
id,
...matterResult.data as { date: string; title: string },
};
});
받은 배열을 map을 사용해 하나씩 가져와 replace를 통해.md라는 확장자를 없애주고 파일명만 id에 저장한다.
그리고 다시 한번 path를 사용해서 파일명의 정보를 utf8으로 인코딩을 해준 후
md파일을 데이터로 변환해주기 위해서는 gray-matter이라는 패키지가 필요하다.
https://www.npmjs.com/package/gray-matter
gray-matter
Parse front-matter from a string or file. Fast, reliable and easy to use. Parses YAML front matter by default, but also has support for YAML, JSON, TOML or Coffee Front-Matter, with options to set custom delimiters. Used by metalsmith, assemble, verb and .
www.npmjs.com
그런데 모든 md파일에 있는 컨텐츠가 아무런 규칙 없이 데이터로 변환되는 건 아니고 title, slug, date나 object처럼 key, value로 되어있는 부분이 있고 나머지 부분이 있는데 key, value는 변환 시 그에 맞는 key와 value로 자동 선언 되고 나머지는 content로 바뀐다.
그리고 우리가 구한 id와 matterResult를 반환해 준다.
typescript의 as 같은 경우는 추후에 더 자세히 알아봐야겠다. 궁금하신 분들은 typescript as를 검색해 봐도 좋을 거 같다.
sort를 사용해서 date를 비교한 이유는 최신글이 상단으로 오게 하기 위해 sort를 사용하여 정렬을 해주었다. if을 안 쓰고 그냥 sort()을 사용할 경우 오름차순으로 정렬하고 따로 a < b를 하면 내림차순으로 할 수 있다.
index.tsx에 리스트를 적용시켜 보았다.
import { GetStaticProps, NextPage } from 'next';
import Head from "next/head";
import Link from 'next/link';
import { Inter } from "next/font/google";
import homeStyles from "../styles/Home.module.css";
import { getSortedPostsData } from '../lib/post';
const inter = Inter({ subsets: ["latin"] });
const Home = ({allPostData} : {
allPostData: {
date:string
title:string
id:string
}[]
}) => {
return (
<div className={homeStyles.container}>
<Head>
<title>Create Next App</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>
<section className={homeStyles.headingMd}>
<p>[John Ahn Introduction]</p>
<p>(This is a website)</p>
</section>
<section className={`${homeStyles.headingMd} ${homeStyles.padding1px}`}>
<h2 className={homeStyles.headingLg}>Blog</h2>
<ul className={homeStyles.list}>
{
allPostData.map(({id, title, date}) =>
<li className={homeStyles.listItem} key={id}>
<Link href={`/posts/${id}`}>{title}</Link>
<br/>
<small className={homeStyles.lightText}>{date}</small>
</li>
)
}
</ul>
</section>
</div>
);
}
export default Home
export const getStaticProps: GetStaticProps = async () => {
const allPostData = getSortedPostsData();
return {
props: {
allPostData
}
}
}
GetStaticProps를 사용해 post.tsx파일에서 만들었던 getSortedPostsData를 불러와 props로 return 해준다.
그리고 Home에서 allPostData를 받아오고 타입을 정의해 준다.
allPostData는 배열 안에 있기 때문에 배열이라고도 꼭 알려주어야 한다.
ul안에 allPostData를 map으로 하나씩 뿌려준다.
file system
리액트에서는 route를 위해서 rect-router라는 라이브러리를 사용하였다.
NextJS에는 페이지 개념을 기반으로 구축된 파일 시스템 기반 라우터가 있어 파일이 페이지 디렉토리에 추가되면 자동으로 경로를 사용할 수 있다.
페이지 디렉토리 내의 파일은 가장 일반적인 패턴을 정의하는 데 사용할 수 있다.
- 파일 생성 예시
pages/index.js → /
pages/blog/index.js → /blog
pages/blog/first-post.js → /blog/first-post
pages/dashboard/settings/username.js → /dashboard/settings/username
일반적으로 웹사이트를 만들 때 보던 패턴과 비슷하다. 지금 다니는 회사에서 jsp를 사용하는데 jsp에서 페이지 보여는 주는 방식과 많이 비슷한 거 같다.
자세히 기억은 안 나지만 web.ini였나 페이지들을 관리하던 파일이 있었는데 거기서도 저렇게 비슷하게 유저들에게 보여주는 기능을 하는 파일이었다.
pages/blog/[slug].js → /blog/:slug(/blog/hello-wrold)
pages/[username]/settings.js → /:username/settings(/foo/settings)
pages/post/[...all].js → /post*(/post/2020/id/title)
Dynamic Routes인데 처음에는 정확히 어떤 개념인지 이해가 잘 안 되었다. 그냥 폴더 내에 하위폴더들을 한 번에 선언하는 건 줄 알았는데
NextJS 공식 사이트에 블로그 만들기를 예를 들어 보면 제목을 클릭하면 상세 페이지로 넘어가야 되는데 현재 상세페이지 정보는 알고 있지만 제목별로 하나하나 링크를 걸 수 없기 때문에
{
allPostData.map(({id, title, date}) =>
<li className={homeStyles.listItem} key={id}>
<Link href={`/posts/${id}`}>
<a>{title}</a>
</Link>
<br/>
<small className={homeStyles.lightText}>{date}</small>
</li>
)
}
이렇게 /posts의 폴더 안에 있는 id값에 맞는 페이지로 이동하라는 코드이다.
만약 `/posts/${id}`를 사용하지 않으면 /posts/1.js, /posts/2.js, /posts/3.js 이런 식으로 하나하나 링크 작업을 해주어야 한다.
pages폴더에 posts폴더를 생성 후 [id].tsx파일을 만들었다.
완성된 코드를 작성 후 하나씩 알아가 보자
[id].tsx
import { GetStaticPaths, GetStaticProps } from "next";
import Head from "next/head";
import React from "react";
import { getAllPostIds, getPostData, } from "../../lib/post";
import postStyle from "../../styles/Post.module.css";
const Post = ({postData,}: {
postData: {
title: string;
date: string;
contentHtml: string;
}
}) => {
return (
<div className={postStyle.container}>
<Head>
<title>{postData.title}</title>
</Head>
<article>
<h1 className={postStyle.headingXl}>{postData.title}</h1>
<div>{postData.date}</div>
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</article>
</div>
);
};
export default Post;
export const getStaticPaths: GetStaticPaths = async () => {
const paths = getAllPostIds();
// [{params: {id:'pre-rendering', {parmas...}}}]
return {
paths,
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const postData = await getPostData(params.id as string);
return {
props: {
postData
}
};
};
GetStaticPaths
동적 라우팅이 필요할 때 getStaticPaths로 경로 리스트를 정의하고 HTML에 build 시간에 렌더 된다.
NextJS는 pre-render에서 정적으로 getStaticPaths에서 호출하는 경로들을 가져온다.
lib에 만들었던 post.ts파일에서 ID를 전달할 수 있게 아래의 코드를 작성해준다.
export function getAllPostIds() {
const fileNames = fs.readdirSync(postsDirectory);
console.log('fileNames',fileNames)
return fileNames.map(fileName => {
return {
params: {
id: fileName.replace(/\.md$/, ''),
},
};
});
}
보면 위에서 상세 보기 도메인으로 가기 위해 작업하던 path부분과 비슷하다. 아이디값을 전달한다.
export const getStaticPaths: GetStaticPaths = async () => {
const paths = getAllPostIds();
// [{params: {id:'pre-rendering', {parmas...}}}]
return {
paths,
fallback: false,
};
};
먼저 paths에 getAllPostIds를 받아온다.
fallback은 Static에서 전달받은 url에서 해당 페이지가 없으면 자동으로 404페이지가 나오도록 하는 옵션이다.
true로 하면 원하는 404페이지를 지정할 수 있다.
GetStaticProps
page에 데이터를 가져오는 데 사용한다.
export const getStaticProps: GetStaticProps = async ({ params }) => {
const postData = await getPostData(params.id as string);
return {
props: {
postData
}
};
};
이번에도 post.ts에서 getPostData를 만들어서 postData에 data를 전달해주어야 한다.
export async function getPostData(id: string) {
const fullPath = path.join(postsDirectory, `${id}.md`);
const fileContents = fs.readFileSync(fullPath, "utf8");
const matterResult = matter(fileContents);
const processedContent = await remark()
.use(remarkHtml)
.process(matterResult.content);
const contentHtml = processedContent.toString();
return {
id,
contentHtml,
...(matterResult.data as { date: string; title: string })
};
}
리스트페이지 만들 때랑 비슷한 점이 많다.
id값을 전달받으면 제 일위에서 정의해 두었던 postsDirectory에 ${id}.md 를 선언해 주고 readFileSync를 통해 파일을 인코딩하고 matter를 통해 데이터화해주고 remark 패키지를 사용해야 된다.
npm install remark remark-html
위의 npm으로 터미널에서 설치해 준다.
위의 패키지가 무슨 역할을 하냐면 받아온 데이터를 html화를 시켜준다. 즉 html에 필요한 태그를 추가해 준다.
remark의 각 속성에 대해서는 나도 정확히는 모르겠다.
https://www.npmjs.com/package/remark
remark
unified processor with support for parsing markdown input and serializing markdown as output. Latest version: 14.0.2, last published: a year ago. Start using remark in your project by running `npm i remark`. There are 870 other projects in the npm registry
www.npmjs.com
https://www.npmjs.com/package/remark-html
remark-html
remark plugin to compile Markdown to HTML. Latest version: 15.0.2, last published: 3 months ago. Start using remark-html in your project by running `npm i remark-html`. There are 290 other projects in the npm registry using remark-html.
www.npmjs.com
remarkHtml은 html로 변환할 때 사용한다고 한다.
process에는 matterResult.content를 넣어주면 된다.
마지막으로 remark한걸 string으로 변환 후 id와 content와 data를 return 해 준다.
이제 받은 데이터를 잘 사용하기만 하면 된다.
[id].tsx
import { GetStaticPaths, GetStaticProps } from "next";
import Head from "next/head";
import React from "react";
import { getAllPostIds, getPostData, } from "../../lib/post";
import postStyle from "../../styles/Post.module.css";
const Post = ({postData,}: {
postData: {
title: string;
date: string;
contentHtml: string;
}
}) => {
return (
<div className={postStyle.container}>
<Head>
<title>{postData.title}</title>
</Head>
<article>
<h1 className={postStyle.headingXl}>{postData.title}</h1>
<div>{postData.date}</div>
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</article>
</div>
);
};
export default Post;
export const getStaticPaths: GetStaticPaths = async () => {
const paths = getAllPostIds();
// [{params: {id:'pre-rendering', {parmas...}}}]
return {
paths,
fallback: false,
};
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const postData = await getPostData(params.id as string);
return {
props: {
postData
}
};
};
최종 코드이다 postData를 전달받고 그걸 Post 컴포넌트에서 전달받고 타입을 정의 해준 뒤 데이터를 뿌려 주면 된다.
간단한 블로그 프로젝트도 생각보다 적을 양이 많아서 놀랬다 만들 때는 오래 걸리지 않았지만 내용 적는 시간이 더 오래 걸린 거 같다.
'공부일지 > Project' 카테고리의 다른 글
비전공자의 프로젝트 만들기) Disneyplus App 만들기 part3 (0) | 2023.05.23 |
---|---|
비전공자의 프로젝트 만들기) disneyplus App 만들기 part2 (0) | 2023.05.22 |
비전공자의 프로젝트 만들기) disneyplus App 만들기 part1 - 구조만들기 (0) | 2023.05.18 |
비전공자의 프로젝트 만들기) TicTacToeApp (Function Type) (0) | 2023.05.17 |
비전공자의 프로젝트 만들기) TicTacToe App (Class Type) (0) | 2023.05.16 |