본문 바로가기

공부일지/Project

비전공자의 프로젝트 만들기) Blog - MarkDown, NextJS, TypeScript, React, Remark, RemarkHtml

728x90
반응형

블로그 프로젝트 만들어보기

 

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폴더

제일 상단폴더에 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 컴포넌트에서 전달받고 타입을 정의 해준 뒤 데이터를 뿌려 주면 된다.

 

간단한 블로그 프로젝트도 생각보다 적을 양이 많아서 놀랬다 만들 때는 오래 걸리지 않았지만 내용 적는 시간이 더 오래 걸린 거 같다.

 

728x90
반응형