최근에 진행한 프로젝트에서, SSR되는 페이지에 렌더링 속도가 느려 UX 저하시키는 문제를 맞닥드렸습니다.
앞서 공부했던 Skeleton이나 Loading UI 화면을 띄울 필요가 있었고, Next페이지를 찾아보니 loading.tsx라는 파일을 도메인 디렉토리 안에 넣어주면 해결되는 엄청 간단한 문제였습니다. (참고로 앱 라우터 사용)

어떤 원리로 돌아가는건지 궁금해져서 공부해 보겠습니다.
https://react.dev/reference/react/Suspense
<Suspense>lets you display a fallback until its children have finished loading.
<Suspense>는 children 로딩이 끝날 때 까지 fallback을 디스플레이 해줍니다
React에서 기본으로 제공되는 컴포넌트로, 칠드런 컴포넌트(원래 디스플레이 되어야 할 컴포넌트)의 로딩이 끝나기 전 까지 fallback(로딩, 스켈레톤 UI)의 props를 디스플레이합니다.
<Suspense fallback={<Loading />}>
<SomeComponent />
</Suspense>
해당 기능을 활용해서 Next.js에서도 route segment content가 로딩 되는 동안 서버에서 instant loading state를 표시하는 loading.js이 제작되었습니다. (한국어로 어떻게 번역 해야할지 모르겠는 나,,,)
Instant Loading States== 네비게이션 시 즉시 표시되는 대체 UI
앞서 말했듯이, 이 Instant Loading State를 담고 있는 loading.js파일을 도메인 디렉토리 하위에 생성하면 됩니다. 같은 폴더 내에서 loading.js는 layout.js 안에 중첩시켜 page.js와 loading.js를 <Suspense>구조로 감싸 각각 fallback, children으로 알잘딱 분배를 해주는 원리 입니다.
Next.js측에서는 loading.js를 생성한 구조를 사용할 것을 추천합니다. (최적화 땜시) 하지만 <Suspense>를 manually활용하는 방법도 소개해주고 있는데요, 해당 방법을 시도하려면 1. App Router를 사용하고 2. streaming 기능이 활용되어야 합니다.
스트리밍이 어떻게 넥스트에서 작동하느냐에 대해 알아보려면 SSR에 대한 이해가 우선적으로 필요합니다. (SSR에 대해 자세히 작성한 글이 있으니 짧게 얘기하고 넘어가겠습니다.)
SSR에서 사용자가 페이지를 보고 상호작용이 일어나기 까지 아래와 같은 과정이 발생합니다.
- 특정 페이지에 필요한 모든 데이터를 서버에서 가져옵니다.
- 서버가 페이지의 HTML을 렌더링합니다.
- 페이지의 HTML, CSS, JavaScript가 클라이언트로 전송됩니다.
- 생성된 HTML과 CSS를 사용해 비상호작용 사용자 인터페이스가 표시됩니다.
- 마지막으로 React가 사용자 인터페이스를 하이드레이션하여 상호작용이 가능하도록 만듭니다.
해당 단계들은 각각 순차적(sequential)으로 이루어집니다. 두개의 단계가 동시에 일어나거나, 하나가 끝나갈 때 쯤 다음 단계 선 시작 이런 구조가 아니라는 이야긴데요, 서버는 모든 데이터를 가져온 후에만 페이지의 HTML을 렌더링할 수 있습니다. 또한 클라이언트는 모든 페이지의 코드를 다운 받은 후에야 React가 UI 하이드레이션이 가능해집니다.
모든 데이터 가져오기가 서버에서 완료된 후에만 페이지를 사용자에게 표시할 수 있기 때문에 사용자는 '느리다'라고 생각할 수 밖에 없을텐데, 스트리밍(Streaming)은 페이지의 HTML을 더 작은 조각으로 나누고, 이 조각들을 서버에서 클라이언트로 점진적으로 전송할 수 있도록 합니다.
HTML을 조각내어 서버에 한꺼번에 전송하는게 아니라, 점진적으로 전송해 주는 방식입니다. 모든 데이터가 로드될 때까지 기다리지 않고도 페이지의 일부를 더 빨리 표시할 수 있게 만들어 주는데요, 스트리밍(Streaming)은 React의 컴포넌트 모델과 잘 작동합니다.
우선순위가 높은 컴포넌트(예: 제품 정보)나 데이터를 포함하지 않는 컴포넌트(예: 레이아웃)을 먼저 전송하여 React에서 해당 컴포넌트로 하이드레이션을 더 빨리 시작 시킵니다. 우선순위가 낮은 컴포넌트(예: 리뷰, 관련 제품)는 데이터를 가져온 후 동일한 서버 요청에서 전송되어 비교적 하이드레이션을 늦게 시작하는거죠.

위 사진을 보면 하나의 chunk로 들어오던 각 A, B, C, D의 단계를 작게 쪼개서 처리합니다.
<Suspense> 직접 어떻게 쓰는데?스트리밍(Streaming)은 기본적으로 Next.js의 App Router에 내장되어 있습니다. 따라서 <Suspense>를 넥스트에서 사용할 때 별도의 streaming를 위한 처리해줄 필요가 없습니다.
다만 비동기 함수와 인터랙션이 있을 때만 작동(당연함 새 데이터를 가져와야 되는 거니까)하기 때문에, 적제적소하게 활용하기는 나름일 것 같네요ㅎㅎ
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
export default function Posts() {
return (
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>
)
}
https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming#instant-loading-states
https://nextjs.org/docs/app/building-your-application/rendering/server-components#streaming