이 글은Next.js의 공식 튜토리얼 문서를 참고하여 작성한 글입니다.


 

인덱스 페이지를 만들었지만 아직 블로그의 개인 포스트 페이지는 만들지 않았다. 이제 dynamic routes를 통해 각 포스트 페이지마다 고유의 URL을 갖게 해보자.

외부 데이터에 의존하는 페이지 경로

이전 글에서 getStaticProps를 사용하여 인덱스 페이지를 만들면서 블로그 콘텐츠가 외부 데이터에 의존하는 경우를 다루었다.

 

이 글에서는 각 페이지 경로가 외부 데이터에 의존하는 경우에 대해 다룰 것이다. Next.js는 외부 데이터에 의존하는 페이지를 경로와 함께 정적으로 생성하게 해준다. 이를 가능하게 하는 것이 dynamic URLs이다.

 

 

그렇다면 어떻게 페이지를 Dynamic Routes를 사용하여 정적 생성할까?

 

블로그에서 각 포스트의 경로가 /posts/<id>로 지정되길 원한다고 하면, <id>posts디렉토리 안에 있는 마크다운 파일명이 될 것이다.

예를 들어 이전 글에서 ssg-ssr.mdpre-rendering.md를 만들어 놨으므로, 해당 글의 경로는 각각 /posts/ssg-ssr.md/posts/pre-rendering이 될 것이다.

 

getStaticPaths

먼저 pages/posts안에 [id].js파일을 만든다. Next.js에서 dynamic routes를 사용하는 페이지는 []로 감싸져있어야 한다.

first-post.js파일은 더이상 사용하지 않기 때문에 삭제해주자.

 

pages/posts/[id].js파일을 열어 일단 다음과 같이 작성해주자.

 

import Layout from "../../components/layout";

export default function Post() {
  return <Layout>...</Layout>;
}

 

다음으로 lib/posts.js에서 getAllPostIds함수를 밑에 추가 해준다. 이 함수는 posts디렉토리 안에 있는 파일명 리스트를 리턴할 것이다.

 

export function getAllPostIds() {
  const fileNames = fs.readdirSync(postsDirectory);

  // 아래와 같은 형태로 파일명 리스트를 리턴한다.
  // [
  //   {
  //     params: {
  //       id: 'ssg-ssr'
  //     }
  //   },
  //   {
  //     params: {
  //       id: 'pre-rendering'
  //     }
  //   }
  // ]
  return fileNames.map(fileName => {
    return {
      params: {
        id: fileName.replace(/\.md$/, ""),
      },
    };
  });
}

 

위 코드에서 볼 수 있듯이 리턴되는 리스트는 문자열 형태가 아니라 객체 형태여야 한다. 각 객체는 params라는 키와 id키를 포함하고 있다. 그렇지 않으면 getStaticPaths함수는 정상적으로 동작하지 않을 것이다.

 

마지막으로 getAllPostIds함수를 import하고 getStaticPaths함수 내부에서 사용한다. pages/posts/[id].js에서 Post컴포넌트 위에 다음과 같이 작성한다.

 

import { getAllPostIds } from "../../lib/posts";

export async function getStaticPaths() {
  const paths = getAllPostIds();
  return {
    paths,
    fallback: false,
  };
}

 

이렇게 하면 paths에는 pages/posts/[id].js에서 정의한 params를 포함하는 getAllPostIds에서 리턴한 배열이 담겨 있을 것이다.

 

getStaticProps

이제 id를 통해 포스트에 필요한 데이터를 가져올 필요가 있다.

이를 위해서 lib/posts.jsgetPostData함수를 추가해주자. 이 함수는 id에 맞는 포스트 데이터를 리턴할 것이다.

 

export function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`);
  const fileContents = fs.readFileSync(fullPath, "utf8");

  // 포스트의 메타데이터를 파싱하기 위해 gray-matter 사용
  const matterResult = matter(fileContents);

  // 데이터를 id 와 병합시킨다.
  return {
    id,
    ...matterResult.data,
  };
}

 

그리고 pages/posts/[id].js를 다음과 같이 수정한다.

 

import { getAllPostIds, getPostData } from "../../lib/posts";

export async function getStaticProps({ params }) {
  const postData = getPostData(params.id);
  return {
    props: {
      postData,
    },
  };
}

 

이제 포스트 페이지는 getStaticProps안에 있는 getPostData를 통해 데이터를 받아올 수 있고 props로 리턴할 수도 있다.

 

// [id].js

export default function Post({ postData }) {
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
    </Layout>
  );
}

 

이제 서버를 실행시켜 확인해보자.

 

http://localhost:3000/posts/ssg-ssr
http://localhost:3000/posts/pre-rendering

 

다음은 지금까지의 과정을 요약한 것이다.

 

 

Markdown 사용

마크다운 문법을 사용하기 위해 remark라이브러리를 설치한다.

 

npm install remark remark-html

 

lib/posts.js에서 다음과 같이 import시킨 후, getPostData함수를 수정해준다.

 

import remark from "remark";
import html from "remark-html";

...

export async function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`);
  const fileContents = fs.readFileSync(fullPath, "utf8");

  // 포스트의 메타데이터를 파싱하기 위해 gray-matter 사용
  const matterResult = matter(fileContents);

  // remark 를 사용하여 마크다운을 HTML로 변환
  const processedContent = await remark()
    .use(html)
    .process(matterResult.content)
  const contentHtml = processedContent.toString()

  // 데이터를 id 와 병합시킨다.
  return {
    id,
    contentHtml,
    ...matterResult.data,
  };
}

 

remark를 사용하기 위해 getPostData함수에서 async await을 사용했다. 따라서 pages/posts/[id].jsgetStaticProps에서 getPostData를 호출할 때 await를 붙여 호출한다.

 

export async function getStaticProps({ params }) {
  const postData = await getPostData(params.id);
  return {
    props: {
      postData,
    },
  };
}

 

마지막으로 pages/posts/[id].jsPost컴포넌트에서 contentHtml을 렌더링하기 위해 dangerouslySetInnerHTML를 사용한다.

 

export default function Post({ postData }) {
  return (
    <Layout>
      {postData.title}
      <br />
      {postData.id}
      <br />
      {postData.date}
      <br />
      <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
    </Layout>
  );
}

 

 

그럼 결과를 확인 해보자.

 

 

다음으로는 포스트 페이지에 스타일도 추가하고 title도 추가하면서 더 다듬어 보자.

[id].jstitle을 추가하기 위해 next/head에서 Head컴포넌트를 추가하고 다음과 같이 Post컴포넌트를 수정한다.

 

export default function Post({ postData }) {
  return (
    <Layout>
      <Head>
        <title>{postData.title}</title>
      </Head>
      <br />
      {postData.id}
      <br />
      {postData.date}
      <br />
      <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
    </Layout>
  );
}

 

포스트 날짜를 나타내기 위해 date-fns라이브러리를 설치해준다.

 

npm install date-fns

 

그리고 components/date.js파일을 만들어 Date컴포넌트를 생성한다.

 

import { parseISO, format } from "date-fns";

export default function Date({ dateString }) {
  const date = parseISO(dateString);
  return <time dateTime={dateString}>{format(date, "LLLL d, yyyy")}</time>;
}

 

pages/posts/[id].js에서 Date컴포넌트를 import한 후, 다음과 같이 사용한다.

 

export default function Post({ postData }) {
  return (
    <Layout>
      <Head>
        <title>{postData.title}</title>
      </Head>
      <br />
      {postData.id}
      <br />
      <Date dateString={postData.date} />
      <br />
      <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
    </Layout>
  );
}

 

마지막으로 이전에 했던 것처럼 styles/utils.module.css파일을 사용하여 CSS를 추가해보자.

[id].js에서 CSS파일을 import시킨 후, Post컴포넌트를 다음과 같이 수정한다.

 

export default function Post({ postData }) {
  return (
    <Layout>
      <Head>
        <title>{postData.title}</title>
      </Head>
      <article>
        <h1 className={utilStyles.headingXl}>{postData.title}</h1>
        <div className={utilStyles.lightText}>
          <Date dateString={postData.date} />
        </div>
        <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
      </article>
    </Layout>
  );
}

 

http://localhost:3000/posts/pre-rendering

 

포스트 페이지를 다듬었으니 홈에 있는 다음은 인덱스 페이지를 다듬을 차례이다.

먼저, 각 포스트 페이지에 Link컴포넌트를 통해 링크를 걸어주자.

 

pages/index.js에서 LinkDate를 가져온다.

 

import Link from "next/link";
import Date from "../components/date";

 

그리고 Home컴포넌트에서 li태그에 있는 내용을 다음과 같이 바꿔준다.

 

<li className={utilStyles.listItem} key={id}>
  <Link href={`/posts/${id}`}>
    <a>{title}</a>
  </Link>
  <br />
  <small className={utilStyles.lightText}>
    <Date dateString={date} />
  </small>
</li>

 

http://localhost:3000/

 

 

 

 

'Next.js' 카테고리의 다른 글

[Next.js] Pre-rendering과 Data Fetching  (2) 2021.04.09
[Next.js] Assets, Metadata와 CSS  (1) 2021.04.07
[Next.js] 페이지 간 이동하기  (0) 2021.04.06
[Next.js] Next.js 프로젝트 시작하기  (0) 2021.04.06

생강강

,