하위 태스크 1 ~ 2
Suspense 기본 사용
컴포넌트를 Suspense로 감싸기
fallback 구현
로딩 중 표시할 UI 작성
app/(with-searchbar)/layout.tsx:
import { ReactNode, Suspense } from "react";
import Form from "next/form";
import { load } from "@/utils"
async function SearchBar() {
const books = await load<Book[]>("http://localhost:3005/books");
return (
<Form className="flex gap-x-1" action="/search">
<input
className="bg-slate-400 border border-white"
type="text"
name="q"
required
placeholder={`총 ${books.length}개의 책 보유 중...`}
/>
<button className="px-1 bg-slate-50 border border-white" type="submit">검색</button>
</Form>
);
}
export default function SearchableLayout({ children }: { children: ReactNode }) {
return (
<>
<div className="flex w-80 p-2 mx-auto bg-slate-300">
<Suspense fallback={<div>검색창 로딩 중...</div>}>
<SearchBar />
</Suspense>
</div>
{children}
</>
);
}로딩 전:

로딩 후:

하위 태스크 3 ~ 5
스켈레톤 UI 디자인
실제 콘텐츠와 유사한 로딩 UI
스켈레톤 애니메이션
펄스 효과 등 애니메이션 추가
병렬 스트리밍
여러 독립적인 데이터 동시 스트리밍
components/BookListSkeleton.tsx:
export default function BookListSkeleton() {
return (
<ul className="pl-4 list-disc underline">
<li className="w-40 my-1 bg-slate-200 animate-pulse"></li>
<li className="w-40 my-1 bg-slate-200 animate-pulse"></li>
<li className="w-40 my-1 bg-slate-200 animate-pulse"></li>
<li className="w-40 my-1 bg-slate-200 animate-pulse"></li>
</ul>
);
}app/(with-searchbar)/page.tsx:
import type { Book } from "@/domains";
import { Suspense } from "react";
import Link from "next/link";
import BookListSkeleton from "@/components/BookListSkeleton";
import { load } from "@/utils";
async function loadAllBooks() {
return load<Book[]>("http://localhost:3005/books");
}
async function loadRecoBooks(){
const books = await load<Book[]>("http://localhost:3005/books");
const shuffledBooks = books.sort(() => Math.random() - 0.5);
return shuffledBooks.slice(0, books.length / 2);
}
async function AllBooks() {
const allBooks = await loadAllBooks();
return (
<ul className="pl-4 list-disc underline">
{allBooks.map(({ id, title }) => <li key={id}><Link href={`/book/${id}`}>{title}</Link></li>)}
</ul>
);
}
async function RecoBooks() {
const recoBooks = await loadRecoBooks();
return (
<ul className="pl-4 list-disc underline">
{recoBooks.map(({ id, title }) => <li key={id}><Link href={`/book/${id}`}>{title}</Link></li>)}
</ul>
);
}
export default async function TestPage() {
return (
<main className="p-4">
<h2 className="mb-4 font-bold">전체 출판물</h2>
<Suspense fallback={<BookListSkeleton />}>
<AllBooks />
</Suspense>
<h2 className="my-4 font-bold">추천 출판물</h2>
<Suspense fallback={<BookListSkeleton />}>
<RecoBooks />
</Suspense>
</main>
);
}
하위 태스크 6 ~ 8
loading.tsx 구현
페이지 레벨 로딩 UI
로딩 우선순위 설정
중요한 콘텐츠 우선 표시
사용자 경험 최적화
로딩 경험 개선
app/(with-searchbar)/search/loading.tsx:
export default function SearchLoading() {
return <p className="p-4">콘텐츠 로딩 중...</p>;
}app/(with-searchbar)/page.tsx:
import type { Book } from "@/domains";
import Link from "next/link";
import { load } from "@/utils";
export default async function SearchPage() {
const allBooks = await load<Book[]>("http://localhost:3005/books");
return (
<main className="p-4">
<h2 className="mb-2 font-bold">검색</h2>
<input className="mb-2 bg-slate-200 border border-slate-500" type="text" placeholder="검색어" />
<ul className="pl-4 list-disc underline">
{allBooks.map(({ id, title }) => <li key={id}><Link href={`/book/${id}`}>{title}</Link></li>)}
</ul>
</main>
);
}로딩 전:

로딩 후:
