Next.js data fetching

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

Chapter 6

해당 챕터에서는 Github 레포지토리를 생성하고, Vercel과 연동하여 배포, Postgres database를 생성하여 연결을 진행한다. /scripts 폴더에 존재하는 seed.js 파일의 스크립트를 통해 생성한 데이터베이스에 테이블들을 만들고, /app/lib 폴더의 placeholder-data.js 에 존재하는 목업 데이터들을 넣는 것 까지 진행한다.

Vercel을 이번기회에 처음 적용해보게되었는데, 굉장히 간편하고 빠르게 사용할 수 있는 것 같다. DB 생성과 연결까지 간단하게 처리할 수 있으니 혼자 프로젝트 진행할 때 써먹어도 참 좋을 것 같다. 👍

Chapter 7

Using Server Components to fetch data

Next.js에서는 데이터 페칭을 위해 디폴트로 React Server Components를 이용한다. 서버 컴포넌트를 활용해 데이터에 접근하는 것은 다음과 같은 장점이있다.

  • 서버 컴포넌트는 promise를 지원하기 때문에, 데이터 페칭과 같은 비동기 작업에 간단한 해결책이다. useEffect, useState 와 같은 훅이나 데이터 페칭 라이브러리 없이 async/await 만으로도 데이터 페칭을 해결할 수 있다.
  • 서버 컴포넌트는 서버에서 실행되기 때문에,데이터 접근, 로직과 같은 비싼 작업을 서버에서 처리하고 결과만 클라이언트에 보낼 수 있다.
  • 상기한대로 서버 컴포넌트는 서버에서 실행되기 때문에 추가적인 API 레이어 없이도 데이터베이스에 직접 쿼리를 보낼 수 있다.

Using SQL

해당 프로젝트에서는 Vercel Postgres SDK와 SQL을 이용한 데이터 베이스 쿼리를 작성한다. SQL을 이용하는 이유는 다음과 같다.

  • SQL은 RDB에 쿼리를 보내는 산업 표준이다. (ORM도 내부적으로 SQL을 생성한다.)
  • SQL에 대한 기본적인 이해는 RDB의 기반을 이해하고, 다른 도구들을 사용할 수 있는 지식을 제공한다.
  • SQL은 특정 데이터에 접근하고 조작하는데 다재다능하다.
  • Vercel Postgres SDK는 SQL injection에 대한 보호를 제공한다.

/app/lib/data.ts 에서 데이터베이스에 쿼리를 보내는 모습을 확인할 수 있다.

import { sql } from '@vercel/postgres';

...
	const data = await sql<Revenue>`SELECT * FROM revenue`;
...

이런식으로 데이터베이스에 쿼리를 보낼 수 있다.

Fetching data for the dashboard overview page

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';
import { fetchCardData, fetchLatestInvoices, fetchRevenue } from '../lib/data';

export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  const {
    numberOfCustomers,
    numberOfInvoices,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
  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>
  );
}

위 코드를 보면

  • Page 컴포넌트는 async 컴포넌트이다. 이를 통해 데이터 페칭 시 await 을 이용할 수 있다.
  • 데이터를 필요로하는 <Card>, <RevenueChart>, <LatestInvoices> 컴포넌트들이 있다.

해당 컴포넌트들에 필요한 데이터를 페칭해오기 위해 사용한 함수들을 확인해보면 위에서 말한 sql을 이용해 데이터베이스에 쿼리를 날려 데이터를 가져오는 모습을 확인할 수 있다.

하지만 짚고 넘어가야할 문제가 두 가지 존재한다.

  1. 데이터 요청은 의도적이지 않게 서로를 blocking 할 수 있다. 이를 request waterfall이라고 한다.
  2. 디폴트로, Next.js는 성능 향상을 위해 라우트들을 사전 렌더링하고, 이를 Static Rendering이라고 한다. 이때문에 만약 데이터가 바뀌어도 즉각 반영되지 않을 수 있다.

하나씩 짚어보자.

What are request waterfalls?

“waterfall” (폭포)는 일련의 네트워크 요청이 다른 요청의 완료에 의존하고 있는 상태를 말한다. 데이터 접근의 경우에, 각각의 요청이 이전 요청들이 끝나야지만 시작되는 경우를 말한다.

6

const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // wait for fetchRevenue() to finish
const {
  numberOfInvoices,
  numberOfCustomers,
  totalPaidInvoices,
  totalPendingInvoices,
} = await fetchCardData(); // wait for fetchLatestInvoices() to finish

예를들어 위의 경우에 fetchLatestInvoices() 함수가 실행되기 위해서는 fetchRevenue() 함수가 끝나야만 한다.

이러한 패턴은 필연적으로 좋지 않다. 다음 요청을 보내기 전에 특정 조건을 만족시켜야함을 확인해야한다거나 하는 경우에는 이러한 “waterfall”이 필요할 수도 있다. 예를 들어 유저의 ID와 프로필 정보에 접근할 때, ID를 먼저 받고 그 뒤에 해당 ID를 통해 요청을 보내야 하면 이전 요청인 ID 요청을 끝낸 뒤에 프로필 정보 요청을 보내야한다.

하지만 이러한 행동은 때때로 비의도적이고 성능에 영향을 끼칠 수도 있다.

Parallel data fetching

이러한 waterfall을 해결하기 위한 흔한 방법으로는, 모든 데이터 요청을 병렬적으로 동시에 보내는 방법이 있다.

자바스크립트에서는 Promise.all() 또는 Promise.allSettled() 함수를 통해 모든 프로메스를 시작할 수 있다. 예를들어 data.tsfetchCardDatat() 함수를 확인해보면

export async function fetchCardData() {
  try {
    const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
    const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
    const invoiceStatusPromise = sql`SELECT
         SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
         SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
         FROM invoices`;

    const data = await Promise.all([
      invoiceCountPromise,
      customerCountPromise,
      invoiceStatusPromise,
    ]);
    // ...
  }
}

위와 같이 Promise.all() 함수를 이용하고 있다.

이러한 패턴을 이용함으로써

  • 모든 데이터 접근을 동시에 시작하여 성능 향상을 얻을 수 있다.
  • 네이티브 자바스크립트 패턴이므로 어떤 라이브러리나 프레임워크에서도 적용할 수 있다.

하지만, 이 방법 또한 한 가지 단점이 존재한다. 만약 한 데이터 요청이 다른 모든 요청들에 비해 매우 느리다면 어떻게 될까?

profile
🐧

0개의 댓글

관련 채용 정보