Loading과 Streaming

notion image
 

SSR의 한계

notion image
동적 콘텐츠의 경우 SSR을 통하게 된다.
이건 사용자의 요청이 수신되면 그 때 서버에서 렌더링을 진행하고 반환하는 것을 의미한다.
그런데 서버 컴포넌트를 만드는 데 필요한 api 요청 등이 시간이 많이 걸린다면?
  • 위 그림에서 A가 길어지는 경우에 해당
사용자는 SSR이 완료될 때까지 초기 html을 받을 수 없고, 따라서 긴 시간동안 빈 화면을 보게 될 수 있다.
 
사용자가 초기 화면을 빠르게 볼 수 있도록 하기 위한 용도로 서버 사이드 렌더링을 많이 활용하는데,
그 목적이 훼손될 수 있다.
 
조금 더 자세히 설명하면
  1. SSR은 동적인 컨텐츠를 지니고 있어서, 미리 만들어둘 수 없고, 요청받은 시점에 생성하는 것을 뜻한다.
  1. 이를 위해서 next.js로 빌드하면, module graph 형태로 갖고 있고,
  1. 사용자가 요청을 하면 이 module graph를 기반으로 RSC Payload를 만들게 된다.
  1. RSC Payload에는 props 등이 다 포함된 형태다.
  1. 따라서 RSC Payload를 만드는 과정 속에 SSR에 필요한 data fetch 등의 네트워크 작업들이 포함된다.
  1. 즉, 네트워크 요청 처리가 무거운 경우 RSC Payload의 생성도 지연되게 된다.
  1. RSC Payload가 완성되면, 이걸 기반으로 SSR을 수행하여 사용자에게 보여질 초기 html을 생성한다.
📌
정리하면, 네트워크 요청 등이 무겁다면 결론적으로 사용자가 볼 초기 html 생성도 그만큼 느려진다.
따라서 사용자가 빈 화면을 보는 시간도 길어진다.

즉각적으로 보여질 html 만들기

위의 SSR의 한계점에 대한 대안으로 네트워크 요청 처리 등이 처리되기 전,
사용자에게 즉각적으로 보여줄 html을 먼저 보내서 빈화면을 보지 않도록 하는 방법이 대안이 된다.
방법은 크게 2가지가 있다.

Loading

notion image
loading.js 파일을 선언해두면 알아서 React Suspense를 이용해서 컴포넌트를 대체해준다.
 
loading 파일을 포함해서 초기 html을 만들어서 사용자에게 즉각적으로 보여질 1차 html을 보낸다.
이후 백그라운드에서 SSR을 수행하고, 페이지 컴포넌트가 완성되면 자연스럽게 swap 시켜준다.
 
notion image
내부적으로는 위 그림과 같이 동작하게 된다.
Next.js는 프레임워크니까, 내부적으로는 React의 Suspensefallback을 이용해서 layout.children을 감싼다.
📌
정리하면, 1. Layout + loading 즉시 표시 가능한 로딩 화면의 1차 html을 빠르게 만들어서 보내고, 2. 이후 백그라운드에서 SSR을 수행하고 2차 html + RSC Payload를 보내서 업데이트 한다. 3. 이후 RSC Payloadjs bundle을 기반으로 CSR이 수행된다.

Streaming

loading.js는 page.tsx 통째로 로딩 컴포넌트로 감싸는 방식이다.
SSR 과정에서 컴포넌트 별로 처리되는 데 필요한 시간이 각각 다르다.
빠르게 사용자에게 보여줄 수 있는 컴포넌트도 있고, 속도가 느린 컴포넌트도 있을텐데, loading.js를 이용하게 되면 페이지 기준에서 필요한 모든 것이 처리된 후에야 page.tsx가 swap 된다.
 
notion image
스트리밍은 컴포넌트가 점진적으로 렌더링 될 수 있도록 도와준다.
next.js의 app route는 페이지 단위가 아닌, 컴포넌트 단위로 전략(SSR, CSR)을 선택할 수 있도록 했다.
즉 컴포넌트 단위로 처리될 수 있도록 설계되었고,
streaming은 loading.js의 컴포넌트화된 버전이다.
 
notion image
서버 컴포넌트 별로 소요되는 시간이 다르다.
api가 무겁다면 늦게 처리되고, 가벼운 것은 빠르게 처리될 것이다.
처리가 완료된 컴포넌트를 바로바로 사용자에게 업데이트 시켜주는 방식이 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> ) }
이렇게 각 컴포넌트 별로 Suspensefallback으로 구성시켜주면 Streaming이 된다.
페이지 단위가 아니라 chunk 단위로 처리할 수 있도록 되었고, React에서 chunk는 컴포넌트를 의미한다.
 
notion image
컴포넌트 단위로 다르게 스트리밍 된다.
먼저 완료된 것은 화면에 표시되고, 아직 처리되지 않은 컴포넌트는 fallback을 보여준다.

Grouping components

By default, the whole tree inside Suspense is treated as a single unit.
reactjsreactjsSuspense – React
React의 Suspense를 읽어보면 Suspense 하위에 있는 컴포넌트는 하나의 유닛으로 간주된다.
즉 여러개의 컴포넌트가 하나의 skeleton 으로 묶이고, 완료됐을 때 한번에 보여지길 원한다면 그 범위만큼 묶어서 Suspense로 감싸주면 된다.
<Suspense fallback={<Loading />}> <Biography /> <Panel> <Albums /> </Panel> </Suspense>

Suspsense 중첩

reactjsreactjsSuspense – React
<Suspense fallback={<BigSpinner />}> <Biography /> <Suspense fallback={<AlbumsGlimmer />}> <Panel> <Albums /> </Panel> </Suspense> </Suspense>
Suspense 중첩도 가능하다. 각 Suspense는 독립적으로 처리된다.
notion image
위 그림에선 다음과 같이 처리된다.
Biography → Albums 순으로 완료됐을 때
  • Biography가 완료됐다면 BigSpinner가 해제된다.
  • Albums가 아직 처리 중이라면 AlbumsGlimmer는 보여진다.
Albums → Biography 순으로 완료됐을 때
  • Albums는 완료되었지만, 상위가 아직 완료되지 않았으므로 BigSpinner가 보여진다.
  • Biography가 완료되면 BigSpinner가 해제된다.