하위 태스크 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.json

src/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로 재검증 시간 이후 새 페이지를 만들기도 하지만, 특성상 웹 문서의 콘텐츠가 빠르게 바뀌는 환경에서 쓰기에 적합하지 않다.