Next.js에서는 HTML 스트리밍 기능을 통해 사용자에게 더 빠르고 부드러운 페이지 로딩 경험을 제공할 수 있습니다.
이 게시물에서는 스트리밍이 무엇인지부터 Next.js에서의 사용법, 그리고 loading.tsx와 React.Suspense를 적절히 활용하는 방법까지 하나하나 예제를 통해 소개하겠습니다.
✅ 스트리밍(Streaming)이란?
스트리밍은 서버에서 클라이언트로 데이터를 한번에 모두 전달하는 것이 아니라, 작게 나눈 데이터 조각을 순차적으로 전송하는 방식입니다.
예를 들어, 서버에서 큰 데이터를 처리하거나 시간이 오래 걸리는 작업이 있다면,
그 작업이 끝나기를 기다리지 않고 준비된 부분부터 먼저 보내는 것이 스트리밍입니다.
덕분에 사용자는 모든 데이터가 준비되기 전에도 화면을 볼 수 있어서 로딩 시간을 체감상 짧게 느낄 수 있게 됩니다.
🚀 Next.js에서의 HTML 스트리밍
Next.js는 이런 스트리밍 방식을 HTML 페이지 렌더링에도 도입했습니다.
다음과 같은 방식으로 사용됩니다.
- 먼저 빠르게 렌더링 가능한 컴포넌트를 보여주고,
- 시간이 오래 걸리는 컴포넌트는 로딩 UI를 보여주며 기다리게 한 뒤,
- 렌더링이 완료된 컴포넌트만 따로 후속 전송합니다.
이를 통해 화면이 텅 빈 상태로 오래 대기하지 않고, 일단 보여줄 수 있는 부분부터 사용자에게 전달하게 됩니다.
🧩 스트리밍이 필요한 상황 - Dynamic Page
Dynamic Page는 빌드 시점이 아니라 요청 시마다 생성되는 페이지입니다. 따라서 캐싱이 어렵고,
페이지 내부의 컴포넌트들이 동기적으로 모두 준비될 때까지 기다려야 하는 구조입니다.
👉 이럴 때 스트리밍을 적용하면, 데이터 패칭이 느린 컴포넌트가 전체 로딩을 지연시키는 것을 방지할 수 있습니다.
오래 걸리지 않는 컴포넌트만 빠르게 렌더링하여 곧바로 브라우저에게 응답해줌으로써 사용자에게 빈 화면 대신 일단 레이아웃처럼 빠르게 렌더링할 수 있는 부분이라도 보여주고 오래 걸리는 부분은 로딩바 같은 대체 ui를 보여주고 있게 됩니다.
데이터 패칭이 완료되어 오래 걸리는 컴포넌트까지 렌더링이 완료되면 후속으로 렌더링된 데이터를 보내줍니다.
이렇게 실제 데이터를 채워 넣어서 사용자가 로딩 화면을 조금이라도 덜 지루하게 느낄 수 있도록 아주 좋은 경험을 제공할 수 있게 됩니다.
🔧 스트리밍 적용 방법
1. loading.tsx 파일로 스트리밍 설정
loading.tsx는 동일 경로의 페이지 컴포넌트와 그 하위 컴포넌트들 전체에 스트리밍을 적용합니다.
/app/search/page.tsx
/app/search/loading.tsx
// search/loading.tsx
export default function Loading() {
return <div>Loading ...</div>;
}
search/page.tsx가 데이터를 패칭 중일 때, 먼저 loading.tsx가 렌더링됩니다.
⚠️ 주의사항
⓵ loading.tsx는 하위 페이지도 모두 스트리밍에 포함시킵니다.
- /search/setting/page.tsx도 자동으로 스트리밍 적용됨
// search/setting/page.tsx
import { delay } from "@/util/delay";
export default async function Page() {
await delay(2000);
return <div>setting page</div>;
}
- /search/page.tsx
- /search/setting/page.tsx
모두 search/loading.tsx의 로딩 컴포넌트를 사용하게 됩니다.
따라서 이와 같이 /search/setting 경로로 접속하게 되면 자동 스트리밍이 적용된 것을 확인할 수 있습니다.
⓶ async 함수로 작성된 페이지 컴포넌트에서만 동작합니다.
✅ 작동 예시
export default async function Page() { ... } // ⭕ async 함수
❌ 작동하지 않는 예시
export default function Page() { ... } // ❌ 동기 함수
⓷ 일반 컴포넌트에서는 사용할 수 없습니다. (layout.tsx, components/* 불가)
- 레이아웃(layout.tsx)이나 일반 컴포넌트(components/SomeComponent.tsx)에는 적용되지 않습니다.
- 일반 컴포넌트에 로딩 UI를 보여주고 싶다면 React의 Suspense를 직접 사용해야 합니다.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
⓸ 쿼리스트링 변경 시 재렌더링 트리거가 되지 않습니다.
/search?query=자바 ➡ /search?query=최적화
- 페이지 자체는 바뀌지 않았기 때문에 로딩 화면이 다시 나타나지 않음
2. Suspense로 컴포넌트 단위 스트리밍 제어
export default async function Page({ searchParams }: { searchParams: Promise<{ q?: string }> }) {
const { q } = await searchParams;
return (
<Suspense fallback={<div>Loading...</div>}>
<SearchResult q={q || ""} />
</Suspense>
);
}
- fallback은 대체 UI를 설정합니다.
해당 페이지를 확인해보면 Suspense 컴포넌트를 통해 스트리밍이 설정된 것을 확인할 수 있습니다.
- key 값을 쿼리 스트링 기반으로 지정하면, 쿼리 변경 시마다 리렌더링이 가능합니다.
return (
<Suspense key={q || ""} fallback={<div>Loading...</div>}>
<SearchResult q={q || ""} />;
</Suspense>
);
이와 같이 key 옵션을 추가하면 쿼리스트링이 '자바'로 설정된 상태에서 '최적화'로 변경하게 되어도 로딩화면이 나타납니다.
3. 페이지 내 여러 컴포넌트를 동시에 스트리밍하기
아래처럼 페이지 내 여러 컴포넌트를 Suspense로 감싸면 각 컴포넌트별로 렌더링 시점을 제어할 수 있습니다.
async function AllBooks() {
await delay(1500);
(...)
}
async function RecoBooks() {
await delay(3000);
(...)
}
export const dynamic = "force-dynamic"; // dynamic 페이지로 강제 설정
export default function Home() {
return (
<div className={style.container}>
<section>
<h3>지금 추천하는 도서</h3>
<Suspense fallback={<div>도서를 불러오는 중입니다...</div>}>
<RecoBooks />
</Suspense>
</section>
<section>
<h3>등록된 모든 도서</h3>
<Suspense fallback={<div>도서를 불러오는 중입니다...</div>}>
<AllBooks />
</Suspense>
</section>
</div>
);
}
이렇게 하면 빠른 컴포넌트부터 순차적으로 렌더링(AllBooks → RecoBooks)되어, UX가 훨씬 부드럽게 됩니다.
✨ 마무리 정리
방법 | 장점 | 주의사항 |
loading.tsx | 페이지 전체 로딩 관리에 적합 | 쿼리스트링 변경 감지 불가 |
Suspense + fallback | 컴포넌트 단위 제어, 쿼리 감지 가능 | 복잡한 UI 구성 시 관리 필요 |
'📍 프로그래밍 언어 > Next.js' 카테고리의 다른 글
[ Next.js ] 서버 액션(Server Actions) - API 없이 서버에서 함수 실행하기 (1) | 2025.06.28 |
---|---|
[ Next.js ] 클라이언트 라우터 캐시와 레이아웃 최적화 원리 (1) | 2025.06.26 |
[ Next.js ] 라우트 세그먼트(Route Segments): dynamic 옵션 정리 (2) | 2025.06.25 |
[ Next.js ] 캐싱 전략 이해하기: 풀 라우트 캐시(Full Route Cache) (1) | 2025.06.25 |
[ Next.js ] 중복 API 요청을 줄이는 방법: Request Memoization (1) | 2025.06.17 |