하위 태스크 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`);
// ...
}