ReactNextCentral

데이터 생성과 가져오기

Published on
데이터를 생성, 저장, 조회하는 다양한 방법을 배웁니다.
Table of Contents

6장: 데이터베이스 설정하기

대시보드 작업을 계속하기 전에 데이터가 필요합니다. 이 장에서는 @vercel/postgres를 사용하여 PostgreSQL 데이터베이스를 설정합니다. PostgreSQL에 이미 익숙하고 자신의 제공자를 사용하고 싶다면 이 장을 건너뛰고 직접 설정할 수 있습니다. 그렇지 않다면 계속 진행합시다!

이 장에서 다룰 주제는 다음과 같습니다.

  • 프로젝트를 GitHub에 푸시하기
  • Vercel 계정을 설정하고 GitHub 리포지토리를 연결하여 즉각적인 프리뷰와 배포를 설정하기
  • Postgres 데이터베이스를 생성하고 연결하기
  • 초기 데이터로 데이터베이스를 씨딩하기

GitHub 리포지토리 생성하기

시작하기 전에, 아직 하지 않았다면 리포지토리를 GitHub에 푸시합시다. 이를 통해 데이터베이스를 설정하고 배포하기가 더 쉬워집니다.

리포지토리를 설정하는 데 도움이 필요하다면 GitHub 가이드를 참조하세요.

알아두기:

  • GitLab이나 Bitbucket 같은 다른 Git 제공자를 사용할 수도 있습니다.
  • GitHub에 익숙하지 않다면 GitHub 데스크탑 앱을 사용하는 것이 개발 워크플로우를 단순화하는 데 도움이 됩니다.

Vercel 계정 생성하기

vercel.com/signup에 방문하여 계정을 생성하세요. 무료 "hobby" 계획을 선택하세요. Continue with GitHub를 선택하여 GitHub과 Vercel 계정을 연결하세요.

프로젝트 연결 및 배포하기

다음 화면에서 방금 생성한 GitHub 리포지토리를 선택하고 import를 클릭하세요.

Vercel 대시보드 스크린샷으로 사용자의 GitHub 리포지토리 목록이 있는 프로젝트 가져오기 화면을 보여줌 프로젝트에 이름을 지정하고 Deploy를 클릭하세요. 프로젝트 이름 필드와 배포 버튼이 있는 배포 화면

축하합니다! 🎉 프로젝트가 이제 배포되었습니다. 프로젝트가 배포

GitHub 리포지토리를 연결함으로써 main 브랜치에 변경 사항을 푸시할 때마다 Vercel은 자동으로 애플리케이션을 재배포하며, 아무런 설정이 필요 없습니다. 풀 리퀘스트를 열 때 즉각적인 프리뷰를 가질 수 있어 배포 오류를 조기에 발견하고 프로젝트의 미리보기를 팀원들과 공유하여 피드백을 받을 수 있습니다.

Postgres 데이터베이스 생성하기

다음으로 데이터베이스를 설정하려면 Continue to Dashboard를 클릭하고 프로젝트 대시보드에서 Storage 탭을 선택하세요. Connect Store → Create New → Postgres → Continue를 선택하세요.

스토어 연결 화면으로 Postgres 옵션과 KV, Blob, Edge Config 옵션이 함께 보임 약관을 수락하고 데이터베이스에 이름을 지정하며 데이터베이스 지역을 한국과 가장 가까운 **Singapore (sin1)**로 설정하세요. 데이터베이스를 애플리케이션 코드와 같은 지역이나 가까운 지역에 배치하면 데이터 요청에 대한 지연 시간을 줄일 수 있습니다.

데이터베이스 이름과 지역을 보여주는 데이터베이스 생성 모달

알아두기: 데이터베이스 지역은 초기화된 후에는 변경할 수 없습니다. 다른 지역을 사용하고 싶다면 데이터베이스를 생성하기 전에 설정해야 합니다.

연결된 후 .env.local 탭으로 이동하여 secret 표시를 클릭하고 스니펫을 복사하세요. 복사하기 전에 비밀을 드러내야 합니다.

.env.local 탭으로 숨겨진 데이터베이스 비밀을 보여줌

코드 편집기로 이동하여 .env.example 파일의 이름을 .env로 변경하세요. Vercel에서 복사한 내용을 붙여넣으세요.

중요. .gitignore 파일로 이동하여 .env가 무시된 파일에 포함되어 있는지 확인하세요. GitHub에 푸시할 때 데이터베이스 비밀이 노출되지 않도록 하기 위함입니다.

마지막으로 터미널에서 npm i @vercel/postgres를 실행하여 Vercel Postgres SDK를 설치하세요.

데이터베이스에 초기 데이터 넣기

이제 데이터베이스가 생성되었으니 초기 데이터로 채워봅시다. 이렇게 하면 대시보드를 구축하면서 작업할 데이터가 생깁니다.

프로젝트의 /scripts 폴더에 seed.js라는 파일이 있습니다. 이 스크립트에는 invoices, customers, user, revenue 테이블을 생성하고 씨딩하는 지침이 포함되어 있습니다.

코드가 하는 모든 일을 이해하지 못해도 걱정하지 마세요. 하지만 개요를 드리자면 스크립트는 SQL을 사용하여 테이블을 생성하고, 생성된 후에 placeholder-data.js 파일의 데이터로 채웁니다.

다음으로 package.json 파일에 다음 줄을 스크립트에 추가하세요.

/package.json
"scripts": {
  "build": "next build",
  "dev": "next dev",
  "start": "next start",
  "seed": "node -r dotenv/config ./scripts/seed.js"
},

이것은 seed.js를 실행할 명령입니다.

이제 npm run seed를 실행하세요. 터미널에서 스크립트가 실행되고 있음을 알려주는 console.log 메시지가 표시됩니다.

문제 해결
  • .env 파일에 복사하기 전에 데이터베이스 secret을 드러내세요.
  • 스크립트는 [bcrypt](https://www.npmjs.com/package/bcryptjs를 사용하여 사용자의 비밀번호를 해시합니다. 만약 bcrypt가 환경과 호환되지 않는다면 스크립트를 bcryptjs로 업데이트할 수 있습니다.
  • 데이터베이스 씨딩 중 문제가 발생하고 스크립트를 다시 실행하고 싶다면 데이터베이스 쿼리 인터페이스에서 DROP TABLE tablename을 실행하여 기존 테이블을 삭제할 수 있습니다. 하지만 이 명령은 테이블과 모든 데이터를 삭제하므로 주의하세요. 예제 앱에서는 자리 표시자 데이터로 작업하기 때문에 이 명령을 실행하는 것이 괜찮지만, 배포 앱에서는 이 명령을 실행하지 않아야 합니다.
  • Vercel Postgres 데이터베이스를 씨딩하는 동안 문제가 지속되면 GitHub에서 토론을 열어주세요.

데이터베이스 탐색하기

데이터베이스가 어떻게 생겼는지 살펴봅시다. Vercel로 돌아가 사이드네비게이션에서 Data를 클릭하세요.

이 섹션에서는 users, customers, invoices, revenue의 네 개 새 테이블을 찾을 수 있습니다.

데이터베이스 화면으로 users, customers, invoices, revenue의 네 개 테이블을 보여주는 드롭다운 목록 각 테이블을 선택하면 레코드를 볼 수 있고 placeholder-data.js 파일의 데이터와 일치하는지 확인할 수 있습니다.

쿼리 실행하기

데이터베이스와 상호 작용하기 위해 "쿼리" 탭으로 전환할 수 있습니다. 이 섹션은 표준 SQL 명령어를 지원합니다. 예를 들어, DROP TABLE customers를 입력하면 "customers" 테이블과 모든 데이터가 삭제됩니다 - 그러니 주의하세요!

첫 번째 데이터베이스 쿼리를 실행해 봅시다. Vercel 인터페이스에 다음 SQL 코드를 붙여넣고 실행하세요.

SELECT invoices.amount, customers.name
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
WHERE invoices.amount = 666;

7장: 데이터 가져오기

이제 데이터베이스를 생성하고 씨딩했으니 애플리케이션에 데이터를 가져오는 다양한 방법을 알아보고 대시보드 개요 페이지를 구축해 봅시다.

이 장에서 다룰 주제는 다음과 같습니다.

  • 데이터 가져오기 접근 방법에 대해 알아보기: API, ORM, SQL 등
  • 서버 컴포넌트가 백엔드 자원에 더 안전하게 접근하는 데 어떻게 도움이 되는지 알아보기
  • 네트워크 워터폴이 무엇인지 알아보기
  • 자바스크립트 패턴을 사용한 병렬 데이터 가져오기 구현 방법

데이터 가져오기 방법 선택하기

API 레이어

API는 애플리케이션 코드와 데이터베이스 사이의 중간 계층입니다. API를 사용해야 하는 몇 가지 경우는 다음과 같습니다.

  • API를 제공하는 타사 서비스를 사용하는 경우
  • 클라이언트에서 데이터를 가져오는 경우, 데이터베이스 secret을 클라이언트에 노출하지 않기 위해 서버에서 실행되는 API 레이어를 갖고 싶을 것입니다

Next.js에서는 라우트 핸들러를 사용하여 API 엔드포인트를 생성할 수 있습니다.

데이터베이스 쿼리

풀스택 애플리케이션을 만들 때 데이터베이스와 상호 작용하는 로직을 작성해야 합니다. Postgres와 같은 관계형 데이터베이스의 경우, SQL이나 Prisma 같은 ORM을 사용할 수 있습니다.

데이터베이스 쿼리를 작성해야 하는 몇 가지 경우는 다음과 같습니다.

  • API 엔드포인트를 생성할 때 데이터베이스와 상호 작용하는 로직을 작성해야 합니다.
  • 리액트 서버 컴포넌트(서버에서 데이터 가져오기)를 사용하는 경우, 추가적인 API 레이어 없이 데이터베이스를 직접 쿼리할 수 있으며 클라이언트에 데이터베이스 비밀을 노출시키지 않습니다.

서버 컴포넌트를 사용한 데이터 가져오기에 대해 더 알아보겠습니다.

서버 컴포넌트를 사용하여 데이터 가져오기

기본적으로 Next.js 애플리케이션은 리액트 서버 컴포넌트를 사용합니다. 서버 컴포넌트를 사용한 데이터 가져오기는 비교적 새로운 접근 방법이며 사용하는 데 몇 가지 이점이 있습니다.

  • 서버 컴포넌트는 프로미스(async/await)를 지원하므로 useEffect, useState 또는 데이터 가져오기 라이브러리를 사용하지 않고도 비동기 작업에 대한 간단한 솔루션을 제공합니다.
  • 서버 컴포넌트는 서버에서 실행되므로 비용이 많이 드는 데이터 가져오기 및 로직을 서버에서 처리하고 결과만 클라이언트로 전송할 수 있습니다.
  • 앞서 언급했듯이 서버 컴포넌트는 서버에서 실행되므로 추가적인 API 레이어 없이 데이터베이스를 직접 쿼리할 수 있습니다.

SQL 사용하기

대시보드 프로젝트의 경우, Vercel Postgres SDK와 SQL을 사용하여 데이터베이스 쿼리를 작성할 것입니다. SQL을 사용하는 몇 가지 이유는 다음과 같습니다.

  • SQL은 관계형 데이터베이스를 쿼리하는 업계 표준입니다(예: ORM은 내부적으로 SQL을 생성합니다).
  • SQL의 기본을 이해하면 관계형 데이터베이스의 기본 원리를 이해할 수 있어 다른 도구에 대한 지식을 적용할 수 있습니다.
  • SQL은 다양하게 데이터를 가져오고 조작할 수 있게 합니다.
  • Vercel Postgres SDK는 SQL injections에 대한 보호를 제공합니다.

SQL을 사용해본 적이 없다면 걱정하지 마세요. 쿼리를 제공했습니다.

/app/lib/data.ts로 이동하면 @vercel/postgres에서 sql 함수를 가져오는 것을 볼 수 있습니다. 이 함수를 사용하면 데이터베이스를 쿼리할 수 있습니다.

/app/lib/data.ts
import { sql } from '@vercel/postgres';
 
// ...

서버 컴포넌트 내에서 sql을 호출할 수 있습니다. 하지만 컴포넌트를 더 쉽게 탐색할 수 있도록 모든 데이터 쿼리를 data.ts 파일에 유지하고 컴포넌트로 가져올 수 있습니다.

참고: 6장에서 자신의 데이터베이스 제공자를 사용했다면 /app/lib/data.ts에 있는 쿼리를 제공자와 호환되도록 업데이트해야 합니다.

대시보드 개요 페이지를 위한 데이터 가져오기

이제 데이터를 가져오는 다양한 방법을 이해했으니 대시보드 개요 페이지에 데이터를 가져와 봅시다. /app/dashboard/page.tsx로 이동하여 다음 코드를 붙여넣고 탐색해 보세요.

/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`}>
        대시보드
      </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) 컴포넌트입니다. 이를 통해 await를 사용하여 데이터를 가져올 수 있습니다.
  • 데이터를 받는 3개의 컴포넌트가 있습니다. <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';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  // ...
}

그런 다음, <RevenueChart/> 컴포넌트의 주석을 해제하고, 컴포넌트 파일(/app/ui/dashboard/revenue-chart.tsx)로 이동하여 내부의 코드 주석을 해제하세요. 로컬호스트에서 확인하면, 수익 데이터를 사용하는 차트를 볼 수 있습니다.

지난 12개월 동안의 총 수익을 보여주는 수익 차트 더 많은 데이터 쿼리를 가져와 보겠습니다!

<LatestInvoices/>를 위한 데이터 가져오기

<LatestInvoices /> 컴포넌트에는 날짜 순으로 정렬된 최근 5개의 청구서 데이터가 필요합니다.

모든 청구서를 가져와서 자바스크립트를 사용하여 정렬할 수 있습니다. 우리의 데이터가 작기 때문에 문제가 되지 않지만, 애플리케이션이 커지면 각 요청마다 전송되는 데이터 양과 이를 정렬하는 데 필요한 자바스크립트 양이 크게 증가할 수 있습니다.

메모리 내에서 최근 청구서를 정렬하는 대신, SQL 쿼리를 사용하여 최근 5개의 청구서만 가져올 수 있습니다. 예를 들어, data.ts 파일에서 이 SQL 쿼리를 사용합니다.

/app/lib/data.ts
// 날짜 순으로 최근 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';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  // ...
}

그런 다음, <LatestInvoices /> 컴포넌트의 주석을 해제합니다. /app/ui/dashboard/latest-invoices에 위치한 <LatestInvoices /> 컴포넌트 자체 내부의 관련 코드도 주석 해제해야 합니다.

로컬호스트를 방문하면 데이터베이스에서 최근 5개만 반환되는 것을 볼 수 있습니다. 데이터베이스를 직접 쿼리하는 이점을 보기 시작했기를 바랍니다!

수익 차트와 함께 표시되는 최근 청구서 컴포넌트

실습: `<Card>` 컴포넌트를 위한 데이터 가져오기

이제 <Card> 컴포넌트를 위한 데이터를 가져올 차례입니다. 카드는 다음 데이터를 표시합니다.

  • 수금된 청구서의 총액
  • 대기 중인 청구서의 총액
  • 청구서의 총 개수
  • 고객의 총 수

다시 한번, 모든 청구서와 고객을 가져와 자바스크립트를 사용하여 데이터를 조작하고 싶을 수 있습니다. 예를 들어, 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. 데이터 요청이 서로를 차단하여 request waterfall을 만듭니다.
  2. 기본적으로 Next.js는 성능을 향상시키기 위해 라우트를 정적으로 렌더링합니다. 이것을 정적 렌더(Static Rendering)이라고 합니다. 따라서 데이터가 변경되더라도 대시보드에 반영되지 않습니다.

Request waterfall이 무엇인가요?

"Waterfall"은 이전 요청의 완료에 의존하는 네트워크 요청 순서를 가리킵니다. 데이터 가져오기의 경우, 각 요청은 이전 요청이 데이터를 반환할 때까지만 시작할 수 있습니다. Request waterfall 예를 들어, fetchRevenue()가 실행을 완료하기 전까지는 fetchLatestInvoices()가 실행을 시작할 수 없고, 그 다음 요청도 마찬가지로 이전 요청이 완료될 때까지 기다려야 합니다.

이 패턴이 반드시 나쁜 것은 아닙니다. 다음 요청을 하기 전에 조건이 충족되길 원하는 경우에는 Waterfall이 필요할 수 있습니다. 예를 들어, 사용자의 ID와 프로필 정보를 먼저 가져온 후에 그 ID를 사용하여 그들의 친구 목록을 가져오고 싶을 수 있습니다. 이 경우, 각 요청은 이전 요청에서 반환된 데이터에 의존합니다.

그러나 이런 행동은 의도치 않게 발생하고 성능에 영향을 줄 수도 있습니다.

병렬 데이터 가져오기

Waterfall을 피하는 일반적인 방법은 모든 데이터 요청을 동시에 - 병렬로 - 시작하는 것입니다.

자바스크립트에서는 Promise.all() 또는 Promise.allSettled() 함수를 사용하여 모든 프로미스를 동시에 시작할 수 있습니다. 예를 들어, data.ts에서 fetchCardData() 함수를 사용할 때 Promise.all()을 사용합니다.

이 패턴을 사용하면 다음과 같은 이점을 얻을 수 있습니다.

  • 모든 데이터 가져오기를 동시에 시작하여 성능 향상을 이끌어낼 수 있습니다.
  • 어떤 라이브러리나 프레임워크에도 적용할 수 있는 네이티브 자바스크립트 패턴을 사용할 수 있습니다.

그러나 이 자바스크립트 패턴에만 의존하는 것의 단점은 다른 모든 요청보다 한 데이터 요청이 느린 경우 어떻게 될지입니다.