2023. 4. 26. 22:49ㆍNext.js/Tutorial
4. Pre-rendering
여기에서 이미 언급한 바 있지만, Next.js는 기본적으로 모든 페이지를 사전 렌더퓨 링suppre-render/sup한다. 즉, 각 페이지에 관련된 HTML을 사전에 만들어서 제공한다는 뜻으로 더 나은 성능과 SEO를 보장한다.
이렇게 생성된 HTML은 해당 페이지에 필요한 최소한의 JS코드와 연결되고, 브라우저가 페이지를 로드하면 이 JS코드를 실행해 페이지를 완전히 동적인 페이지로 바꿔놓는다(이 과정을 hydration이라고 부름)
4.1 SSG, SSR
사전 렌더링의 형태는 두 가지 SSG와 SSR이 있는데, 둘을 나누는 기준은 언제 HTML이 생성되느냐이다. SSG(여기선 Static Generation이라고만 쓰고있긴 한데 아무튼)는 HTML을 build time때 생성해두고 매 request마다 빌드 타임에 생성된 HTML을 재사용한다. 반면, SSR은 매 request마다 HTML을 생성한다.
Next.js에선 각 페이지마다 SSG, SSR 원하는 형태로 취사선택이 가능한데, 이 말인 즉슨 둘을 적당히 섞어놓은 하이브리드 앱 또한 가능하다는 말이다.
그런데 보통 이렇게 선택지가 주어지면 정작 뭘 써야할지 막막할 수 있다. 뭐가 더 좋은지 감이 안잡힌다면 다음 가이드라인을 따라해보자.
SSG가 유리
일단 가능하다면 최대한 SSG를 쓰는게 좋다. 그 이유는 빌드 시 한번만 생성하면 되므로 매 요청마다 페이지를 렌더링하는 SSR과 비교하면 훨씬 빠르고 코스트가 적기 때문이다. 구체적으론 아래 유형의 페이지에서 쓸 수 있음.
- 마케팅 페이지
- 블로그 포스트
- E-commerce 상품 목록 페이지
- Help 및 documentation 문서
물론 이게 항상 정답은 아니므로 항상 스스로에게 물어보자. '유저의 request에 앞서 이 페이지를 미리 렌더링하는게 가능한가'라는 질문에 YES라는 답이 나온다면 SSG를 따르는게 대체로 좋다.
SSR이 유리
반면 NO 라는 답이 나온다면 좀 느리더라도 SSR을 선택하는게 좋다. 보통 NO가 나온 경우라면 페이지내 데이터가 자주 업데이트되는 경우라던가 매 요청마다 페이지 내용물이 바뀌는 경우일텐데, 비교적 느릴지언정 페이지를 계속 최신 상태로 유지시킬 수 있으므로 SSR을 쓰자.
(물론 CSR형식이 불가능한건 아니지만 그럴거면 굳이 Next.js를 쓸 필요가?)
4.2 SSG with and without data
지금까지 튜토리얼에서 만든 페이지는 외부 데이터를 가져올 필요가 없었다. 이런 경우 앱이 프로덕션용으로 빌드될 때 자동으로 정적 페이지를 생성한다.
그러나 외부 데이터를 가져오지 않으면 HTML을 렌더링할 수 없는 경우도 있다(사실 꽤 많을듯) 예를 들어, 파일 시스템에 접근한다든지 외부 API를 가져오거나 DB에 쿼리를 날려야 된다는지 하는 식으로 말이다. 다행히 Next.js는 이러한 경우에도 데이터를 사용한 SSG를 지원한다.
구체적으로는 관련 페이지의 컴포넌트를 export할 때 getStaticProps라는 비동기 함수를 쓰면 가능한데, 이 함수는 다음과 같은 역할을 수행한다
- 기본적으로 getStaticProps 함수는 프로덕션 빌드 시 동작한다
- 함수 내에서 외부 데이터를 fetch하고, 이를 페이지의 props로 전달한다
- 쉽게 말해 Next.js에게 "이 페이지엔 데이터 종속성이 있으니, 빌드 타임의 사전 렌더링 작업 시 종속성 먼저 해결해야 됨"이라고 통지하는 역할
구체적으로 어떻게 사용하는지는 다음 장에서 알아보자.
5. Data Fetching
이전 장에서 데이터 종속성을 가지는 SSG를 구현하기 위해 getStaticProps를 소개했는데, 실제로 이를 어떻게 사용하는지 알아보는 것부터 시작해보자. 전체적인 개요는 아래와 같다.
5.1 블로그 데이터 만들기
당장은 DB같은걸 써서 외부 데이터를 가져오진 않을것이다. 대신 로컬 환경에 마크다운 파일들을 저장해두고, 파일시스템으로부터 해당 데이터를 읽어오는 방식으로 데이터 패칭 실습을 진행해보자.
[1] 생성 : 마크다운 파일
우선 최상위 디렉토리에 posts란 이름으로 폴더를 하나 만들고, 해당 폴더에 pre-rendering.md, ssg-ssr.md라는 파일을 만들어보자. 그리고 아래 내용을 저장해두기만 하면 된다.
---
title: 'Two Forms of Pre-rendering'
date: '2020-01-01'
---
Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
---
title: 'When to Use Static Generation v.s. Server-side Rendering'
date: '2020-01-02'
---
We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
You can use Static Generation for many types of pages, including:
- Marketing pages
- Blog posts
- E-commerce product listings
- Help and documentation
You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.
In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
각 마크다운 파일의 상단엔 title과 date가 포함된 메타데이터 섹션이 있는데, 이를 YAML Front Matter 라고 한다. 이들은 gray-matter라는 라이브러리를 써서 파싱할 수 있다.
[2] gray-matter 설치
위 과정이 끝났으면 각 마크다운 파일의 메타데이터 파싱을 위해 gray-matter 라이브러리를 설치하자.
npm install gray-matter
자세한 내용은 해당 라이브러리 공식문서를 보면 알 수 있겠지만, 요약하자면 마크다운 파일의 내용을 객체 형태로 뽑아준다고 보면 된다.
[3] 생성 : 유틸리티 함수
이제 파일 시스템으로부터 실제로 데이터를 파싱해줄 유틸리티 함수를 하나 만들어보자. 이 함수는
1) 각 마크다운 파일의 title, date, filename을 추출하고
2) 데이터를 date순으로 정렬해 인덱스 페이지에 나열하는 역할을 하게 만들 예정이다.
여하튼 이를 위해 최상위 디렉토리에 lib라는 이름으로 폴더를 하나 만들고, 해당 폴더 안에 posts.js라는 파일을 만들어 아래 코드를 붙여넣어주자.
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const postsDirectory = path.join(process.cwd(), 'posts');
export function getSortedPostsData() {
// /posts디렉토리의 파일 네임들을 가져옴
const fileNames = fs.readdirSync(postsDirectory);
const allPostsData = fileNames.map((fileName) => {
// 파일명의 .md 부분을 지워줌
const id = fileName.replace(/\.md$/, '');
// 마크다운 파일을 스트링 형태로 읽어옴
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, 'utf8');
// gray-matter 라이브러리르 써서 메타데이터 섹션을 파싱함
const matterResult = matter(fileContents);
// 데이터와 id를 묶어서 객체 형태로 반환
return {
id,
...matterResult.data,
};
});
// date순으로 포스트를 정렬
return allPostsData.sort((a, b) => {
if (a.date < b.date) return 1;
return -1;
});
}
이제 블로그 데이터와 이를 파싱해줄 함수까지 마련됐으니 남은건 index page에서 데이터를 패칭하는 일만 남았다. 이 대목에서 getStaticProps 메서드를 쓸건데, 바로 이어서 알아보자.
5.2 getStaticProps 구현하기
이제 데이터를 패칭하는 getStaticProps를 구현해보자. 우선 pages/index.js로 가서 아래와 같이 메서드를 구현해준다.
import { getSortedPostsData } from '../lib/posts';
// ...중략
export async function getStaticProps() {
const allPostsData = getSortedPostsData();
return {
props: {
allPostsData,
},
};
}
함수 구현이 끝났으면 데이터를 실제로 보여주기 위해 Home컴포넌트에 아래와 같이 섹션을 하나 추가하고, 패칭된 데이터를 props로 넘겨받아 나열해주자.
export default function Home({ allPostsData }) {
return (
<Layout home>
<Head>...</Head>
<section className={utilStyles.headingMd}>...</section>
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<h2 className={utilStyles.headingLg}>Blog</h2>
<ul className={utilStyles.list}>
{allPostsData.map(({ id, date, title }) => (
<li className={utilStyles.listItem} key={id}>
{title}
<br />
{id}
<br />
{date}
</li>
))}
</ul>
</section>
</Layout>
)
}
export async function getStaticProps() {...}
이제 코드를 돌려보면 아래와 같은 결과를 확인할 수 있다.
5.3 getStaticProps 관련 세부사항
그런데 위 코드를 보면 이상한 점이 하나 있다. getStaticProps함수를 만들긴 했지만 직접 동작시킨 적은 없는데 도대체 언제 함수가 실행된걸까? 사실 getStaticProps는 빌드 시에 항상 동작된다. 때문에 함수를 만들어놓기만 했는데도 데이터를 패칭하고 Home컴포넌트에 넘겨줄 수 있었던 것이다.
이 외에도 여러 세부사항들이 많은데 자세한건 여기에서 확인하기로 하고, 지금은 중요한 몇 가지 특징들만 짚고 넘어가기로 하자.
1) 서버측에서만 구동됨 ( ☞ 외부 API나 DB쿼리도 얼마든지 가능)
getStaticProps는 클라이언트 사이드에선 구동되지 않고, 서버 사이드에서만 동작한다. 실제로 빌드하고 나면 브라우저로 넘겨질 JS 번들 내에도 포함되지 않는다. 즉, 서버에서 필요한 작업을 다 하므로 외부 API라든지 DB에 쿼리를 날려서 데이터를 가져오는 동작도 얼마든지 가능하다.
예를 들어, 위 예시에선 로컬 파일을 가져오기만 했지만 아래와 같은 작업도 얼마든지 가능하다.
// 외부 API 앤드포인트에서 데이터를 가져온다든지
export async function getSortedPostsData() {
const res = await fetch('..');
return res.json();
}
// DB에서 데이터를 가져온다든지
import someDatabaseSDK from 'someDatabaseSDK'
const databaseClient = someDatabaseSDK.createClient(...)
export async function getSortedPostsData() {
return databaseClient.query('SELECT posts...')
}
2) Page에서만 허용됨
Next.js에서 말하는 page란 pages 디렉토리내에 위치하는 .js, .jsx, .ts, .tsx 형식의 리액트 컴포넌트를 말한다. 그리고 getStaticProps는 이 page들에서만 export될 수 있다. 이렇게 제한을 둔 이유는 여러 가지가 있지만, 그중 하나는 페이지가 렌더링되기 전 필요한 데이터를 전부 갖춰놓기 위해서이다.
3) 개발 환경 vs 프로덕션 환경
getStaticProps함수는 환경별로 동작하는게 좀 다른데, 개발 환경에선 모든 request에 대해서 돌아가지만 프로덕션 환경에선 빌드 시에만 동작하고 끝난다. 물론, getStaticPaths가 리턴하는 fallback key같은걸 쓰면 조정이 가능하긴 하지만.
이런 차이가 발생하는 근본적인 원인은 빌드 시점에만 실행되도록 설계되어있기 때문이다.
지금까지 간략히 SSG에서의 데이터 패칭에 대해 알아봤는데, 만약 데이터가 자주 업데이트되거나 유저의 요청에 따라 변경된다면 SSR 방식을 사용해야 한다.
5.4 SSR
빌드 타임이 아니라 request time에 데이터를 패칭하고 싶다면 SSR을 써야한다. SSR의 경우 getServerSideProps를 사용하는데, 지금 당장 구현하진 않겠지만 대강 아래와 같은 탬플릿을 하고 있다. 자세한건 여기를 참조.
export async function getServerSideProps(context) {
return {
props: {
// props for your component
},
};
}
여기서 중요한건 context라는 프로퍼티인데, 요청과 관련된 내용들을 담고있다. 어찌됐건 getServerSideProps는 요청시 데이터를 패칭해 사전 렌더링이 필요한 경우에만 쓰는걸 권장한다. 이유는 서버가 매 요청마다 새로 연산을 수행해야 하므로 TTFBsupTime to first byte/sup가 느리고, 별도 설정없이는 CDN에서 캐시도 안돼서 재사용하기 어렵기 때문이다.
※ TTFB : 서버가 응답을 보내는데까지 걸리는 시간
5.5 CSR
그리고 만약 데이터를 사전 렌더링할 필요가 없다면 CSR을 사용할 수도 있다. 이걸 언제 써먹겠냐고 생각할 수 있는데, 유저 대시보드처럼 프라이빗하고 SEO와는 하등 상관없는 페이지라면 의외로 유용하게 써먹을 수 있다.
5.6 SWR
마지막으로 Next.js 팀에서 데이터 패칭을 위해 만든 SWR이라는 리액트 훅이 있는데, 클라이언트 사이드에서 데이터 패칭시 유용하게 써먹을 수 있으므로 강력히 추천하는 바이다. 캐싱, 트래킹, refetching, 갱신(revalidate) 등 다양한 기능들이 있으니 꼭 한번 써보길 바라며 자세한건 여기를 참조하길 바란다.
6. Dynamic Routes
이전 장에선 getStaticProps를 활용해 외부에서 데이터를 가져와 포스트를 보여주는 작업을 해봤다. 이번 장에선 외부 데이터에 따라 페이지 경로가 만들어지는 케이스에 대해 알아보자.
Next.js에선 외부 데이터에 따라 페이지 경로를 정적으로 생성할 수 있는데, 이를 통해 동적 URL을 만들 수 있다.
동적 경로dynamic routes라는 말이 와닿지 않을수도 있는데, 예를 들자면 id에 따라 URL 경로가 생성되는(동적인) 경우를 말하는 것뿐이다. 즉, 지금같은 경우 파일 이름이 ssg-ssr.md, pre-rendering.md니까 각 포스트 경로가 /posts/ssg-ssr, /posts/pre-rendering로 생성되는걸 말하는 것이다.
실습은 아래와 같은 단계로 이뤄질건데, 내용이 제법 있으므로 차근차근 따라가보자.
- pages/posts 디렉토리에 [id].js 형식으로 페이지를 만들어준다. 여기서 [ ]로 시작하는 페이지들은 다이나믹 라우트에 쓰임.
- getStaticPaths라는 비동기 함수를 만들고 export해준다. 이 함수는 id에 써먹을 수 있는 값을 리턴함.
- getStaticProps를 만들어서 필요한 데이터를 가져온다.
6.1 getStaticPaths 구현하기
가장 먼저 getStaticPaths를 구현할건데, 얘가 하는 역할이 대체 뭔가 싶을 것이다. 이 함수는 Dynamic Route의 페이지인 경우 실제로 참조할 수 있는 경로를 만드는 역할을 한다.
예를 들어, 아래와 같은 코드가 있을때 getStaticPaths는 /posts/1, /posts/2 경로를 만들어준다. 정확히 말하면 Next.js는 getStaticPaths가 리턴하는 paths프로퍼티를 참조해서 정적 페이지 경로를 사전에 만들어두는 것이다.
export async function getStaticPaths() {
return {
paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
fallback: false, // can also be true or 'blocking'
}
}
여하튼 자세한건 여기1와 여기2를 참조하기로 하고, 코드로 돌아와서 우선 pages/posts 디렉토리의 first-post.js파일을 제거하고 [id].js 파일을 생성해주자. 그 후 lib/posts.js로 가서 getAllPostIds라는 이름으로 새로운 함수 하나를 추가해주자. 이 함수는 posts 디렉토리의 파일 이름을 배열 형태로 리턴하는 역할을 담당할 예정이다.
export default function Post() {
return <Layout>나중에 채울 예정</Layout>
}
const postsDirectory = path.join(process.cwd(), 'posts')
export function getSortedPostsData() {...}
// 객체를 담은 배열을 리턴함(중요)
// [
// { params: { id: 'ssg-ssr' } },
// { params: { id: 'pre-rendering' }, },
// ]
export function getAllPostIds() {
const fileNames = fs.readdirSync(postsDirectory);
return fileNames.map((fileName) => {
return {
params: {
id: fileName.replace(/\.md$/, ''),
},
};
});
}
getAllPostIds함수에서 중요한건 객체가 담긴 배열을 리턴한다는건데, 특히 그 객체 안엔 params라는 키를 담고있어야 한다. 또한, params 안엔 id라는 키가 필요하다(이 id키는 어디까지나 지금 튜토리얼이 [id]라는 이름으로 파일을 만들어 쓰고있어서 그런것 뿐임) 이 포맷을 안지키면 getStaticPaths가 제대로 동작하지 않으므로 주의하자.
마지막으로 다시 [id].js파일을 열어서 getStaticPaths 함수를 아래와 같이 작성해주자.
import Layout from "../../components/layout";
import { getAllPostIds } from "../../lib/posts";
export default function Post() {
return <Layout>나중에 채울 예정</Layout>
}
export async function getStaticPaths() {
const paths = getAllPostIds();
return {
paths,
fallback: false, // 이건 나중에 알아보기로 하자
}
}
6.2 getStaticProps 구현하기
이제 id를 바탕으로 포스트 렌더링에 필요한 데이터를 가져와보자.
먼저 lib/posts.js 파일을 열어서 getPostData 함수를 새롭게 하나 추가해줄건데, 이 함수는 id값을 바탕으로 포스트 데이터를 리턴할 예정이다.
const postsDirectory = path.join(process.cwd(), 'posts')
export function getSortedPostsData() { ... }
export function getAllPostIds() { ... }
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,
};
}
그 다음 [id].js파일을 열어서 실제로 데이터를 가져올 getStaticProps 함수를 아래와같이 만들어주자. 이전과는 다르게 파라미터로 context가 들어가는걸 볼 수 있는데, context는 params, preview, previewData 등등의 값들을 담고있는 객체이다.
import Layout from "../../components/layout";
import { getAllPostIds, getPostData } from "../../lib/posts";
export default function Post() { ... }
export async function getStaticPaths() { ... }
export async function getStaticProps(constext) {
const { params } = constext;
const postData = getPostData(params.id);
return {
props: {
postData,
},
};
}
// context를 콘솔로 찍어보면
// {
// params: { id: 'pre-rendering' },
// locales: undefined,
// locale: undefined,
// defaultLocale: undefined
// }
여기서 중요한건 params인데, parmas는 dynamic route를 사용하는 페이지에 대한 경로 파라미터를 담고있다. 예를 들어, 페이지 이름이 [id].js라면 params는 { id: ... } 같은 형태를 가지게 된다.
즉 getStaticPaths함수가 만들어낸 페이지들의 경로를 params라는 키로 참조하고, 각 경로값을 기반으로 데이터를 가져와 미리 정적 페이지를 렌더링 시켜놓는 것이다. 따라서 getStaticPaths가 만들어주는 경로가 필요함을 알 수 있고, 결론적으로 getStaticPaths와 함께 쓰는 경우에만 유효함을 알 수 있다.
이제 포스트 페이지는 getStaticProps내 getPostData 함수를 통해 데이터를 가져오고, 이를 props라는 키에 담아 리턴하게 된다. 마지막으로 남은건 이 props를 Post 컴포넌트에 전달해 화면상에 그려주기만 하면 된다.
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
<br />
</Layout>
)
}
6.3 마크다운 렌더링하기
마크다운 내용물을 빼내기 위해 remark라는 라이브러리를 사용해보자.
npm install remark remark-html
설치가 완료되면 lib/posts.js 파일에서 getPostData함수를 아래와같이 수정해주자. 여기서 중요한 것은 비동기 함수가 되므로 aysnc문을 사용한다는 점이다.
import { remark } from 'remark';
import html from 'remark-html';
const postsDirectory = path.join(process.cwd(), 'posts')
export function getSortedPostsData() { ... }
export function getAllPostIds() { ... }
export async function getPostData(id) {
const fullPath = path.join(postsDirectory, `${id}.md`);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const matterResult = matter(fileContents);
const processedContent = await remark()
.use(html)
.process(matterResult.content);
const contentHTML = processedContent.toString();
return {
id,
contentHTML,
...matterResult.data,
};
}
데이터를 가져오는 getPostData함수가 비동기로 바뀌었으니 이와 연관된 getStaticProps도 비동기 함수로 바꿔줘야 한다. 그래봤자 await 하나 붙여주는게 끝이긴 하지만 말이다. 그 후 마지막으로 Post 컴포넌트가 포스트 내용을 렌더링할 수 있게 태그를 하나 추가해주자.
export default function Post({ postData }) {
return (
<Layout>
{...어쩌구}
<div dangerouslySetInnerHTML={{ __html: postData.contentHTML }} />
</Layout>
)
}
export async function getStaticPaths() { ... }
export async function getStaticProps(constext) {
const { params } = constext;
const postData = await getPostData(params.id);
return {
props: {
postData,
},
};
}
이제 locahost:3000/posts/pre-rendering으로 접속해보면 아래와 같은 제대로된 포스트 내용물을 볼 수 있을 것다.
6.4 Post 페이지 다듬기
1) title 추가하기
pages/posts/[id].js 파일에서 title을 추가해보자. 대단한건 아니고 Post컴포넌트에 next/head 모듈의 Head 컴포넌트를 이용하는게 끝이다.
(참고로 말하지만 여기서 말하는 title은 브라우저 맨 위에 보이는 타이틀을 말하는거임)
export default function Post({ postData }) {
return (
<Layout>
<Head>
<title>{postData.title}</title>
</Head>
// ...
</Layout>
)
}
2) 날짜 형식 다듬기
공식문서에선 date-fns 라이브러리를 이용했지만 내 맘대로 할거기때문에 JS의 표준 내장 객체인 Intl.DateTimeFormat을 이용해보자. 여기서 Intl 객체는 각 언어에 맞는 문자, 숫자, 시간, 날짜비교를 제공하는 ECMAScript 국제화 API이다. 자세한건 여기 참조
new Intl.DateTimeFormat(locale, [, options])
- locales : 언어 태그를 포함하는 문자열이나 문자열의 배열. 브라우저 기본 로케일 사용시 빈 배열전달하면 됨
- options : dateStyle, timeStyle, calender, dayPeriod 등등 형식과 관련된 온갖 것들을 지원함
날짜 표출을 담당해줄 별도의 컴포넌트를 components/date.js에 만들고, 이를 기존의 포스트 페이지에 껴넣는 방식으로 진행해보자.
export default function Dates({ dateString }) {
const date = new Date(dateString);
const str = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
}).format(date);
return <div>{str}</div>
}
3) CSS 추가하기
마지막으로 styles/utils.module.css 파일에 추가해뒀던 CSS를 바탕으로 스타일을 입혀보자.
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}>
<Dates dateString={postData.date} />
</div>
<div dangerouslySetInnerHTML={{ __html: postData.contentHTML }} />
</article>
</Layout>
)
}
6.5 Index 페이지 다듬기
이제 진짜 마지막으로 index페이지에서 각 게시물 제목 클릭시 해당 게시물로 이동하는 네비게이션 기능을 달아주자.
<ul className={utilStyles.list}>
{allPostsData.map(({ id, date, title }) => (
<li className={utilStyles.listItem} key={id}>
<Link href={`/posts/${id}`}>{title}</Link>
<br />
<small className={utilStyles.lightText}>
<Dates dateString={date}/>
</small>
</li>
))}
</ul>
6.6 Dynamic Routes 세부사항
동적 라우팅에 대한 자세한 사항은 여기에서 확인하기로 하고, 여기선 간단하게 몇 가지 중요한 내용들만 정리하고자 한다.
1) 외부 API, DB쿼리 날리기
getStaticProps와 마찬가지로 getStaticPaths도 외부 소스에서 데이터를 가져올 수 있다. 예를 들어, getStaticPaths에서 쓰던 getAllPostIds를 아래처럼 쓸 수도 있다
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,
},
};
});
}
2) Fallback
튜토리얼에서 getStaticPaths를 쓸 때 fallback: false를 설정해줬었는데, 이게 무슨 의미였을까? 먼저, fallback은 true, false, blocking 세 가지 상태를 가질 수 있는데 false인 경우 getStaticPaths가 리턴하지 않는 어떠한 경로로 접근시 404 page로 넘어가게 된다.
반면, fallback이 true이면 동작이 꽤 많이 바뀌게 되는데 구체적으로는
- 빌드 타임때 getStaticPaths가 생성하지 않은 경로로 접속해도 404 page로 넘어가진 않는다
- 대신 해당 경로에 대한 'fallback' 버전의 페이지를 리턴한다
- 백그라운드에선 요청된 경로를 정적으로 생성한다
- 그 후 동일 경로에 대한 후속 요청이 들어오면 빌드 시점에 pre-rendered된 다른 페이지와 마찬가지로 생성된 페이지를 제공한다
마지막으로 fallback이 blocking인 경우 새 경로는 getStaticProps와 함께 server-side rendered되고, 이를 캐싱해 향후 요청에 써먹을 수 있게 대비해둔다. 결과물이 캐시되므로 이 작업은 경로당 한 번만 이뤄진다. (더 자세한 정보는 여기를 참조할 것)
3) Catch all Routes
동적 경로는 괄호 안에 스프레드 오퍼레이터(...)를 쓰면 경로를 더 확장할 수 있다. 이게 무슨소리냐면 지금까진 posts/pre-rendering같은 경로만 써봤는데, 이를 더 확장해서 posts/a/b/c 같은 경로도 가능하다는 뜻이다.
pages/posts/[...id].js
위 예시처럼 스프레드 오퍼레이터를 쓰면 끝인데, 이렇게 만들면 posts/a, posts/a/b, posts/a/b/c 모두 가능하다. 그리고 이렇게 하고싶다면 getStaticPaths의 params에 경로로 쓸 프로퍼티를 배열로 전달해줘야 한다. (자세한건 여기)
return [
{
params: {
// Statically Generates /posts/a/b/c
id: ['a', 'b', 'c'],
},
},
//...
];
export async function getStaticProps({ params }) {
// params.id = ['a', 'b', 'c']
}
4) 개발용 vs 프로덕션
개발용 서버에선 getStaticPaths는 모든 request에 대해 동작하고, 프로덕션 환경에선 빌드 시 한번만 동작한다.
5) 라우터
Next.js의 라우터에 접근하고 싶다면 useRouter훅을 사용하면 된다.
6) 404 페이지
커스텀 404 페이지를 만들고싶다면 다른 페이지 생성과 마찬가지로 pages/404.js로 만들면 된다. 그러면 빌드 타임에 정적으로 생성되고, 다른 페이지와 동일하게 제공될 것이다. 원한다면 404뿐만 아니라 다른 에러 페이지도 생성할 수 있으니 자세한건 여기를 참고.
export default function Custom404() {
return <h1>404 - Page Not Found</h1>;
}
'Next.js > Tutorial' 카테고리의 다른 글
[Tutorial] Create First App(3) (0) | 2023.04.29 |
---|---|
[Tutorial] Create First App (0) | 2023.04.24 |