스트리밍은 Next.js13 App Router와 함께 도입되었으며, 이후 Next.js14 에서는 더욱 최적화되어 서버 액션과 함께 서버에서 데이터를 바로 조작하고 스트리밍 할 수 있도록 개선되었습니다.
이번 글에서는 Next.js 15 버전을 공부하면서 새롭게 알게된 스트리밍에 대해서 정리해보려고 합니다!🙂
스트리밍이란 서버에서 클라이언트로 매우 큰 용량의 데이터를 보낼 줄 때 데이터를 잘게 쪼개서 보내주는 것을 말합니다.
기존 SSR 방식은 모든 데이터를 서버에서 처리한 후 한번에 클라이언트로 전송하지만, 스트리밍 기반 SSR은 서버에서 준비된 데이터부터 점진적으로 클라이언트에 전송하기 때문에 전체 데이터가 다 준비되지 않아도 준비된 부분부터 먼저 화면에 표시가 가능합니다.
결과적으로 사용자는 페이지 로딩을 기다리는 시간이 줄고 빠르게 화면을 볼 수 있게됩니다.
Next.js에서는 페이지 스트리밍과 컴포넌트 스트리밍을 지원합니다.먼저 렌더링된 컴포넌트(동기 작업)를 보여주고, 느리게 렌더링 되는 컴포넌트(비동기 작업)는 대체 UI를 보여주게 됩니다.
Next.js에서는 페이지 단위에서 스트리밍을 적용할 수 있습니다.
예를 들어 page.tsx
에서 데이터를 불러오는 작업이 오래 걸리는 경우, 해당 디렉토리에 loading.tsx
파일을 함께 정의하면 해당 페이지가 로딩 중일 때 loading.tsx
에서 지정한 로딩 UI를 먼저 보여줄 수 있습니다.
// page.tsx
export default function Page({
searchParams,
}: {
searchParams: { q?: string };
}) {
return (
<div>
<div>test</div>
<SearchResult q={searchParams.q || ""} />
</div>
);
}
// loading.tsx
export default function Loading() {
return <div>로딩 중...</div>;
}
위의 코드에서 SearchResult 컴포넌트가 API 호출을 통해 데이터를 받아와야 한다면, 해당 데이터가 준비될 때까지 아무것도 표시되지 않을 수 있습니다. 이를 해결하기 위해 동일한 경로에 loading.tsx
파일을 추가하면, SearchResult가 로딩되는 동안 대체 UI를 표시할 수 있습니다.
페이지 스트리밍 시에 몇 가지 주의해야할 점이 있습니다.
loading.tsx
는 현재 경로에 있는 page 컴포넌트 뿐만 아니라 마치 layout 처럼 해당 경로 아래에 있는 모든 비동기 page 컴포넌트를 스트리밍 되도록 만든다.
스트리밍은 async가 적용된 비동기 페이지에서만 동작합니다.
→ page.tsx
가 서버 컴포넌트이어야 합니다.
loading.tsx
는 page.tsx
에만 적용되며, layout.tsx
나 일반 컴포넌트에서는 사용할 수 없습니다.
쿼리 스트링 변경 시에는 loading.tsx
가 트리거되지 않습니다.
→ URL의 쿼리 스트링만 바뀌는 경우에는 다시 로딩 상태로 돌아가지 않습니다.
그렇다면 만약 전체 페이지에 대한 스트리밍을 적용하지 않고, 특정 컴포넌트 혹은 클라이언트 컴포넌트에서 스트리밍하고 싶다면 어떻게 해야할까요? 그때 Suspense를 사용해서 해결할 수 있습니다.
React의 Suspense는 비동기 컴포넌트가 로딩되는 동안 대체 UI를 제공하는 기능입니다. 즉, 비동기 작업이 완료될 때까지 사용자가 기다리지 않도록, 미리 준비된 UI를 보여주고 이후 데이터를 채워 넣을 수 있도록 도와줍니다.
Suspense는 fallback 속성을 통해 비동기 작업이 끝날 때까지 대체 UI를 보여 줄 수 있습니다.
비동기 작업이 끝나게 되면 Suspense로 감싸고 있던 컴포넌트를 렌더링하여 보여주게 됩니다.
페이지 스트리밍에서 부족했던 부분을 다음과 같은 보완할 수 있습니다.
export default function Page({
searchParams,
}: {
searchParams: { q?: string };
}) {
return (
<div>
<div>test</div>
<Suspense
key={searchParams.q || ""}
fallback={<BookListSkeleton count={3} />}
>
<SearchResult q={searchParams.q || ""} />
</Suspense>
</div>
);
}
페이지 스트리밍에서는 쿼리 스트링 변경 시에는 loading.tsx
가 트리거되지 않습니다. 하지만 쿼리 스티링이 바뀌면서 새로 데이터를 요청해야하는 경우가 있습니다. Suspense에서는 key 속성을 사용하여 컴포넌트가 리렌더링되도록 할 수 있습니다.
기본적으로 Suspense 컴포넌트는 최초로 한번 내부 컴포넌트 로딩이 완료된 이후로는 내부의 콘텐츠가 변경되어도 로딩 상태로 돌아가지 않습니다. 따라서 key 값을 동적으로 설정하여 key값이 변할 때마다 새로운 컴포넌트로 인식하게 하여 매번 로딩 상태를 보여주도록 합니다.
Suspense는 페이지 전체가 아닌 특정 컴포넌트에만 스트리밍을 적용할 수 있습니다. 페이지 내 여러 개의 컴포넌트 중 일부만 비동기적으로 데이터를 로딩할 때, 그 부분만 대체 UI를 보여줄 수 있습니다.
위 코드에서 <div>test</div>
영역은 바로 보여지고, <SearchResult />
영역만 비동기 작업을 수행하는 동안 대체 UI안 BookListSkeleton를 보여주고 있습니다.