본문 바로가기

Next.js9

Next.js - 페이지 자동 생성하기 - Dynamic Routes (동적 경로)

지금까지 첫페이지에 블로그 제목 리스트가 나오도록 작성했지만, 각 페이지들을 만들지는 않았었어!

페이지들의 URL을 만들고, 요기 접속하면, 각각의 페이지가 나오는 실습을 할꺼야!

한마디로, Dynamic Routes(동적 경로) 기능을 사용 한다는 말씀!

 

데이터를 이용해 페이지 Path 만들기

이전 튜토리얼에서 페이지 컨텐츠가 외부 데이터에 의존 하는 걸 다뤘어! 'getStaticProps' 함수를 사용해서 첫 페이지에 데이터를 가져왔었지.

이번에는 첫페이지가 아니라 각 페이지 path별로 데이터를 가져오려고 해!

Next.js가 외부 데이터를 기반으로 페이지들을 고정적으로 만들수 있다는 건 이전 Pre-Rendering 강좌에서 봤고, 요 기능 덕분에 Next.js가 동적(Dynamic)으로 URL을 생성할 수 있어.

 

뭐가 어떻게 된다는 말이야?

'http://localhost:3000/posts/bumuk-zzikmuk'이라고 브라우저에 입력하면 이전실습에서 만들었던 'bumuk-zzikmuk.md' 파일 내용이 나와!

마찬가지로 'http://localhost:3000/posts/pre-rendering'이라고 입력하면 'posts/pre-rendering.md' 파일이 나오는 거지

오키?

 

어떻게 하면 돼?

실습이 길 수 있으니 미리한번 설명하고 갈께. 뒤에 자세히 진행할꺼니 전체 흐름만 보면 될꺼야!

  1. 먼저 '[id].js'라는 이름으로 페이지 컴포넌트를 만들어 줄꺼야. 파일 이름이 신기하지? 꺽쇠 '[', ']'를 사용하는 페이지들을 'Dynamic Page' 라고 불러. 각 글에 대한 페이지 컴포넌트 코드 추가!
  2. 'getStaticPaths' 추가! 함수 안에 id에 들어갈 값들의 리스트를 넘길꺼야. 
  3. 'getStaticProps' 추가! Static-Generation용 함수 기억나지? 이번엔, id값을 가져오기 위해서 사용할꺼고 params객체에서 id값을 가져와서 페이지들을 미리 생성함!

그럼 본격적으로 시작해볼까?

getStaticPaths 구현!

이름에 static이란 말이 있는 걸 보니 뭔가 고정적으로 호출할 경로를 만들기 위한 함수라는 거 추측이 되지?

이 함수는 getStaticProps로 페이지 컴포넌트들을 생성할때도 사용하고, 요 함수를 통해 URL과 생성된 페이지를 연결 시켜주는 역할을 해.

'[id].js'을 pages/posts 디렉토리 안에 만들자. 

import Layout from '../../components/layout'

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

 <Layout> 컴포넌트 사이의 '...' 는 아래에서 내용을 넣을꺼야!

파일명 추출 함수 추가!

함수들을 모아놓기로한 lib/posts.js 파일에 코드에 추가!

  // 아래와 같은 배열이 리턴됩니다.
  // [
  //   {
  //     params: {
  //       id: 'bumuk-jjickmuk'
  //     }
  //   },
  //   {
  //     params: {
  //       id: 'pre-rendering'
  //     }
  //   }
  // ]
export function getAllPostIds() {
  const fileNames = fs.readdirSync(postsDirectory)
  return fileNames.map(fileName => {
    return {
      params: {
        id: fileName.replace(/\.md$/, '')
      }
    }
  })
}

이상하지? 왜 복잡하게 객체를 만든 걸까? id값만 있는 배열이 리턴되는게 아니고 params에 id 키에다가 값을 넣었어!

요 id값은 위에서 파일명에 사용했던 '[id].js'의 id값을 의미해. 그래서 요렇게 작성하지 않으면 'getStaticPaths' 함수에서 오류가 날꺼야.

'[id].js' 에서 import하자

import { getAllPostIds } from '../../lib/posts'

 

getStaticPaths 메서드 추가!

만든 getStaticPaths함수 넣기!

export async function getStaticPaths() {
  const paths = getAllPostIds()
  return {
    paths, // paths: paths와 동일
    fallback: false
  }
}

id 값이 들어있는 배열은 'paths' 라는 키에 값으로 들어가야해!

fallback 값에 대해서는 아래에서 설명할께!

 

getStaticProps 메서드 구현!

이미 알겠지만, 페이지를 미리 생성할때 사용하는 메서드!

id값을 기준으로 마크다운 파일을 읽어서 페이지 렌더링에 필요한 정보들을 넘겨줄꺼야.

'lib/posts.js'에 함수로 추가하자!

// id값(파일명)으로 마크다운 파일을 읽기
export function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')

  // gray-matter를 사용해 메타데이터 파싱!
  const matterResult = matter(fileContents)

  // 데이터와 아이디 결합해서 리턴!
  return {
    id,
    ...matterResult.data
  }
}

'[id].js' 에서 임뽀오뚜 추가! 위에 import했던거에 추가만 했어!

import { getAllPostIds, getPostData } from '../../lib/posts'

이제 함수는 준비됬으니 getStaticProps에 추가!

export async function getStaticProps({ params }) {
  const postData = getPostData(params.id) // params에서 id값을 가져와 데이터 추출!
  return {
    props: {
      postData
    }
  }
}

 

마지막으로 Post 페이지 컴포넌트 수정!

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

 

접속되는지 볼까?

뾰로롱!

 

 

 

지금까지 한일

  • [id].js 형태의 Dynamic Page 파일을 생성!
  • 페이지 컴포넌트 추가
  • id값 리스트를 넘기는 getStaticPaths 메서드 추가!
  • id에 대응하는 마크다운 파일 읽어오는 getStaticProps 추가!

 

무슨일이 일어났던 걸까?

정리하자면, Next.js 가 페이지를 미리생성한거야.

어떻게? 외부 데이터를 사용해서 자동생성! 그래서 Dynamic Routes

getStaticPaths함수로 어떤 규칙으로 URL과 페이지가 연결될지 알려주고, getStaticProps가 이를 받아 페이지 생성

오케이도케이? 

 

근데 말야!

글내용이 안나오잖아!

어 맞아 ㅎ 글 내용도 나오게 하자!

마크다운 형식으로 작성된 택스트 파일을 html로 바꿔주는 라이브러리를 사용할거야. 이름은 'remark'

설치해주자!

npm install remark remark-html

 

lib/posts.js에 임뽀뚜!

import remark from 'remark'
import html from 'remark-html'

 

getPostData 메서드에 html읽는 코드 추가!

// id값(파일명)으로 마크다운 파일을 읽기
// async 키워드 추가!
export async function getPostData(id) {
  const fullPath = path.join(postsDirectory, `${id}.md`)
  const fileContents = fs.readFileSync(fullPath, 'utf8')

  // gray-matter를 사용해 메타데이터 파싱!
  const matterResult = matter(fileContents)

  // 마크다운을 HTML로 바꾸기 위해 remark사용
  const processedContent = await remark()
    .use(html)
    .process(matterResult.content)
  const contentHtml = processedContent.toString()

  // 데이터와 아이디 결합해서 리턴!
  return {
    id, 
    contentHtml,
    ...matterResult.data
  }
}

 

여기서 주의해야 할 게 있어!

일단 getPostData 함수 정의라인에 async 키워드가 들어갔어 remark()메서드가 비동기 방식이라 await를 써야 했거든. 그래서 getPostData 함수를 사용하는 곳에도 await을 써줘야 해!

[id].js 파일의 getStaticProps 함수 수정!

export async function getStaticProps({ params }) {
  // "await" 키워드를 아래와 같이 추가!
  const postData = await getPostData(params.id)
  // ...
}

마지막으로 Post 페이지 컴포넌트에 내용 추가!

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

 

뾰로롱!

 

 

 

이제 글도 나왔으니 첫페이지의 블로그 리스트에 링크를 걸어주자!

pages/index.js 파일 수정!

export default function Home({ allPostsData }) {
  return (
    <Layout home>
... 생략 ...
      <section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
        <h2 className={utilStyles.headingLg}>블로그 글 목록</h2>
        <ul className={utilStyles.list}>
          {allPostsData.map(({ id, date, title }) => (
            <li className={utilStyles.listItem} key={id}>
              <Link href="/posts/[id]" as={`/posts/${id}`}>
                 <a>{title}</a>
              </Link>
              <br />
              <small className={utilStyles.lightText}>
              {date}
              </small>
            </li>
          ))} 
        </ul>
      </section>
... 생략 ...
    </Layout>
  )
}

그냥 제목만 있던 부분에 링크를 걸고 날짜 부분은 작은 회색글씨로 음영을 넣어줬어!

자 index.js도 접속해 볼까!

http://localhost:3000/

 

 

글링크도 막 눌러보고 홈으로가기 버튼을 막 눌러봐! 어때? 

이제 posts 디렉토리에 ".md"파일만 하나 추가하면 바로 첫페이지에 나오는거야 한번 복사해 추가해보아! 

 

마무우리!

지금까지 실습 위주로 달렸는데, 한번 정리하고 가자!

실습은 파일을 읽어서 보여주는 거였는데, DB를 사용하거나 API를 호출 하려면 어떻게 해야 할까?

우리 예제라면 getAllPostIds의 내용을 바꿔주면 되!

export async function getAllPostIds() {
  const res = await fetch('..')
  const posts = await res.json()
  return posts.map(post => {
    return {
      params: {
        id: post.id
      }
    }
  })
}

 

getStaticPaths는 언제 호출되는거야?

getStaticProps가 Static-Generation에 의해 실행된다고 위에서 한번 이야기 해줬지?

실환경(Production)에서는 빌드할때만!

개발환경(npm run dev)에서는 요청시마다 호출!

꼭 알아야해!

 

Fallback 설명 안해줬어!

응 설명이 늦었지? ^^;;;

getStaticPaths에서 fallback: false로 설정했어.

이 말인 즉슨, getStaticPaths에서 주지 않은 id값이 들어오면 무조건 404페이지를 리턴하겠다는 말이야!

그렇다면 true일때는?

404페이지가 아니라 해당 페이지의 대체 버전을 보여주게 되어있어. Post 페이지의 경우 postData에 빈 객체가 들어오게되서 title같은 정보가 없다는 리액트 에러페이지가 나올꺼야.

만약 요 경우에 별도의 에러 화면을 구현한다면 해당 페이지가 나와!

그리고 백그라운드에서는 요 페이지를 빌드타임에 만든것처럼 생성해서 다음부터는 요 생성한 페이지가 나오게 된다는 사실!

쫭이쥬?

404페이지도 커스텀 하고 싶다고?

pages/404.js 라고 페이지 컴포넌트 한번 만들어보아. ^^

자세한건  공식사이트 Error Pages 참조!

 

그럼 오늘은 요귀꽈쥐!

다음에는 Next.js에서 API는 어떻게 구현할지 실습할께? 어때 두근대지?

 

https://ppsu.tistory.com/67

 

Next.js - API 만들기 (API Routes)

이제 클라이언트 쪽은 알겠는데, API는 어떻게 만들까? Next.js에서 함수 하나로 API를 만들 수 있어! 엄청쉬워! 'pages/api' 디렉토리에다가 함수하나만 만들면 되! // req = 요청데이터, res = 응답 데이터

ppsu.tistory.com