이제 데이터베이스를 생성하고 시드했으므로 애플리케이션에 대한 데이터를 가져올 수 있는 다양한 방법에 대해 논의하고 대시보드 개요 페이지를 구축해 보겠습니다.
API는 애플리케이션 코드와 데이터베이스 사이의 중간 계층입니다. API를 사용할 수 있는 몇 가지 경우가 있습니다.
Next.js에서는 Route Handlers를 사용하여 API 엔드포인트를 생성할 수 있습니다.
풀스택 애플리케이션을 생성할 때 데이터베이스와 상호 작용하는 로직도 작성해야 합니다. Postgres 처럼 관계형 데이터베이스의 경우 SQL이나 Prisma 같은 ORM을 사용하여 이 작업을 수행할 수 있습니다.
데이터베이스 쿼리를 작성해야 하는 몇 가지 경우가 있습니다.
React Server 구성 요소에 대해 자세히 알아 보겠습니다.
기본적으로 Next.js 애플리케이션은 React Server Components를 사용합니다. 서버 구성 요소를 사용하여 데이터를 가져오는 것은 비교적 새로운 접근 방식이며 이를 사용하면 몇 가지 이점이 있습니다.
useEffect, useState나 데이터를 가져오는 라이브러리에 접근하지 않고도 async/await 구문을 사용할 수 있습니다.대시보드 프로젝트의 경우 Vercel Postgres SDK와 SQL을 사용하여 데이터베이스 쿼리를 작성합니다. SQL을 사용하는 데에는 몇 가지 이유가 있습니다.
이전에 SQL을 사용해 본 적이 없더라도 걱정하지 마세요. 우리가 쿼리를 제공해 드리고 있습니다.
/app/lib/data.ts로 이동하면 우리가 @vercel/postgres에서 sql 함수를 가져오는 것을 볼 수 있습니다. 이 함수를 사용하면 데이터베이스를 쿼리할 수 있습니다.
import { sql } from '@vercel/postgres';
// ...
sql을 모든 서버 구성 요소 내부에서 호출할 수 있습니다. 그러나 구성 요소를 더 쉽게 탐색할 수 있도록 모든 데이터 쿼리를 data.ts 파일에 보관했으며 이를 구성 요소로 가져올 수 있습니다.
참고: 6장에서 자체 데이터베이스 공급자를 사용한 경우 공급자와 함께 작동하려면 데이터베이스 쿼리를 업데이트해야 합니다.
/app/lib/data.ts에서 쿼리를 찾을 수 있습니다.
이제 데이터를 가져오는 다양한 방법을 이해했으므로 대시보드 개요 페이지에 대한 데이터를 가져오겠습니다. /app/dashboard/page.tsx로 이동하여 다음 코드를 붙여넣고 잠시 탐색해 보세요.
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>
);
}
위의 코드에서:
await를 사용할 수 있습니다.<Card>, <RevenueChart> 및 <LatestInvoices>. 현재는 애플리케이션 오류를 방지하기 위해 주석 처리되어 있습니다.<RevenueChart/>의 데이터 가져오기<RevenueChart/> 구성 요소에 대한 데이터를 가져오려면 data.ts에서 fetchRevenue 함수를 가져오고 구성 요소 내에서 호출하세요.
// /app/dashboard/page.tsx
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 { fetchRevenue } from '@/app/lib/data'; // fetchRevenue 불러오기
export default async function Page() {
const revenue = await fetchRevenue(); // 변수 선언
// ...
}
그런 다음 <RevenueChart/> 구성 요소의 주석 처리를 제거하고 구성 요소 파일(/app/ui/dashboard/revenue-chart.tsx)로 이동하여 그 안에 있는 코드의 주석 처리를 제거합니다. 로컬 호스트를 확인하면 revenue 데이터를 사용하는 차트를 볼 수 있습니다.

계속해서 더 많은 데이터 쿼리를 가져오겠습니다.
<LatestInvoices/>의 데이터 가져오기<LatestInvoices /> 구성 요소의 경우 날짜별로 정렬된 최신 5개의 송장을 가져와야 합니다.
JavaScript를 사용하여 모든 송장을 가져와서 정렬할 수 있습니다. 데이터가 작기 때문에 이는 문제가 되지 않지만 애플리케이션이 커짐에 따라 각 요청에 대해 전송되는 데이터의 양과 이를 정렬하는 데 필요한 JavaScript가 크게 늘어날 수 있습니다.
메모리 내에서 최신 송장을 정렬하는 대신 SQL 쿼리를 사용하여 마지막 5개의 송장만 가져올 수 있습니다. 예를 들어 다음은 data.ts 파일의 SQL 쿼리입니다.
// 날짜별로 정렬된 마지막 5개의 송장 가져오기
const data = await sql<LatestInvoiceRaw>`
SELECT invoices.amount, customers.name, customers.image_url, customers.email
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
ORDER BY invoices.date DESC
LIMIT 5`;
페이지에서 다음 fetchLatestInvoices 함수를 가져옵니다.
// /app/dashboard/page.tsx
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 { fetchRevenue, fetchLatestInvoices } from '@/app/lib/data'; // fetchLatestInvoices 불러오기
export default async function Page() {
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // 변수 선언
// ...
}
그런 다음 <LatestInvoices /> 구성요소의 주석 처리를 제거하십시오. 또한 /app/ui/dashboard/latest-invoices에 있는 구성 요소 <LatestInvoices /> 자체에서 관련 코드의 주석 처리를 제거해야 합니다.
로컬 호스트를 방문하면 데이터베이스에서 마지막 5개만 반환되는 것을 볼 수 있습니다. 이제 데이터베이스를 직접 쿼리하는 것의 이점을 확인하기 시작하셨기를 바랍니다.

<Card> 구성요소 에 대한 데이터 가져오기이제 <Card> 구성 요소에 대한 데이터를 가져올 차례입니다. 카드에는 다음 데이터가 표시됩니다.
다시 말하지만, 모든 송장과 고객을 가져오고 JavaScript를 사용하여 데이터를 조작하고 싶은 유혹을 받을 수도 있습니다. 예를 들어 Array.length를 사용하여 총 송장 수와 고객 수를 얻을 수 있습니다.
const totalInvoices = allInvoices.length;
const totalCustomers = allCustomers.length;
그러나 SQL을 사용하면 필요한 데이터만 가져올 수 있습니다. Array.length를 사용하는 것보다 약간 길지만 요청 중에 전송해야 하는 데이터가 적다는 것을 의미합니다. 이것은 SQL 대안입니다.
// /app/lib/data.ts
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
가져와야 하는 함수를 fetchCardData이라고 합니다. 함수에서 반환된 값을 재구성해야 합니다.
힌트:
data.ts 파일을 확인하세요.좋습니다. 이제 대시보드 개요 페이지에 대한 모든 데이터를 가져왔습니다. 귀하의 페이지는 다음과 같아야 합니다:

하지만... 알아두어야 할 두 가지 사항이 있습니다.
이번 장에서는 1번에 대해 논의하고, 다음 장에서는 2번에 대해 자세히 살펴보겠습니다.
"waterfall"는 이전 요청의 완료에 따라 달라지는 일련의 네트워크 요청을 나타냅니다. 데이터 가져오기의 경우 각 요청은 이전 요청이 데이터를 반환한 후에만 시작할 수 있습니다.

예를 들어, fetchLatestInvoices() 실행을 시작하려면 fetchRevenue()가 먼저 실행될 때까지 기다려야 합니다.
// /app/dashboard/page.tsx
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // fetchRevenue()가 끝날 때까지 기다리기
const {
numberOfInvoices,
numberOfCustomers,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData(); // fetchLatestInvoices()가 끝날 때까지 기다리기
이 패턴이 반드시 나쁜 것은 아닙니다. 다음 요청을 하기 전에 조건이 충족되기를 원하기 때문에 폭포수를 원하는 경우가 있을 수 있습니다. 예를 들어 사용자 ID와 프로필 정보를 먼저 가져오고 싶을 수 있습니다. ID가 있으면 친구 목록을 가져올 수 있습니다. 이 경우 각 요청은 이전 요청에서 반환된 데이터에 따라 달라집니다.
그러나 이 동작은 의도하지 않은 것일 수도 있으며 성능에 영향을 미칠 수도 있습니다.
폭포수를 방지하는 일반적인 방법은 모든 데이터 요청을 동시에 병렬로 시작하는 것입니다.
JavaScript에서는 Promise.all() 또는 Promise.allSettled()를 모든 promise를 동시에 시작하는 기능으로 사용할 수 있습니다. 예를 들어 data.ts에서, fetchCardData() 함수 안에 Promise.all() 함수를 사용하고 있습니다.
// /app/lib/data.js
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`;
// Promise.all() 사용
const data = await Promise.all([
invoiceCountPromise,
customerCountPromise,
invoiceStatusPromise,
]);
// ...
}
}
이 패턴을 사용하면 다음을 수행할 수 있습니다.
그러나 이 JavaScript 패턴에만 의존하면 한 가지 단점이 있습니다. 하나의 데이터 요청이 다른 모든 데이터 요청보다 느리면 어떻게 될까요?