Frontend/Next.js
[Next.js/App Router] 9. 스트리밍과 에러 핸들링
ayeongjin
2025. 2. 10. 00:18
스트리밍이란?
데이터가 너무 커서 빠르게 전송하기 어려울 때, 데이터를 작은 단위로 나누어 전송하는 방식
1) Next.js에서 제공하는 HTML 스트리밍
- 사용자가 페이지에 접속하면 먼저 렌더링이 빠른 단순한 컴포넌트를 표시
- 데이터 페칭 등 렌더링이 오래 걸릴 것으로 예상되는 컴포넌트는 대체 UI를 표시했다가 나중에 렌더링
- 장점:
- 사용자가 빠르게 콘텐츠를 볼 수 있도록 UX 개선
- 로딩 바 등 대체 UI를 보여줄 수 있어 더 좋은 환경에서 대기 가능
✅ Next.js의 스트리밍은 Dynamic Page에서 주로 사용된다.
스트리밍 적용하기
1) 페이지 - 자동 스트리밍
- loading.tsx 파일을 생성하면 자동으로 스트리밍 적용됨
// src/app/(with-searchbar)/search/loading.tsx
export default function Loading() {
return <div>Loading ...</div>;
}
// src/app/(with-searchbar)/search/page.tsx
import BookItem from "@/components/book-item";
import { BookData } from "@/types";
import { delay } from "@/util/delay";
export default async function Page({ searchParams }: { searchParams: { q?: string; }; }) {
await delay(1500);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/search?q=${searchParams.q}`,
{ cache: "force-cache" }
);
const books: BookData[] = await response.json();
return (
<div>
{books.map((book) => (
<BookItem key={book.id} {...book} />
))}
</div>
);
}
주의할 점
- search 폴더 내에 새로운 하위 폴더 (예: settings/)를 추가하면 자동으로 loading.tsx 적용됨
- loading.tsx는 async가 적용된 비동기 컴포넌트에서만 적용 가능
- loading.tsx는 반드시 page.tsx에만 적용 가능 (다른 컴포넌트에는 Suspense 사용 필요)
- 쿼리스트링이 변경될 때는 적용되지 않음 → 해결 방법: Suspense 사용
2) 컴포넌트 - Suspense 적용
- Suspense 컴포넌트로 감싸면 자동으로 스트리밍 적용된다.
import BookItem from "@/components/book-item";
import { BookData } from "@/types";
import { delay } from "@/util/delay";
import { Suspense } from "react";
// 비동기작업 컴포넌트 분리
async function SearchResult({ q }: { q: string }) {
await delay(1500);
const response = await fetch(
`${process.env.NEXT_PUBLIC_API_SERVER_URL}/book/search?q=${q}`,
{ cache: "force-cache" }
);
const books: BookData[] = await response.json();
return (
<div>
{books.map((book) => (
<BookItem key={book.id} {...book} />
))}
</div>
);
}
export default function Page({ searchParams }: { searchParams: { q?: string; }; }) {
return (
<Suspense
key={searchParams.q || ""}
fallback={<div>Loading ...</div>}
>
<SearchResult q={searchParams.q || ""} />
</Suspense>
);
}
- 비동기 작업을 하는 모든 컴포넌트를 분리하여 Suspense로 감싸면 스트리밍 적용됨
- fallback 속성을 사용해 대체 UI 제공 가능
- key 값을 설정하면 매번 새로운 검색어로 변경될 때 새로운 요청을 보냄
스켈레톤 UI 적용하기
대략적인 UI 구조를 미리 보여주고 데이터를 불러오는 동안 사용자 경험을 개선하는 기법
// src/components/skeleton/book-item-skeleton.tsx
import style from "./book-item-skeleton.module.css";
export default function BookItemSkeleton() {
return (
<div className={style.container}>
<div className={style.cover_img}></div>
<div className={style.info_container}>
<div className={style.title}></div>
<div className={style.subtitle}></div>
<div className={style.author}></div>
</div>
</div>
);
}
// src/components/skeleton/book-item-skeleton.tsx
import BookItemSkeleton from "./book-item-skeleton";
export default function BookListSkeleton({
count,
}: {
count: number;
}) {
// count개만큼 배열 생성,
return new Array(count)
.fill(0)
.map((_, idx) => (
<BookItemSkeleton key={`book-item-skeleton-${idx}`} />
));
}
// 적용
<Suspense fallback={<BookListSkeleton count={10} />}>
<AllBooks />
</Suspense>
// src/components/skeleton/book-item-skeleton.module.css
.container {
display: flex;
gap: 15px;
padding: 20px 10px;
border-bottom: 1px solid rgb(220, 220, 220);
}
.cover_img {
width: 80px;
height: 105px;
background-color: rgb(230, 230, 230);
}
.info_container {
flex: 1;
}
.title,
.subtitle,
.author {
width: 100%;
height: 20px;
background-color: rgb(230, 230, 230);
}
- 로딩 중에도 사용자가 UI 구조를 인지할 수 있도록 Skeleton UI 적용 가능
✅ react-loading-skeleton 라이브러리도 사용하면 자동으로 스켈레톤 UI 생성 가능
에러 핸들링 (Error Handling)
Next.js에서는 에러가 발생했을 때 전용 컴포넌트를 활용해 대체 UI 제공 가능
// error.tsx
// 서버 또는 클라이언트 중 어떤 컴포넌트에서 발생하는 오류든 다 적용할 수 있도록 클라이언트 컴포넌트로 설정
"use client";
import { useRouter } from "next/navigation";
import { startTransition, useEffect } from "react";
export default function Error({
error, // 현재 발생한 에러의 원인 또는 메세지 -> 전달된 props에서 사용 가능
// 자바스크립트 에러 타입
reset, // 에러가 발생하는 페이지를 복구하기 위한 함수, void 타입
}: {
error: Error;
reset: () => void;
}) {
const router = useRouter();
useEffect(() => {
console.error(error.message);
}, [error]);
return (
<div>
<h3>오류가 발생했습니다</h3>
<button
onClick={() => {
// 함수 하나를 인수로받아서, 해당 함수 내부의 코드를 동기적으로 실행
startTransition(() => {
router.refresh(); // 현재 페이지에 필요한 서버컴포넌트들을 다시 불러옴
reset(); // 에러 상태를 초기화, 컴포넌트들을 다시 렌더링
});
}}
>
다시 시도
</button>
</div>
);
}
- error.tsx 파일을 특정 폴더에 두면 해당 경로의 모든 하위 페이지에서 적용 가능
- reset() 함수를 사용하여 에러 상태를 초기화하고 다시 요청 가능
- Next.js의 자동 에러 핸들링을 활용하여 UX 개선 가능
🎯 배운점
✔ Next.js의 스트리밍 기능을 활용하면 더 빠르고 유연한 사용자 경험 제공 가능
✔ 페이지 로딩을 최적화하고, Suspense 및 loading.tsx를 사용하여 성능 개선 가능
✔ 스켈레톤 UI, 에러 핸들링 등을 적용하여 더 나은 UX 제공 가능