하위 태스크 1
getStaticProps 구현
SSG를 위한 정적 데이터 페칭 함수 작성
src/assets/books.json:
{
"books": [
{
"id": 1,
"title": "침묵의 기록",
"authors": ["김지훈"],
"genres": ["인문", "에세이"],
"summary": "소란한 세상 속에서 나만의 고유한 속도를 되찾아주는 문장들.",
"price": 16000
},
{
"id": 2,
"title": "코드 너머의 연결",
"authors": ["이진우"],
"genres": ["기술", "교양"],
"summary": "디지털 시대, 우리가 잃어버린 인간적 유대와 기술의 공존을 묻다.",
"price": 18500
},
{
"id": 3,
"title": "내일의 계절",
"authors": ["박서영"],
"genres": ["소설"],
"summary": "끝나지 않을 것 같던 겨울을 지나, 다시 시작되는 우리들의 이야기.",
"price": 14800
},
{
"id": 4,
"title": "데이터의 숲을 걷는 법",
"authors": ["최현우"],
"genres": ["기술", "인문"],
"summary": "복잡한 관계 속에서 단순한 진리를 찾아내는 SQL과 삶의 철학.",
"price": 22000
},
{
"id": 5,
"title": "객체의 언어",
"authors": ["정민석"],
"genres": ["기술", "자기계발"],
"summary": "세상을 구조화하는 객체지향적 사고가 우리 삶에 주는 힌트.",
"price": 19000
},
{
"id": 6,
"title": "어느 백엔드 엔지니어의 밤",
"authors": ["한주희"],
"genres": ["에세이"],
"summary": "보이지 않는 곳에서 흐름을 만드는 사람들의 고독과 열정에 관하여.",
"price": 15500
},
{
"id": 7,
"title": "알고리즘의 리듬",
"authors": ["강동원"],
"genres": ["기술", "예술"],
"summary": "가장 효율적인 정답을 찾아가는 과정에서 발견한 뜻밖의 아름다움.",
"price": 21000
}
]
}json-server를 사용해 3005 포트에서 API를 제공한다.
npx json-server -p 3005 src/assets/books.jsonsrc/pages/index.tsx:
import { InferGetStaticPropsType } from "next";
import Link from "next/link";
import Book from "@/domains/book";
export async function fetchBooks(count?: number): Promise<Book[]> {
const response = await fetch("http://localhost:3005/books");
const books = await response.json();
return count !== undefined && count <= books.length ? books.slice(0, count) : books;
}
export async function fetchRandomBooks(count?: number) {
const books = await fetchBooks();
const shuffledBooks = books.sort(() => Math.random() - 0.5);
return count !== undefined && count <= books.length ? shuffledBooks.slice(0, count) : shuffledBooks;
}
export async function getStaticProps() {
const latestBooks = (await fetchBooks()).toReversed().slice(3);
const randomBooks = await fetchRandomBooks(3);
return {
props: { latestBooks, randomBooks },
revalidate: 60,
};
}
export default function Home({ latestBooks, randomBooks }: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<div className="w-80 p-2 mx-auto bg-slate-100">
<header>
<h1 className="mb-2 text-lg font-bold">오늘의 발견</h1>
</header>
<main>
<h2 className="mb-2 italic">우연히 만난 한 권의 책이 당신의 세계를 바꿀지도 모릅니다.</h2>
<h3 className="mb-2 font-bold">신간</h3>
<ul>
{latestBooks.map(({ id, title, authors, price }) => (
<li key={id}>
<ul className="my-2">
<li>제목: <Link className="underline" href={`/book/${id}`}>{title}</Link></li>
<li>작가: {authors.join(", ")}</li>
<li>가격: {price}</li>
</ul>
</li>
))}
</ul>
<h3 className="mb-2 font-bold">추천</h3>
<ul>
{randomBooks.map(({ id, title, authors, price }) => (
<li key={id}>
<ul className="my-2">
<li>제목: <Link className="underline" href={`/book/${id}`}>{title}</Link></li>
<li>작가: {authors.join(", ")}</li>
<li>가격: {price}</li>
</ul>
</li>
))}
</ul>
</main>
</div>
);
}
하위 태스크 2
병렬 데이터 페칭
Promise.all을 활용한 효율적인 데이터 로딩
src/pages/index.tsx:
// ...
export async function getStaticProps() {
const [allBooks, randomBooks] = await Promise.all([fetchBooks(), fetchRandomBooks(3)]);
const latestBooks = allBooks.toReversed().slice(0, 3);
return {
props: { latestBooks, randomBooks },
};
}
// ...하위 태스크 3
getServerSideProps 구현
SSR을 위한 서버 사이드 데이터 페칭 함수 작성
src/pages/book/[id].tsx:
import { fetchBooks } from "..";
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
export async function getServerSideProps({ query }: GetServerSidePropsContext) {
const books = await fetchBooks();
const book = books.find((book) => {
const { id } = query;
if (typeof id !== "string") {
return false;
}
return book.id.toString() === id;
}) ?? null;
return {
props: { book },
};
}
export default function BookDetail({ book }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<div className="w-80 p-2 mx-auto bg-slate-100">
<header>
<h1 className="mb-2 text-lg font-bold">책 상세</h1>
</header>
<main>
{
book !== null ? (
<>
<h2 className="mb-2 italic">{book.title}</h2>
<ul>
<li>작가: {book.authors.join(", ")}</li>
<li>장르: {book.genres.join("/")}</li>
<li>요약: {book.summary}</li>
<li>가격: {book.price}</li>
</ul>
</>
) : "요청한 책을 찾을 수 없습니다."
}
</main>
</div>
);
}

하위 태스크 4
동적 메타데이터 설정
Head 컴포넌트로 SEO 최적화
src/pages/book/[id].tsx:
import Head from "next/head";
// ...
export default function BookDetail({ book }: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<div className="w-80 p-2 mx-auto bg-slate-100">
<Head>
<title>{book !== null ? book.title : "책 상세"}</title>
</Head>
{/* ... */}
</div>
)
}
하위 태스크 5
ISR 설정
revalidate 옵션으로 주기적 갱신 구현
src/pages/index.tsx:
// ...
export async function getStaticProps() {
const [allBooks, randomBooks] = await Promise.all([fetchBooks(), fetchRandomBooks(3)]);
const latestBooks = allBooks.toReversed().slice(0, 3);
return {
props: { latestBooks, randomBooks },
revalidate: 60,
};
}
// ...하위 태스크 6 ~ 9
getStaticPaths 구현
동적 라우트에서 정적 페이지 생성
fallback 처리
존재하지 않는 경로에 대한 처리 구현
타입 안전성 확보
InferGetStaticPropsType 등으로 타입 정의
에러 처리
데이터 페칭 실패 시 적절한 처리
src/pages/book/[id].tsx:
import Head from "next/head";
import { fetchBooks } from "..";
import { GetStaticPropsContext, InferGetStaticPropsType } from "next";
import { useRouter } from "next/router";
export async function getStaticPaths() {
const paths = [
{ params: { id: "1" } },
{ params: { id: "2" } },
{ params: { id: "3" } },
];
return {
paths,
fallback: true,
};
}
export async function getStaticProps({ params }: GetStaticPropsContext) {
try {
const books = await fetchBooks();
const book = books.find((book) => {
if (params === undefined) {
return false;
}
const { id } = params;
if (typeof id !== "string") {
return false;
}
return book.id.toString() === id;
}) ?? null;
return {
props: { book },
};
} catch (error) {
return {
props: { book: null },
};
}
}
export default function BookDetail({ book }: InferGetStaticPropsType<typeof getStaticProps>) {
const router = useRouter();
if (router.isFallback) {
return <div className="w-80 p-2 mx-auto">로딩 중...</div>
}
return (
<div className="w-80 p-2 mx-auto bg-slate-100">
<Head>
<title>{book !== null ? book.title : "책 상세"}</title>
</Head>
<header>
<h1 className="mb-2 text-lg font-bold">책 상세</h1>
</header>
<main>
{
book !== null ? (
<>
<h2 className="mb-2 italic">{book.title}</h2>
<ul>
<li>작가: {book.authors.join(", ")}</li>
<li>장르: {book.genres.join("/")}</li>
<li>요약: {book.summary}</li>
<li>가격: {book.price}</li>
</ul>
</>
) : "요청한 책을 찾을 수 없습니다."
}
</main>
</div>
);
}
하위 태스크 10
렌더링 방식 비교
각 방식의 성능과 특징 비교 문서 작성
- SSR: 웹 문서를 동적으로 생성하여 클라이언트로 전달한다. 웹 문서의 콘텐츠가 자주 변하는 경우에 유리하다. 런타임에 웹 문서를 생성하기 때문에 SSG보다 느리다.
- SSG: 빌드 타임에 웹 문서를 미리 생성하고 전달한다. 미리 생성된 웹 문서를 클라이언트로 전달하기 때문에 빠르다. ISR로 재검증 시간 이후 새 페이지를 만들기도 하지만, 특성상 웹 문서의 콘텐츠가 빠르게 바뀌는 환경에서 쓰기에 적합하지 않다.