우리는 인덱스 페이지에 블로그 데이터를 채웠지만 아직 개별 블로그 페이지를 만들지 않았습니다 (여기에 원하는 결과가 있습니다). 이러한 페이지의 URL을 블로그 데이터에 따라 다르게 만들려면 동적 라우트를 사용해야합니다.
이 레슨에서는 다음을 배우게 됩니다:
- getStaticPaths를 사용하여 동적 라우트를 가진 페이지를 정적으로 생성하는 방법.
- 각 블로그 게시물의 데이터를 가져오기 위해 getStaticProps를 작성하는 방법.
- remark를 사용하여 마크다운을 렌더링하는 방법.
- 날짜 문자열을 깔끔하게 서식화하는 방법.
- 동적 라우트를 가진 페이지로 연결하는 방법.
- 동적 라우트에 관한 유용한 정보.
시작 코드 다운로드 (선택 사항)
이전 레슨에서 진행 중이 아니라면 이 레슨을 위한 시작 코드를 다운로드하고 설치하고 실행할 수 있습니다. 이를 통해 이전 레슨의 결과와 동일한 nextjs-blog 디렉토리를 설정합니다.
다시 말하지만, 이전 레슨을 완료한 경우 이것은 필요하지 않습니다.
npx create-next-app@latest nextjs-blog --use-npm --example "https://github.com/vercel/next-learn/tree/main/basics/dynamic-routes-starter"
그런 다음 명령 출력에서 지침을 따르십시오 (디렉토리로 이동하고 개발 서버를 시작하십시오).
또한 다음 파일을 업데이트해야합니다.
- public/images/profile.jpg를 귀하의 사진으로 업데이트하십시오 (권장: 너비/높이 400px).
- components/layout.js 파일에서 const name = '[Your Name]'을 귀하의 이름으로 업데이트하십시오.
- pages/index.js 파일에서 <p>[Your Self Introduction]</p>을 귀하의 자기 소개로 업데이트하십시오.
Page Path Depends on External Data
이전 레슨에서는 페이지 콘텐츠가 외부 데이터에 의존하는 경우에 대해 다루었습니다. 우리는 index 페이지를 렌더링하기 위해 필요한 데이터를 가져 오기 위해 getStaticProps를 사용했습니다.
이 레슨에서는 각 페이지 경로가 외부 데이터에 의존하는 경우를 다룰 것입니다. Next.js는 외부 데이터에 의존하는 경로를 가진 페이지를 정적으로 생성할 수 있도록 허용합니다. 이를 통해 Next.js에서 동적 URL을 사용할 수 있게 됩니다.
동적 경로를 사용하여 페이지를 정적으로 생성하는 방법
우리의 경우 블로그 게시물에 대한 동적 경로를 생성하려고 합니다.
- 각 게시물은 /posts/<id> 경로를 가져야하며, 여기서 <id>는 상위 posts 디렉토리 아래의 마크다운 파일의 이름입니다.
- ssg-ssr.md 및 pre-rendering.md가 있기 때문에 경로는 /posts/ssg-ssr 및 /posts/pre-rendering 여야합니다.
단계 개요
다음 단계를 수행하여이 변경 사항을 만들 수 있습니다. 이러한 변경 사항을 이미 적용할 필요는 없습니다.
다음 단계에서 모두 수행하겠습니다.
먼저, pages/posts 아래에 [id].js라는 페이지를 만듭니다. [로 시작하여 ]로 끝나는 페이지는 Next.js의 동적 경로입니다.
pages/posts/[id].js에는 다른 페이지와 같이 게시물 페이지를 렌더링하는 코드를 작성합니다.
import Layout from '../../components/layout';
export default function Post() {
return <Layout>...</Layout>;
}
이제 새로운 내용입니다. 이 페이지에서 getStaticPaths라는 비동기 함수를 내보냅니다. 이 함수에서 id에 대한 가능한 값 목록을 반환해야 합니다.
import Layout from '../../components/layout';
export default function Post() {
return <Layout>...</Layout>;
}
export async function getStaticPaths() {
// Return a list of possible value for id
}
마지막으로 다시 getStaticProps를 구현해야 합니다. 이번에는 주어진 id를 사용하여 해당 id의 블로그 게시물을 가져 오기 위해 getStaticProps를 구현해야 합니다. getStaticProps는 params를 제공받으며, params에는 id가 포함됩니다(파일 이름이 [id].js이기 때문).
import Layout from '../../components/layout';
export default function Post() {
return <Layout>...</Layout>;
}
export async function getStaticPaths() {
// Return a list of possible value for id
}
export async function getStaticProps({ params }) {
// Fetch necessary data for the blog post using params.id
}
이에 대한 그래픽 요약은 다음과 같습니다:
getStaticPaths 구현
먼저 파일을 설정해 봅시다:
- pages/posts 디렉토리 내에 [id].js라는 이름의 파일을 생성하세요.
- 또한 pages/posts 디렉토리 내의 first-post.js 파일을 제거하세요. 이 파일은 더 이상 사용하지 않습니다.
그런 다음, 에디터에서 pages/posts/[id].js 파일을 열고 다음 코드를 붙여 넣으세요. 나중에 ... 부분을 채우겠습니다.
import Layout from '../../components/layout';
export default function Post() {
return <Layout>...</Layout>;
}
그런 다음, lib/posts.js 파일을 열고 아래의 getAllPostIds 함수를 맨 아래에 추가하세요. 이 함수는 posts 디렉토리의 파일 이름 목록(확장자 .md 제외)을 반환합니다.
export function getAllPostIds() {
const fileNames = fs.readdirSync(postsDirectory);
// Returns an array that looks like this:
// [
// {
// params: {
// id: 'ssg-ssr'
// }
// },
// {
// params: {
// id: 'pre-rendering'
// }
// }
// ]
return fileNames.map((fileName) => {
return {
params: {
id: fileName.replace(/\.md$/, ''),
},
};
});
}
중요: 반환된 목록은 문자열 배열이 아니라 위에 주석처럼 보이는 객체 배열이어야 합니다. 각 객체는 params 키를 가져야 하며 파일 이름에 [id]를 사용하고 있으므로 id 키를 포함해야 합니다. 그렇지 않으면 getStaticPaths가 실패합니다.
마지막으로, getAllPostIds 함수를 가져 와서 getStaticPaths 내에서 사용하겠습니다. pages/posts/[id].js 파일을 열고 내보낸 Post 컴포넌트 위에 다음 코드를 복사하세요.
import { getAllPostIds } from '../../lib/posts';
export async function getStaticPaths() {
const paths = getAllPostIds();
return {
paths,
fallback: false,
};
}
- paths는 getAllPostIds()에서 반환된 알려진 경로 배열을 포함하며, pages/posts/[id].js에서 정의한 params를 포함합니다. paths key documentation에서 더 배울 수 있습니다.
- fallback: false를 무시하세요. 나중에 설명하겠습니다.
거의 다 왔습니다. 하지만 아직 getStaticProps를 구현해야 합니다.
getStaticProps 구현
getPostData 함수를 추가하여 주어진 id에 해당하는 게시물 데이터를 가져와야 합니다.
이를 위해 다시 lib/posts.js 파일을 열고 다음과 같이 getPostData 함수를 맨 아래에 추가하세요. 이 함수는 id를 기반으로 게시물 데이터를 반환합니다.
export function getPostData(id) {
const fullPath = path.join(postsDirectory, `${id}.md`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents);
// Combine the data with the id
return {
id,
...matterResult.data,
};
}
그런 다음, pages/posts/[id].js 파일을 열고 다음 줄을:
import { getAllPostIds } from '../../lib/posts';
다음 코드로 대체하세요:
import { getAllPostIds, getPostData } from '../../lib/posts';
export async function getStaticProps({ params }) {
const postData = getPostData(params.id);
return {
props: {
postData,
},
};
}
이제 게시물 페이지는 getStaticProps에서 getPostData 함수를 사용하여 게시물 데이터를 가져와 props로 반환합니다.
이제 Post 컴포넌트를 업데이트하여 postData를 사용하도록 합시다. pages/posts/[id].js 파일에서 내보낸 Post 컴포넌트를 다음 코드로 대체하세요:
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
</Layout>
);
}
그게 다입니다! 이 페이지들을 방문해 보세요:
이제 각 페이지에서 블로그 데이터를 볼 수 있어야 합니다.
문제가 있나요?
오류를 만나면 파일이 올바른 코드를 가지고 있는지 확인하세요:
- pages/posts/[id].js는 이와 같아야 합니다.
- lib/posts.js는 이와 같아야 합니다.
- (아직 작동하지 않는 경우) 나머지 코드는 이와 같아야 합니다.
문제가 계속되면 GitHub Discussions의 커뮤니티에 질문하십시오. 다른 사람이 코드를 확인할 수 있도록 GitHub에 코드를 업로드하고 링크를 제공하면 도움이 될 것입니다.
요약
다음은 우리가 수행한 작업의 그래픽 요약입니다:
블로그 마크다운 내용을 아직 표시하지 않았습니다. 이것을 다음에 수행해 봅시다.
마크다운 렌더링
마크다운 콘텐츠를 렌더링하기 위해 우리는 remark 라이브러리를 사용할 것입니다. 먼저 이를 설치하세요:
npm install remark remark-html
그런 다음, lib/posts.js 파일을 열고 파일의 맨 위에 다음과 같은 import 문을 추가하세요:
import { remark } from 'remark';
import html from 'remark-html';
그리고 같은 파일 내의 getPostData() 함수를 다음과 같이 업데이트하여 remark를 사용하도록 하세요:
export async function getPostData(id) {
const fullPath = path.join(postsDirectory, `${id}.md`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
// Use gray-matter to parse the post metadata section
const matterResult = matter(fileContents);
// Use remark to convert markdown into HTML string
const processedContent = await remark()
.use(html)
.process(matterResult.content);
const contentHtml = processedContent.toString();
// Combine the data with the id and contentHtml
return {
id,
contentHtml,
...matterResult.data,
};
}
중요: remark를 사용하기 위해 getPostData에 async 키워드를 추가했습니다. 비동기적으로 데이터를 가져오려면 await을 사용해야합니다.
그런 의미에서 pages/posts/[id].js 파일 내의 getStaticProps를 업데이트하여 getPostData를 호출할 때 await을 사용해야합니다:
export async function getStaticProps({ params }) {
// Add the "await" keyword like this:
const postData = await getPostData(params.id);
return {
props: {
postData,
},
};
}
마지막으로, pages/posts/[id].js 파일 내의 Post 컴포넌트를 업데이트하여 contentHtml을 dangerouslySetInnerHTML을 사용하여 렌더링하도록 하세요:
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
<br />
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</Layout>
);
}
다시 다음 페이지를 방문해 보세요:
이제 블로그 콘텐츠를 볼 수 있어야 합니다:
거의 다 왔습니다! 이제 각 페이지를 다듬어 봅시다.
Post 페이지 다듬기
포스트 페이지에 제목 태그를 추가
파일 상단에 next/head를 import하고 Post 컴포넌트를 업데이트하여 제목 태그를 추가하세요:
// Add this import
import Head from 'next/head';
export default function Post({ postData }) {
return (
<Layout>
{/* Add this <Head> tag */}
<Head>
<title>{postData.title}</title>
</Head>
{/* Keep the existing code here */}
</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>;
}
참고: 다양한 format() 문자열 옵션은 date-fns 웹사이트에서 확인할 수 있습니다.
이제 pages/posts/[id].js 파일을 열고 파일 상단에 Date 컴포넌트를 import하고 {postData.date} 대신에 이를 사용하세요:
// Add this import
import Date from '../../components/date';
export default function Post({ postData }) {
return (
<Layout>
{/* Keep the existing code here */}
{/* Replace {postData.date} with this */}
<Date dateString={postData.date} />
{/* Keep the existing code here */}
</Layout>
);
}
만약 http://localhost:3000/posts/pre-rendering에 접근하면 날짜가 "January 1, 2020"로 표시되어야 합니다.
CSS 추가
마지막으로, 이전에 추가한 styles/utils.module.css 파일을 사용하여 CSS를 추가해 봅시다. pages/posts/[id].js 파일을 열고, CSS 파일을 import하고 Post 컴포넌트를 다음 코드로 대체하세요:
// Add this import at the top of the file
import utilStyles from '../../styles/utils.module.css';
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에 접근하면 페이지가 좀 더 깔끔하게 보여야 합니다:
훌륭한 작업입니다! 다음에는 인덱스 페이지를 다듬고 마무리하겠습니다!
인덱스 페이지 다듬기
이제 인덱스 페이지(pages/index.js)를 업데이트해 봅시다. 각 포스트 페이지로 이동할 수 있는 링크를 추가해야 합니다. Link 컴포넌트를 사용하여 이를 수행합니다.
먼저, pages/index.js 파일을 열고 Link와 Date에 대한 다음 import를 파일 상단에 추가하세요:
import Link from 'next/link';
import Date from '../components/date';
그런 다음, 파일 하단에 있는 Home 컴포넌트 내에서 다음과 같이 li 태그를 대체하세요:
<li className={utilStyles.listItem} key={id}>
<Link href={`/posts/${id}`}>{title}</Link>
<br />
<small className={utilStyles.lightText}>
<Date dateString={date} />
</small>
</li>
이제 http://localhost:3000 에 접속하면 페이지에 각 게시물로 이동할 수 있는 링크가 표시됩니다:
만약 무엇인가 작동하지 않는다면 코드가 이와 같이 보이는지 확인하세요.
이게 전부입니다! 이번 레슨을 마무리하기 전에, 동적 라우트에 대한 몇 가지 유용한 팁을 알아보겠습니다.
동적 라우팅 상세
동적 라우팅에 대해 중요한 정보 몇 가지를 알아보겠습니다.
외부 API 가져오기 또는 데이터베이스 쿼리
getStaticProps와 마찬가지로 getStaticPaths는 모든 데이터 소스에서 데이터를 가져올 수 있습니다. 예를 들어 getAllPostIds(getStaticPaths에서 사용됨)는 외부 API 엔드포인트에서 가져올 수 있습니다:
export async function getAllPostIds() {
// Instead of the file system,
// fetch post data from an external API endpoint
const res = await fetch('..');
const posts = await res.json();
return posts.map((post) => {
return {
params: {
id: post.id,
},
};
});
}
개발 환경 대 프로덕션
- 개발 환경에서(npm run dev 또는 yarn dev), getStaticPaths는 모든 요청마다 실행됩니다.
- 프로덕션 환경에서, getStaticPaths는 빌드 시간에 실행됩니다.
Fallback
getStaticPaths에서 fallback: false를 반환했다는 것을 기억하십니까? 이것은 무엇을 의미합니까?
fallback이 false이면 getStaticPaths에서 반환하지 않은 경로는 404 페이지로 이어집니다.
fallback이 true이면 getStaticProps의 동작이 변경됩니다.
- getStaticPaths에서 반환된 경로는 빌드 시간에 HTML로 렌더링됩니다.
- 빌드 시간에 생성되지 않은 경로는 404 페이지로 이어지지 않습니다. 대신, Next.js는 해당 경로로 첫 번째 요청에 대해 "fallback" 버전의 페이지를 제공합니다.
- 백그라운드에서 Next.js는 요청한 경로를 정적으로 생성합니다. 동일한 경로로의 후속 요청은 빌드 시간에 사전 렌더링된 다른 페이지처럼 생성된 페이지를 제공합니다.
fallback이 blocking이면 새로운 경로는 getStaticProps를 사용하여 서버 측에서 렌더링되며 해당 경로에 대한 이후 요청을 위해 캐시됩니다. 따라서 경로당 한 번만 발생합니다.
자세한 내용은 fallback 문서에서 fallback: true 및 fallback: 'blocking'을 알아보세요.
Catch-all 라우트
동적 라우트를 확장하여 대괄호 내에 세 개의 점(...)을 추가하여 모든 경로를 캐치할 수 있습니다. 예를 들어:
- pages/posts/[...id].js는 /posts/a와 일치하지만 /posts/a/b, /posts/a/b/c 등과도 일치합니다.
이렇게 하려면 getStaticPaths에서 id 키의 값으로 배열을 반환해야 합니다. 다음과 같이:
return [
{
params: {
// Statically Generates /posts/a/b/c
id: ['a', 'b', 'c'],
},
},
//...
];
getStaticProps에서 params.id는 배열이 될 것입니다:
export async function getStaticProps({ params }) {
// params.id will be like ['a', 'b', 'c']
}
자세한 내용은 catch-all 라우트 문서를 확인하여 더 알아보세요.
Router
Next.js 라우터에 액세스하려면 next/router에서 useRouter 훅을 가져올 수 있습니다.
404 페이지
커스텀 404 페이지를 만들려면 pages/404.js를 생성하세요. 이 파일은 빌드 시간에 정적으로 생성됩니다.
// pages/404.js
export default function Custom404() {
return <h1>404 - Page Not Found</h1>;
}
자세한 내용은 에러 페이지 문서를 확인하여 더 알아보세요.
더 많은 예제
getStaticProps와 getStaticPaths를 설명하기 위해 몇 가지 예제를 작성했습니다. 자세한 내용은 이들 예제의 소스 코드를 확인하여 더 알아보세요:
여기까지입니다!
다음 레슨에서는 Next.js의 API 라우트에 대해 이야기하겠습니다.
'Dev > Next JS' 카테고리의 다른 글
[공식문서] NextJS - Deploying Your Next.js App (0) | 2023.09.14 |
---|---|
[공식문서] NextJS - API Routes (0) | 2023.09.14 |
[공식문서] NextJS - Pre-rendering and Data Fetching #9 (0) | 2023.09.08 |
[공식문서] NextJS - Pre-rendering and Data Fetching #8 (0) | 2023.09.08 |
[공식문서] NextJS - Pre-rendering and Data Fetching #7 (0) | 2023.09.07 |