[Next.js] 공식 문서만 보고 Next.js 익히기(3)

찐새·2023년 12월 3일
0

공식 문서만 보고

목록 보기
7/9
post-thumbnail

공식 문서 보기를 돌같이 하는 버릇을 고치자!

6. Fetching Data

Next.js는 라우트를 이용해 API 엔드포인트를 제공하여 서드 파티 프로그램이나 서버 없이도 데이터를 패칭할 수 있다. 스킵했지만, 앞선 장에서 Vercel이 제공하는 서비스를 이용해 postgres DB를 생성했는데, prisma같은 ORM을 통해 관계형 DB를 호출할 수 있다.

ORM이란?
Object Relational Mapping의 약자로, 구현한 객체와 관계형 DB의 불일치를 자동으로 매핑한 SQL문을 생성해 호환가능하게 해주는 기술이다.

데이터 패치를 하기에 앞서, Next.js는 기본적으로 React Server Component를 사용하는데, 몇 가지 이점을 알려준다.

  • 프로미스를 지원하여 useStateuseEffect, 데이터 패치 라이브러리, 추가 API 계층 없이 async/await을 사용하여 데이터를 가져올 수 있다.
  • 서버에서 실행되기 때문에 비용이 많이드는 로직은 서버에서 실행하고, 결과만 클라이언트로 전송할 수 있다.

6-1. Fetching data for the dashboard overview page

제공해준 dashboard 페이지의 코드를 보자.

import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';

export default async function Page() {
  return (
    <main>
      <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
        Dashboard
      </h1>
      <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
        {/* <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"
        /> */}
      </div>
      <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
        {/* <RevenueChart revenue={revenue}  /> */}
        {/* <LatestInvoices latestInvoices={latestInvoices} /> */}
      </div>
    </main>
  );
}

페이지 컴포넌트는 async function으로 되어 있다. 이는 await을 곧장 사용할 수 있음을 의미한다. 주석 처리된 컴포넌트들은 모두 데이터를 받는다. 데이터를 패치해 보자.

// ...
import { fetchRevenue } from '@/app/lib/data';

export default async function Page() {
  const revenue = await fetchRevenue();
  // ...
}

컴포넌트에서 await을 사용하는 게 매우 신기하다. ts 에러가 발생하지만 런타임에는 아무 지장없이 잘 실행된다.

여기서 두 가지 주의해야 할 사항이 있다고 한다.

  1. 데이터 요청이 의도치 않게 서로를 차단해 요청 폭포수를 만들고 있다.
  2. 다른 하나는 Next.js가 성능 개선을 위해 기본적으로 prerender하여 정적 렌더링을 하기 때문에 데이터 변화가 있어도 동적으로 반영되지 않는다는 점이다.

이 장에서는 1번을 살피고, 다음 장에서 2번을 살핀다.

6-2. What are request waterfalls?

waterfall이전 요청의 완료 여부에 따라 달라지는 일련의 네트워크 요청을 의미한다. 여기서는 앞선 데이터 패칭이 완료 되어야 다음 데이터 패칭이 이루어지는 것이다.

이전 패칭의 결과가 후행 패치에 영향을 미칠 경우에는 나쁘지 않은 패턴이다. 하지만 앞선 주의사항 대로 성능에 영향을 미칠 수 있다.

6-3. Parallel data fetching

여러 데이터 요청이 동시에 발생하는 경우 JS가 제공하는 Promise.all이나 Promise.allSettled를 사용하여 병행 처리할해 성능을 향상시킬 수 있다. 또한, 자바스크립트가 제공하는 함수를 사용하기 때문에 다른 프레임워크에서도 재사용 가능하다.

7. Static and Dynamic Rendering

이전 챕터에서 문제삼은 주의사항 2번을 해결하는 챕터이다. 정적 렌더링은 빌드나 재검증(revalidate) 중 데이터를 가져오고 렌더링하는 과정이다. 이 결과물을 CDN에 배포해 캐싱한다.

이러한 방식은 다음과 같은 이점이 있다.

  • 더 빠른 웹사이트 : 미리 렌더링된 콘텐츠를 캐싱하여 배포하므로 전 세계 사용자가 웹사이트에 더 빠르고 안정적으로 액세스할 수 있다.
  • 서버 부하 감소 : 콘텐츠가 캐시되므로 서버에서 각 사용자 요청에 대해 콘텐츠를 동적으로 생성할 필요가 없다.
  • SEO : 미리 렌더링된 콘텐츠는 페이지가 로드될 때 이미 콘텐츠를 사용할 수 있으므로 검색 엔진 크롤러가 색인을 생성하기가 더 쉽다. 이는 검색 엔진 순위 향상으로 이어질 수 있다.

따라서 정적 렌더링은 데이터 변화가 없거나 적은 블로그나 제품 페이지 등에 적합하다. 그러나 dashboard와 같이 데이터에 변화가 잦은 페이지에는 적합하지 않을 수 있다.

이와 반대되는 개념이 동적 렌더링(Dynamic Rendering)이다.

7-1. What is Dynamic Rendering?

동적 렌더링은 사용자가 페이지에 방문했을 때 렌더링하여 콘텐츠를 생성한다. 이점은 다음과 같다.

  • 실시간 데이터 : 애플리케이션에서 실시간 또는 자주 업데이트되는 데이터를 표시할 수 있다. 데이터가 자주 변경되는 애플리케이션에 이상적이다.
  • 사용자별 콘텐츠 : 대시보드나 사용자 프로필과 같은 개인화된 콘텐츠를 제공하고 사용자 상호 작용에 따라 데이터를 업데이트하는 것이 더 쉽다.
  • 요청 시간 정보 : 동적 렌더링을 사용하면 쿠키나 URL 검색 매개변수와 같이 요청 시점에만 알 수 있는 정보에 액세스할 수 있다.

7-2. Using Dynamic Rendering

데이터 패치 함수 초입에 unstable_noStore를 불러와 적용한다.

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

export async function fetchRevenue() {
  noStore();
  // ...fetch logic
}

export async function fetchLatestInvoices() {
  noStore();
  // ...fetch logic
}

export async function fetchCardData() {
  noStore();
  // ...fetch logic
}

export async function fetchFilteredInvoices(
  query: string,
  currentPage: number,
) {
  noStore();
  // ...fetch logic
}

export async function fetchInvoicesPages(query: string) {
  noStore();
  // ...fetch logic
}

export async function fetchFilteredCustomers(query: string) {
  noStore();
  // ...fetch logic
}

export async function fetchInvoiceById(query: string) {
  noStore();
  // ...fetch logic
}

unstable_noStore는 실험적인 API이므로 추후 변경될 수도 있다고 한다. 안정적인 API는 Route Segment Configexport const dynamic = "force-dynamic"를 사용한다.

export const dynamic = 'force-dynamic';

export default function MyComponent() {}

동적 렌더링이 가져오는 문제는 느리게 도착하는 데이터에 의해 앱의 성능이 결정된다는 점이다. 이를 해결하는 과정을 다음 챕터에서 안내한다.

8. Streaming

느린 데이터 가져오기 환경을 개선하는 방법을 알려주는 장이다.

8-1. What is streaming?

스트리밍(streaming)은 데이터를 '작은 조각(chunk)'로 분할하여 서버에서 준비되는 대로 클라이언트 측에 보내는 전송 방식을 말한다. 느린 데이터 요청으로 인한 앱 전체가 차단되는 것을 방지하고, 전체 데이터 패칭이 완료되지 않아도 일부 페이지를 조작할 수 있도록 한다.

리액트 컴포넌트는 하나의 청크로 간주될 수 있기 때문에 스트리밍을 적용하기에 좋다. 페이지에서는 loading.tsx을, 컴포넌트에서는 <Suspense>를 사용하여 스트리밍을 적용할 수 있다.

8-2. Using loading.tsx

페이지 전체 로딩을 적용하는 방법은 매우 간단하다. 라우트 경로에 loading.tsx를 추가한다.

// app/dashboard/loading.tsx
export default function Loading() {
  return <div>Loading...</div>;
}

의도적으로 데이터 패치 중 하나를 느리게 만들면 로딩 화면이 보인다.

제공한 스켈레톤으로 로딩을 교체했는데, 작은 버그가 하나 있다. dashboard 바로 아래에 loading을 생성한 탓에 하위 라우트인 dashboard/invoicesdashboard/customers에서도 로딩이 적용된다. dashboard에만 적용하려면 하위에 (overview) 폴더를 추가하고, page.tsxloading.tsx를 옮긴다.

🗂app/
  └─🗂dashboard/
    ├─layout.tsx
    ├─🗂(overview)
    │ ├─loading.tsx
    │ └─page.tsx
    ├─🗂invoices/
    └─🗂customers/

이렇게 경로 나누는 방식이 Route Groups이며, 괄호로 작성한 폴더를 경로에 포함시키지 않으면서 나눌 수 있다. 예를 들어, 여기서 사용한 loading.tsx(overview) 하위에 있는 page.tsx에만 적용된다.

8-3. Streaming a component

위의 방식이 전체 페이지 스트리밍에 해당한다면, <Suspense>는 데이터가 필요한 특정 컴포넌트만 지연 로딩하는 방식이다. 지연 로딩할 부분을 <Suspense>로 감싸고 지연되는 동안 보여줄 fallback을 추가한다.

dashboard에서 하나의 요청을 의도적으로 지연시켜 전체 페이지에 로딩이 발생했다. 해당 요청을 제거하고, 해당 컴포넌트를 <Suspense>로 감싼다.

import RevenueChart from '@/app/ui/dashboard/revenue-chart';
+ import { fetchLatestInvoices, fetchCardData } from '@/app/lib/data';  // remove fetchRevenue
import { Suspense } from 'react';
import { RevenueChartSkeleton } from '@/app/ui/skeletons';

export default async function Page() {
-  const revenue = await fetchRevenue // delete this line
  // ...

  return (
    <main>
      <h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
        Dashboard
      </h1>
      <div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
        {/* ...Cards */}
      </div>
      <div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
+        <Suspense fallback={<RevenueChartSkeleton />}>
+          <RevenueChart />
+        </Suspense>
        {/* ...*/}
      </div>
    </main>
  );
}

특정 컴포넌트의 요청이 끝날 때까지 전체 로딩하던 화면에서 특정 컴포넌트만 지연 로딩되는 화면으로 바뀌었다.

8-3. Deciding where to place your Suspense boundaries

Suspense의 경계는 원하는 사용자 경험, 콘텐츠 우선순위, 컴포넌트가 의존하는 데이터 패칭에 따라 달라진다. 정답은 없지만 일반적으로 데이터가 필요한 컴포넌트를 Suspense로 감싸는 게 낫고, 필요한 경우 전체 페이지를 스트리밍한다.

profile
프론트엔드 개발자가 되고 싶다

0개의 댓글