하위 태스크 1, 10

서버 컴포넌트 데이터 페칭

async 함수로 fetch API 사용

타입 안전성 확보

TypeScript로 데이터 타입 정의

src/domains.ts:

export interface Book {
  id: string;
  title: string;
  authors: string[];
  genres: string[];
  summary: string;
  price: number;
}

src/utils.ts:

export async function load<T>(url: string, init?: RequestInit): Promise<T> {
  const response = await fetch(url, init);
  const data = await response.json();
  return data;
}

src/app/(with-searchbar)/page.tsx:

import type { Book } from "@/domains";
import Link from "next/link";
import { load } from "@/utils";
 
export default async function TestPage() {
  const books = await load<Book[]>("http://localhost:3005/books");
 
  return (
    <main className="p-4">
      <h2 className="mb-4 font-bold">출판물</h2>
      <ul className="pl-4 list-disc underline">
        {books.map(({ id, title }) => <li><Link href={`/book/${id}`}>{title}</Link></li>)}
      </ul>
    </main>
  );
}

하위 태스크 2

캐싱 옵션 적용

force-cache, no-store, revalidate 옵션 사용

src/app/(with-searchbar)/page.tsx:

export default async function TestPage() {
  const books = await load<Book[]>("http://localhost:3005/books", { cache: "force-cache" });
  // const books = await load<Book[]>("http://localhost:3005/books", { cache: "no-store" });
 // const books = await load<Book[]>("http://localhost:3005/books", { next: { revalidate: 60 } });
 
  // ...
}
 

하위 태스크 3

병렬 데이터 페칭

Promise.all로 여러 데이터 동시 가져오기

src/app/(with-searchbar)/page.tsx:

import type { Book } from "@/domains";
import Link from "next/link";
import { load } from "@/utils";
 
async function loadAllBooks() {
  return load<Book[]>("http://localhost:3005/books", { cache: "force-cache" });
}
 
async function loadRecoBooks(){
  const books = await load<Book[]>("http://localhost:3005/books", { next: { revalidate: 3 } });
  const shuffledBooks = books.sort(() => Math.random() - 0.5);
 
  return shuffledBooks.slice(0, books.length / 2);
}
 
export default async function TestPage() {
  const [ allBooks, recoBooks ] = await Promise.all([loadAllBooks(), loadRecoBooks()]);
 
  return (
    <main className="p-4">
      <h2 className="mb-4 font-bold">전체 출판물</h2>
      <ul className="pl-4 list-disc underline">
        {allBooks.map(({ id, title }) => <li key={id}><Link href={`/book/${id}`}>{title}</Link></li>)}
      </ul>
      <h2 className="my-4 font-bold">추천 출판물</h2>
      <ul className="pl-4 list-disc underline">
        {recoBooks.map(({ id, title }) => <li key={id}><Link href={`/book/${id}`}>{title}</Link></li>)}
      </ul>
    </main>
  );
}

하위 태스크 4

동적 렌더링 제어

dynamic 옵션으로 렌더링 방식 설정

src/app/(with-searchbar)/page.tsx:

// ...
 
export const dynamic = "force-dynamic";
 
export default async function TestPage() {
  // ...
}

하위 태스크 5

정적 렌더링 제어

force-static으로 정적 페이지 생성

src/app/(with-searchbar)/page.tsx:

// ...
 
export const dynamic = "force-static";
 
export default async function TestPage() {
  // ...
}

하위 태스크 6

에러 컴포넌트 구현

error.tsx로 에러 처리 UI 생성

src/app/book/[id]/error.tsx:

"use client"
 
export default function BookDetailError({ error }: { error: Error }) {
  return (
    <main>
      <p>오류가 발생했습니다.</p>
    </main>
  );
}

하위 태스크 7 ~ 8

notFound 함수 사용

존재하지 않는 리소스 처리

404 페이지 구현

커스텀 not-found.tsx 생성

src/app/book/[id]/page.tsx:

import type { Book } from "@/domains";
import { notFound } from "next/navigation";
import { load } from "@/utils";
 
export default async function BookDetailPage({ params }: {
  params: Promise<{ id: string; }>
}) {
  const books = await load<Book[]>("http://localhost:3005/books");
 
  const { id } = await params;
  const book = books.find((book) => book.id === id);
 
  if (book === undefined) {
    notFound();
  }
 
  return (
    <main>
      <h2 className="mb-4 font-bold">{book.title}</h2>
      <ul>
        <li>작가: {book.authors.join(", ")}</li>
        <li>장르: {book.genres.join("/")}</li>
        <li>요약: {book.summary}</li>
        <li>가격: {
          new Intl.NumberFormat("ko-KR", {
            style: "currency", currency: "KRW"
          }).format(book.price)
        }</li>
      </ul>
    </main>
  );
}

하위 태스크 9

환경 변수 설정

.env.local에 API URL 설정

.env.local:

API_URL=http://localhost:3005

src/app/book/[id]/page.tsx:

export default async function BookDetailPage({ params }: {
  params: Promise<{ id: string; }>
}) {
  const books = await load<Book[]>(`${process.env.API_URL}/books`);
 
  // ...
}