Loading UI and Streaming

김현학·2023년 9월 30일
0

NEXT Routing Concepts

목록 보기
7/11

목표

Next 공식 문서의 내용에 따라
request 시점에 server에서 이루어지는 data fetch 결과를 반영한 prerender를 onDemand 방식으로 로딩할 수 있도록 지원하고, 그 과정에서의 지연 시간에 보여줄 fallback component를 정의하는 방법을 소개한다.

내용

서론

웹 API의 호출 결과를 반영하여 컴포넌트를 최초 생성할 때,
data fetch 과정이 완료될 때까지는 렌더링을 완료할 수 없다.

이러한 지연에 대한 UX를 향상시키기 위해 React.Suspense를 통해
Skeleton 또는 Blurred Image를 보이도록 구성할 수 있다.

현재 React.Suspense는 React Native에서는 반드시 React.lazy와 사용해야하며,
예외적으로 Relay나 Next.js와 같은 Suspense-enabled 프레임워크에서는 Data fetching 과정이 필요한 범위에 대해 독립적으로 활용할 수 있다.

이하 내용은 본 내용을 이해하기 위한 기본 원리를 설명한다.

How Routing and Navigation Works

App Router를 사용할 때, routing과 navigation에 있어서
Route Tree의 구성요소가 되는 Route Segment가 단위로서 사용된다.

서버 측에서 빌드할 때 Route Segment 단위로 자동 Code-split 해놓고,
클라이언트 측에서의 요청을 받으면
브라우저가 전체 페이지를 reloading하지 않고
변경되어야 하는 최소한의 Route Segment 리렌더될 수 있도록
프레임워크 내부적으로 Prefetching과 Caching을 진행한다.

Prefetching

Prefetching은 개발 단계가 아닌, 배포 단계에서만 유효하다. (이 또한 자명한데, prerender된 페이지 chunk를 준비하기 위해서는 빌드 과정이 필요하기 때문이다.)

사용자가 실제 요청하기 전에 페이지 로드를 위한 준비를 진행한다.
정적인 페이지는 HTML 형태로 로드를 준비하고,
동적인 페이지는 rendering에 필요한 chunk 단위로 준비한다.
(이는 React Server Component Payload 형태로 전달된다.)

prefetch는 페이지를 처음 로드하거나 유저의 뷰포트에 <Link> 태그가 드러났을 때 동작하며, 이외에도 router.prefetch()를 통해 Client-component에서 프로그램적인 명시적 prefetch 또한 가능하다.

prefetch는 실질적인 페이지 로딩 속도에는 영향을 미치지 않는다.
1. 페이지가 로드된 이후,
2. Client-side에서
3. HTTP Caching 방식으로
4. 백그라운드에서 비동기적으로 동작하기 때문이다.

<Link> 태그는 routing 방식에 따라 다른 prefetch 정책을 적용한다.

  1. 고정된 URL에 접근하는 static routing에 대해서는 기본적으로 접근 가능한 모든 경로를 모두 prefetch한다. (prefetch default: true)

  2. URL segment를 props로 전달받아 동적으로 페이지를 로드/생성하는 Dynamic Routing에 대해서는 Intersection Observer API를 통해 사용자가 접근 가능한 시점에 자동적으로 prefetch를 진행한다. (prefetch default: automatic)

    1. Dynamic Routing은 사용자가 입력한 URL에 대해 동적으로 동작하므로, Client-side에서 결정되는 것은 자명하다.
    2. Link 태그가 사용자 Viewport에 등장하면 prefetch를 시작하는데, 이 때 공유하는 layout과 fallback UI로 보여줄 loading file만 다운로드(prefetch)하고, loading 파일은 30초 간 in-memory 캐싱해두는 방식으로 동작한다. (따라서 웹 페이지에서의 링크의 이동이 가능하면 점진적으로 이루어지도록 구성하는 것이 바람직하다.)

물론 사용자의 Viewport에 Link 태그가 보일 때만 동작하긴 하지만,
어찌됐든 네트워크를 통한 리소스 다운로드를 유발하므로 이러한 동작은 상황에 따라 부담이 될 수도 있다.

가능하면 각 segment가 적절하게 code-split 될 수 있도록 컴포넌트를 구성하여, 로드할 prerendered page 또는 chunk가 충분히 작도록 유지해야 한다. 필요하다면 Link 태그의 prefetch 옵션을 false로 바꿔 기본 동작을 비활성화 할 수도 있다.

자세한 내용은 링크를 참고하자.

다음은 위 내용과 관련하여 참고할만한 내용들이다.
how-automatic-prefetching-works
instant-navigation-experiences
Save-Data
NavigationPreloadManager

이 밖에도 CachingPartial Rendering에 대한 이해가 있다면 좋다.

  1. Soft Navigation
    일반적으로 브라우저는 전체 페이지를 다시 로딩하는 방식(Hard Navigation)으로 동작하지만, VDOM을 활용한 React Reconciliation 기법을 통해 변화가 필요한 부분만 로드가 진행된다. 이는 앞서 언급한 Partial Rendering과 관련된 개념이다.

  2. Back and Forward Navigation
    기본적으로 Navigating 사이에 상태를 유지하고, 앞서 언급한 Caching을 활용하여 route segment를 재사용한다.


본론

Convention

  1. <Suspense> 태그: Data fetching이 이루어지는 범위를 정의하고, 그 지연 시간 중 표현할 컴포넌트를 fallback 속성으로 정의한다.
  2. loading 파일: fallback 속성이 없더라도, <Suspense> 범위를 렌더링 준비가 완료되기까지 파일 내용으로 대체한다.

Instant Loading States

loading 파일을 디렉토리에 생성하는 것으로
navigating하면서 data fetching 이전에 loading state를 즉각적으로 보여주는 fallback UI를 정의할 수 있다.


Streaming with Suspense

자세한 내용은 링크(#1 | #2)를 참고하는 편이 낫다.

기존 CSR 방식은 SEO에 어려움이 있었으며,
이에 SSR에 대한 필요가 대두됐다.
하지만 전통적인 SSR 방식은 성능적인 손해가 컸고,
이에 대한 대안으로 하이브리드 렌더링 방식을 지원하는 Next 프레임워크가 등장했다.

기존의 SSR 방식은 위와 같이 모든 과정이 순차적으로 진행되었다는 것이 성능적으로 매우 큰 손해를 유발했다는 것이 핵심이다.

이에 prerender 과정을 통해 우선적으로 non-interactive 페이지를 보여주고, data fetching이 필요한 부분이 완료될 때 로드하는 방식으로 최적화를 진행하여 성능을 개선할 수 있다. (앞서 소개한 React.Suspense 기능)

하지만 이 또한 data fetching에 소요되는 시간이 길면, 기존 SSR과 비슷한 문제가 발생한다.

Server-side에서 이루어지는 data fetching bottleneck을 극복하기 위해 분할된 작은 chunk(rendering 단위)로 점진적인 렌더링을 도입한다. 이는 Next가 내부적으로 지원하는 기능이므로, 특별히 옵션을 부여할 필요 없이 Suspense를 활용하면 된다.

핵심은 하나의 페이지(세그먼트)에 대한 code-split이 빌드 시점에 진행되고, 그 구성 단위가 개별적인 route segment와 Suspense 바운더리로 구분되는 chunk 단위로 나뉜다는 것이다.

이러한 과정을 통해 data fetching이 진행되지 않는 개별 segment가 먼저 렌더링되고, (이 과정은 기본적으로 prefetch를 통해 더 빠르게 수행된다)
data fetching이 필요한 chunk에 대해서는 onDemand로 진행하나, 로딩이 완료되는 시점마다 병렬적으로 Suspense에 상태가 반영된다는 것이다.

app/dashboard/page.tsx

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>
  )
}

Suspense를 사용하는 것으로 얻을 수 있는 이점은 다음과 같다.
1. Server Component에 대한 렌더링이 점진적으로 진행될 수 있도록 최적화한다.
2. React를 통해 interactive하게 만들 컴포넌트의 우선순위를 결정할 수 있다. 이는 중첩된 Suspense의 사용을 통해 구현할 수 있다.

SEO

data fetching을 진행하기 전에 generateMetadata()를 활용한 SEO를 진행하므로 head를 통해 의존성을 정의한 경우에도 렌더링 에러를 걱정할 필요는 없다.

다음 Google의 Mobile Friendly Test를 통해 Google 웹 크롤러에서 페이지가 드러나는 방식과 serialize된 HTML 형태를 볼 수 있다.

상태 코드

병렬적인 비동기적 data fetching request에 성공하면 상태 코드 200을 반환한다. 에러가 발생하는 경우에도 redirect 또는 notFound 함수를 통해 다른 chunk에 대한 영향 없이 streaming에 대한 독립된 관리를 수행할 수 있다.

다음 단계

Error Handling 기능을 활용하여
routing과 navigating 도중 에러가 발생했을 때의 정책을 정의하는 방법을 알아본다.

시리즈

Terminology
Defining Routes
Pages and Layouts
Linking and Navigating
Route Groups
Dynamic Routes
Loading UI and Streaming
Error Handling
Parallel Routes
Intercepting Routes
Route Handlers
Middleware
Project Organization
Internationalization

2개의 댓글

comment-user-thumbnail
2023년 10월 1일

프리패치가 상황에 따라 부담이 되므로 가능하면 세그먼트가 적절히 code split 될 수 있도록 컴포넌트를 구성하여야 한다고 했는데, 여기서 적절히 코드스플릿이 되도록 컴포넌트를 구성한다는게 어떤식으로 구성하는 것인가요? 링크된 페이지의 청크를 작게 만든다는 부분이 이해가 잘 안갑니다!

1개의 답글