React use()와 Suspense: 워터폴 없는 데이터 스트리밍
React use()와 Suspense: 워터폴 없는 데이터 스트리밍
워터폴에는 못생긴 산술적 특성이 있습니다. 체인으로 연결된 4개의 300ms 요청은 300ms에 도착할 수 있었던 데이터를 위해 1.2초의 빈 화면이 됩니다.
일시 중단된 컴포넌트가 실제로 하는 것
준비가 되지 않은 컴포넌트는 Promise를 던집니다. 오류가 아닌 Promise를. React는 Error Boundary가 오류를 잡는 것처럼 그것을 잡고, 부분적으로 렌더링된 작업을 버리고, 가장 가까운 폴백을 표시하고, 던져진 Promise를 구독합니다. 그것이 해결되면 React는 컴포넌트를 처음부터 다시 실행합니다.
Promise는 이 실행들 사이에서 안정적이어야 합니다. 모든 실행에서 새 Promise는 무한 일시 중단입니다. 이것이 use()를 망가뜨리는 가장 일반적인 방법입니다.
tsx
"use client";import { use } from "react";export default function Posts({ posts }) {// promise가 해결될 때까지 일시 중단const allPosts = use(posts);return (<ul>{allPosts.map((post) => (<li key={post.id}>{post.title}</li>))}</ul>);}
Suspense, 정적 셸, 잘못된 순서로 도착하는 HTML
React는 정적 셸을 즉시 보냅니다. 지연된 조각은 문서의 끝에, 보이지 않는 곳에 추가되고, React는 그 옆에 작은 인라인 <script>를 스트리밍하여 콘텐츠를 올바른 위치로 이동시킵니다.
tsx
import { Suspense } from "react";export default function Page() {return (<main><h1>대시보드</h1> {/* 즉시 전송 */}<Suspense fallback={<StatsSkeleton />}><Stats /></Suspense><Suspense fallback={<FeedSkeleton />}><Feed /></Suspense></main>);}
일찍 시작하고, 늦게 읽기
tsx
import { Suspense } from "react";import Posts from "@/app/ui/posts";import Comments from "@/app/ui/comments";export default function Page() {// 두 요청 모두 지금 병렬로 시작const posts = getPosts();const comments = getComments();return (<><Suspense fallback={<div>게시물 로딩 중...</div>}><Posts posts={posts} /></Suspense><Suspense fallback={<div>댓글 로딩 중...</div>}><Comments comments={comments} /></Suspense></>);}
아무도 예상하지 못하는 함정: 스트리밍은 HTTP 상태 코드를 사용
Suspense 폴백이 렌더링되고 스트림이 열리면, 서버는 이미 200 OK에 커밋되었습니다. 스트리밍된 경계 내부에서 트리거되는 notFound()는 진짜 404를 제공할 수 없습니다.
tsx
export default async function PostPage({ params }) {const { slug } = await params;const exists = await checkSlugExists(slug); // 빠르게, 스트림이 열리기 전에if (!exists) notFound(); // 진짜 404return (<Suspense fallback={<PostSkeleton />}><PostContent slug={slug} /></Suspense>);}
스트리밍이 Core Web Vitals에 미치는 영향
- TTFB가 떨어집니다 — 레이아웃 렌더링 시간 정도까지.
- LCP는 악화될 수 있고, 개선되지 않습니다. LCP 요소는 정적 셸에 유지하세요.
- CLS는 당신의 책임입니다.
- INP는 선택적 하이드레이션 덕분에 개선됩니다.
함정과 모범 사례
- Client Component 렌더링 중에 Promise를 생성하지 마세요.
- Suspense는 대기를 잡고, 오류를 잡지 않습니다. 각 경계를 Error Boundary와 쌍으로 사용하세요.
- 경계 크기를 사람이 인식하는 것처럼 맞추세요.
- 봇은 스트림을 받지 않습니다.
- Promise prop은 네트워크를 통과합니다.
결론
시퀀스 — 요청, 대기, 렌더링, 요청, 대기, 렌더링 — 대신 병렬 스트림을 얻습니다: 모든 것이 한번에 시작되고, 페이지의 각 부분은 자체 데이터가 도착하는 즉시 표시됩니다.