Next.js Rendering, Streaming

Minboy·2024년 3월 4일
1
post-thumbnail

Chapter 8

What is Static Rendering?

Static Rendering, 정적 렌더링을 통해 데이터 접근과 렌더링은 서버에서 빌드 타임, 즉 배포시에 일어나거나 데이터 재평가시에 발생한다. 해당 결과는 Content Delivery Network (CDN)에 분산되어 캐시될 수 있다.

7

유저가 앱에 접근하면, 캐시된 결과가 제공된다. 이러한 정적 렌더링에는 다음과 같은 장점들이 있다.

  • 빠른 웹사이트 - 사전 렌더링 된 컨텐츠를 캐시할 수 있고, 유저들은 웹사이트의 컨텐츠에 빠르고 확실하게 접근할 수 있다.
  • 서버 부하 감소 - 컨텐츠들이 캐시되므로, 서버는 모든 각각의 유저들의 요청마다 매번 동적으로 컨텐츠를 생성할 필요가 없다.
  • SEO - 사전 렌더링된 컨텐츠는 컨텐츠들이 사전 접근이 용이하기 때문에 검색 엔진 크롤러들이 색인을 생성하기에 더 쉽다. 이는 검색 엔진 순위 향상을 가져올 수 있다.

정적 렌더링은 블로그 게시글이나, 제품 페이지와 같은 데이터가 없거나, 유저들 간에 공유하는 데이터를 가진 UI에 매우 유용하다.

하지만 각각의 유저마다 다른 정보를 가진 개인화된 데이터나 자주 업데이트가 되는 유저 프로필, 대쉬보드 등과 같은 경우에는 좋은 선택이 아닐 수 있다. 이럴 때는 반대로 dynamic rendering, 동적 렌더링을 이용해야한다.

What is Dynamic Rendering?

동적 렌더링을 통해 컨텐츠는 서버에서 각각의 유저가 보낸 요청에 따라 리퀘스트 타임에 렌더링된다. (유저 방문 등) 동적 렌더링의 장점은 다음과 같다.

  • 실시간 데이터 - 동적 렌더링은 실시간 또는 자주 업데이트 된 데이터가 표시될 수 있게 한다. 이는 데이터가 자주 변경되는 경우에 적합하다.
  • 유저 개인 컨텐츠 - 대쉬보드나 유저 프로필처럼 개인화된 데이터를 제공하고 상호작용하기에 적합하다.
  • 리퀘스트 타임 정보 - 동적 렌더링은 리퀘스트 타임에만 알 수 있는 정보에 접근할 수 있게 해준다. 예를 들면 쿠키나 URL 서치 파라미터와 같은 정보들은 리퀘스트 타임에만 알 수 있고, 동적 렌더링을 이용해야한다.

Making the dashboard dynamic

@vercel/postgres 는 캐시 행동을 디폴트로 정의하지 않는다. 따라서 정적이든, 동적이든 원하는 행동을 설정하면 된다.

Next.js API 중 unstable_noStore 를 이용해 동적 렌더링을 설정할 수 있다.

import { unstable_noStore as noStore } from 'next/cache';

export async function fetchRevenue() {
  // Add noStore() here to prevent the response from being cached.
  // This is equivalent to in fetch(..., {cache: 'no-store'}).
  noStore();

  // ...
}

위와 같이 사용하면 된다.

Simulating a Slow Data Fetch

Promise와 setTimeout을 통해 느린 데이터 요청을 시뮬레이션해보자. 데이터에 접근하는 동안 전체 페이지가 블로킹 되는 결과를 확인할 수 있다.

동적 렌더링을 이용하면 우리의 앱은 가장 느린 데이터 페칭만큼만 빠를 수 있다!

Chapter 9

이전 챕터에서, 대쉬보드 페이지를 동적으로 만들었지만 느린 데이터 페칭이 전체 앱의 성능에 영향을 미칠 수 있다는 것을 확인했다. 느린 데이터 요청에 대해 유저 경험을 향상시키는 방법을 알아보자.

What is streaming?

스트리밍은 한 라우트를 더 작은 여러개의 “chunks” 들로 쪼개어 서버에서 준비되는 대로 점진적으로 불러오는 데이터 교환 기술이다.

8

스트리밍을 통해 느린 데이터 요청이 전체 페이지를 블로킹하는 것을 방지할 수 있다.

9

리액트의 컴포넌트 모델은 스트리밍과 잘 맞는데, 각각의 컴포넌트를 chunk로 생각할 수 있기 때문이다.

Next.js에서 스트리밍을 구현하는 방법은 두가지가 존재한다.

  1. 페이지 레벨에서, loading.tsx 파일을 통해 구현
  2. 구체적인 컴포넌트 레벨에서, <Suspense> 컴포넌트를 통해 구현

Streaming a whole page with loading.tsx

/app/dashboard 폴더에 loading.tsx 파일을 새로 생성하자.

export default function Loading() {
  return <div>Loading...</div>;
}

10

  1. loading.tsx 파일은 Suspense를 이용해 만들어진 특별한 Next.js 파일이다. 이를 통해 페이지가 컨텐츠를 로딩하는 동안 보여줄 fallback UI를 정의할 수 있다.
  2. <Sidebar> 는 정적이므로 즉시 보여진다. 유저는 동적 컨텐츠들이 로딩되는 동안에도 <Sidebar> 와 상호작용할 수 있다.
  3. 유저는 다른 경로로 이동하기 전에 페이지 로딩이 완료되는 것을 기다릴 필요가 없고, 이것을 interruptable navigation이라고 한다.

더 좋은 유저 경험을 위해 “Loading…” 텍스트가 아닌 스켈레톤 UI를 적용하면 좋다.

Fixing the loading skeleton bug with route groups

현재 loading.tsx 파일의 레벨이 /invoices/page.tsx/customers/page.tsx 파일보다 더 높기 때문에, 두 페이지에도 스켈레톤 UI가 적용되고 있는 버그가 존재한다.

이를 Route Groups를 통해 해결할 수 있다. dashboard 폴더에 /(overview) 폴더를 만들어 loading.tsxpage.tsx 를 이동시키자.

11

이제 loading.tsx 파일은 대쉬보드 오버뷰 페이지에만 적용될 것이다.

Route groups는 파일들을 URL path 구조에 영향을 끼치지 않으면서도 논리적인 그룹으로 분리할 수 있게 해준다. () 와 같이 소괄호로 감싼 폴더명으로 새로운 폴더를 만들면, 해당 이름은 URL path에 포함되지 않는다. 따라서 /dashboard/(overview)/page.tsx 는 그냥 /dashboard 가 되는 것이다.

여기서는 loading.tsx 파일이 대쉬보드 오버뷰 페이지에만 적용되게 하기 위해 라우트 그룹을 사용했지만, 더 큰 앱에서는 앱을 섹션별로 분류하기 위해 사용할 수도 있다.

Streaming a component

지금까지는, 전체 페이지를 스트리밍했지만 이제 리액트의 Suspense를 활용해 더 잘게 쪼개어 보자.

Suspense는 특정 조건, 예를 들면 데이터가 로딩될 때까지 렌더링을 미룰 수 있게 해준다. 동적 컴포넌트를 Suspense로 감싸고 fallback 컴포넌트를 전달해주면 동적 컴포넌트가 로드될 때까지 fallback 컴포넌트가 보여질 것이다.

일전의 fetchRevenue() 와 같이 전체 페이지의 성능을 느리게하는 느린 데이터 요청이 전체 페이지를 블로킹하는 대신에, Suspense를 이용하여 해당 컴포넌트의 렌더링만 미루고 페이지의 나머지 UI들은 보일 수 있도록 만들 수 있다.

그렇게 하기 위해, 데이터 페칭을 해당 컴포넌트 내부로 옮겨주어야한다.

...
<Suspense fallback={<RevenueChartSkeleton />}>
  <RevenueChart />
</Suspense>
...

이렇게 Suspense로 감싸주고 fallback에 fallback UI를 전달해주면 된다.

12

이제 느린 데이터 요청을 하는 해당 부분만 로딩이 되고 나머지 부분들은 즉시 보여지는 것을 확인할 수 있다.

Grouping component

네 개의 카드 컴포넌트로 이루어진 부분도 각각의 Card 컴포넌트에서 해당하는 데이터들을 페칭하게하고 Suspense를 통해 fallback UI를 보여줄 수 있을 것이다. 하지만 이렇게 너무 세분화하는 것은 오히려 유저에게 정신없어보이는 안좋은 경험을 줄 수 있다.

따라서 wrapper 컴포넌트로 카드들을 감싸 그룹화를 통해 한 번에 처리하는 것이 좋을 수 있다.

export default async function CardWrapper() {
  const {
    numberOfCustomers,
    numberOfInvoices,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
  return (
    <>
      <Card title="Collected" value={totalPaidInvoices} type="collected" />
      <Card title="Pending" value={totalPendingInvoices} type="pending" />
      <Card title="Total Invoices" value={numberOfInvoices} type="invoices" />
      <Card
        title="Total Customers"
        value={numberOfCustomers}
        type="customers"
      />
    </>
  );
}
...
<Suspense fallback={<CardsSkeleton />}>
  <CardWrapper />
</Suspense>
...

이렇게 wrapper 컴포넌트를 통해 그룹화를 하면 네개의 카드가 모두 데이터 페칭을 완료했을 때 동시에 카드들이 유저에게 보여지도록 할 수 있다.

Deciding where to place your Suspense boundaries

Suspense 바운더리를 어디에 위치시킬 것인지는 다음과 같은 요소들에 의해 정해진다.

  1. 페이지가 스트리밍 될 때 사용자가 어떻게 경험하게 하고 싶어하는지
  2. 어떤 컨텐츠가 더 주요한지
  3. 컴포넌트가 데이터 페칭에 의존하고 있는 경우

정답은 정해진 것이 없다.

  • loading.tsx 를 통해 전체 페이지를 스트리밍 할 수도 있다. 하지만 이는 한 컴포넌트가 긴 데이터 요청을 하는 경우 느린 로딩을 가져올 수도 있다.
  • 모든 각각의 컴포넌트를 스트리밍 할 수도 있다. 하지만 이는 UI가 전부 따로노는 듯한 경험을 가져올 수 있다.
  • 페이지를 섹션들로 구분해 스트리밍을 할 수도 있다. 하지만 이는 wrapper 컴포넌트를 만들어야한다.

Suspense 바운더리를 어디에 위치시킬 지는 어떤 앱인지에 따라 다 다르다. 일반적으로 데이터 페칭은 컴포넌트로 내려보내고 Suspense로 감싸주는 것이 좋은 관습이다. 하지만 섹션별로, 또는 페이지별로 스트리밍하는 것은 틀린 것이 아니다.

많은 경험을 해보는 것이 중요하다.

profile
🐧

0개의 댓글

관련 채용 정보