[Next] 공식 튜토리얼 뿌수기

Zero·2023년 11월 20일
1

[Next]

목록 보기
5/7
post-thumbnail

1장 시작하기

폴더구조

폴더구조

  • /app: 애플리케이션에 대한 모든 경로, 구성 요소 및 논리가 포함되어 있으며 여기서 주로 작업하게 됩니다.

  • /app/lib: 재사용 가능한 유틸리티 함수, 데이터 가져오기 함수 등 애플리케이션에서 사용되는 함수가 포함되어 있습니다.

  • /app/ui: 카드, 테이블, 양식 등 애플리케이션의 모든 UI 구성 요소가 포함되어 있습니다. 시간을 절약하기 위해 이러한 구성 요소의 스타일이 미리 지정되어 있습니다.

  • /public: 이미지와 같은 애플리케이션의 모든 정적 자산을 포함합니다.

  • /script: 이후 장에서 데이터베이스를 채우는 데 사용할 시드 스크립트가 포함되어 있습니다.

  • next.config.js : 애플리케이션 루트에 구성 파일도 있습니다 .

  • create-next-app : 해당 파일을 사용하여 새 프로젝트를 시작할 때 생성되고 사전 구성됩니다

MockData

app/lib/placeholder-data.js 의 경우 외부에서는 mock-data를 해당 이름으로 붙이는 것을 확인하였습니다. 데이터베이스에서 해당 데이터들을 초기 값으로 사용합니다.

// placeholder-data.js

const invoices = [
  {
    customer_id: customers[0].id,
    amount: 15795,
    status: 'pending',
    date: '2022-12-06',
  },
  {
    customer_id: customers[1].id,
    amount: 20348,
    status: 'pending',
    date: '2022-11-14',
  },
  // ...
];

API 타입 정의

/app/lib/definitions.ts 해당 파일에서 데이터베이스에서 반환될 유형들을 수동으로 정의합니다.

export type Invoice = {
  id: string;
  customer_id: string;
  amount: number;
  date: string;
  status: 'pending' | 'paid';
};

Prisma를 추천합니다.

현재 해당 파일에서는 수동으로 데이터 유형들을 선언하고 있습니다. 하지만 더 나은 안전성을 위해서는 Prisma를 권장합니다. 데이터베이스 스키마를 기반으로 유형을 자동으로 생성합니다.

2장 CSS 스타일링

전역으로 설정하기

/app/ui 폴더 안에 global.css 파일이 작성이 되어있습니다. 해당 파일은 CSS 재설정 규칙, HTML 요소에 대한 사이트 전체 스타일 등이 적혀져 있습니다. /app/layout.tsx 에 추가하면 전역으로 CSS가 설정이 됩니다.

import '@/app/ui/global.css';
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

@tailwind 라이브러리

global.css 파일에는 @tailwind 지시문이 적혀져있는 것을 볼 수 있습니다.

// /app/ui/global.css

@tailwind base;
@tailwind components;
@tailwind utilities;

@tailwind 라이브러리를 이용하면 클래스 이름을 통하여 쉽게 스타일을 지정할 수 있습니다.

import AcmeLogo from '@/app/ui/acme-logo';
import { ArrowRightIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
 
export default function Page() {
  return (
    // These are Tailwind classes:
    <main className="flex min-h-screen flex-col p-6">
      <div className="flex h-20 shrink-0 items-end rounded-lg bg-blue-500 p-4 md:h-52">
    // ...
  )
}

모듈의 필요성

만약 전통적인 CSS 규칙을 작성하거나 스타일을 JSX와 별도로 유지하는 것을 선호한다면 CSS 모듈이 훌륭한 대안입니다.

CSS 모듈을 사용 하면 고유한 클래스 이름을 자동으로 생성하여 CSS 범위를 구성 요소로 지정할 수 있으므로 스타일 충돌에 대해서도 걱정할 필요가 없습니다.

/app/ui/home.module.css

.shape {
  height: 0;
  width: 0;
  border-bottom: 30px solid black;
  border-left: 20px solid transparent;
  border-right: 20px solid transparent;
}
import styles from '@/app/ui/home.module.css';
 
//...
<div className="flex flex-col justify-center gap-6 rounded-lg bg-gray-50 px-6 py-10 md:w-2/5 md:px-20">
    <div className={styles.shape}></div>;
// ...

클래스 이름 변경을 도와주는 clsx 라이브러리

상태나 다른 조건에 따라 요소의 스타일을 조건부로 지정해야 하는 경우가 있을 수 있습니다.

[clsx](https://www.npmjs.com/package/clsx)클래스 이름을 쉽게 전환할 수 있는 라이브러리입니다. 문서를 살펴보는 것이 좋습니다.자세한 내용은 기본 사용법은 다음과 같습니다.

import clsx from 'clsx';
 
export default function InvoiceStatus({ status }: { status: string }) {
  return (
    <span
      className={clsx(
        'inline-flex items-center rounded-full px-2 py-1 text-sm',
        {
          'bg-gray-100 text-gray-500': status === 'pending',
          'bg-green-500 text-white': status === 'paid',
        },
      )}
    >
    // ...
)}

status 는 두 가지의 pending 또는 paid가 될 수 있습니다. 해당 조건이 true일 경우 앞의 클래스 이름이 추가되게 할 수 있습니다. pending상태이면 'bg-gray-100 text-gray-500' 가 적용이 됩니다.

만약 조건문을 붙이지 않고 클래스 이름만 붙이게 된다면 해당 클래스는 곧바로 적용이 됩니다.

3장 글꼴 및 이미지 최적화

글꼴 추가하기

글꼴 추가하기

Next.js를 사용하지 않고 페이지를 작성할 때는 폰트가 이후에 로드가 되면서 ui가 변경이 되는 불상사가 발생합니다. 하지만 Next.js는 next/font module을 사용할 경 자동으로 최적화 합니다.

다음 파일을 추가합니다.

/app/ui/fonts.ts

import { Inter } from 'next/font/google';
 
export const inter = Inter({ subsets: ['latin'] });

마지막으로 사용할 최상위 태그에 해당 글꼴을 추가해줍니다.

/app/layout.tsx

import '@/app/ui/global.css'
import {inter} from "@/app/ui/font";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={`${inter.className} antialiased`} >{children}</body>
    </html>
  );
}

inter.className으로 추가하였습니다. antialiased 은 @tailwind에서 지원하는 글꼴을 부드럽게 해주는 클래스입니다.

글꼴 적용

개발자도구를 사용해 적용이 잘 되는 것을 확인할 수 있습니다.

폰트에 적용이 가능한 옵션은 다음과 같습니다.

이미지 최적화 진행하기

Next.js는 public 폴더내에 있는 이미지와 같은 정적 자산을 제공할 수 있습니다.

<img
  src="/hero.png"
  alt="Screenshots of the dashboard project showing desktop version"
/>

그러나 다음과 같은 상황들을 수동으로 해야합니다.

  • 이미지가 다양한 화면 크기에 반응하는지 확인해야합니다..
  • 다양한 장치에 대한 이미지 크기를 지정해야합니다.
  • 이미지가 로드될 때 레이아웃이 바뀌는 것을 방지해야합니다.

이미지 최적화는 그 자체로 전문 분야로 간주될 수 있는 웹 개발의 큰 주제입니다. 이러한 최적화를 수동으로 구현하는 대신 next/image구성 요소를 사용하여 이미지를 자동으로 최적화할 수 있습니다.

<Image> 태그의 구성요소

<Image> 태그는 <img> 의 확장 버전이며 다음과 같은 자동 이미지 최적화 기능을 제공합니다.

  • 이미지가 로드될 때 자동으로 레이아웃 이동을 방지합니다.
  • 뷰포트가 더 작은 장치에 큰 이미지가 전달되는 것을 방지하기 위해 이미지 크기를 조정합니다.
  • 기본적으로 이미지 지연 로딩(이미지가 뷰포트에 들어갈 때 로드됨)
  • WebP 와 같은 최신 형식으로 이미지를 제공합니다.

다음과 같이 설정 할 수 있습니다.

/app/page.tsx

import Image from "next/image";

<Image
    src="/hero-desktop.png"
    width={1000}
    height={760}
    className="hidden md:block"
    alt="Screenshots of the dashboard project showing desktop version"
/>

레이아웃 변경을 방지하기 위해 미리 width와 height의 값을 집어넣어주어야 합니다. 또한 원본 이미지와 가로 세로의 비율이 동일해야합니다.

반응형 이미지 설정하기

<Image
    src="/hero-desktop.png"
    width={1000}
    height={760}
    className="hidden md:block"
    alt="Screenshots of the dashboard project showing desktop version"
/>
<Image
    src="/hero-mobile.png"
    width={560}
    height={620}
    className="block md:hidden"
    alt="Screenshot of the dashboard project showing mobile version"
/>

찾아보면 좋은 문서

4장 레이아웃 및 페이지 만들기

중첩된 라우팅

Next.js는 폴더가 중첩된 경로를 만드는 데 사용되는 파일 시스템 라우팅을 사용합니다 . 각 폴더는 URL 세그먼트 에 매핑되는 경로 세그먼트를 나타냅니다 .

경로설정

layout.tsxpage.tsx를 사용하여 각 경로에 대한 별도의 ui를 만들 수 있습니다.

page.tsx 은 React 구성 요소를 내보내주는 특수한 Next.js의 파일이며 경로에 엑세스하려면 반드시 필요합니다. /app/page.tsx 해당 경로는 루트 경로 / 와 연결된 페이지입니다.

중첩된 경로를 만들려면 다음과 같이 중첩하고 page.tsx를 추가하면 됩니다.

경로설정

/dashboard 경로로 이동할 시에 /dashboard/page.tsx 의 React 구성 요소파일이 보여지게 됩니다.

폴더와 파일을 다음과 같이 구성할 시 /dashboard/customers, /dashboard/invoices 와 같은 경로로 이동을 할 수 있습니다.

나눠진 폴더구조 내에서 font, test , img 파일등을 관리할 수 있습니다.

레이아웃 추가하기

나눠진 폴더에 layout을 추가하게 되면 하위 폴더들에게도 적용이 됩니다.

레이아웃 추가하기

Next.js에서 레이아웃을 사용할 때의 이점 중 하나는 탐색 시 페이지 구성 요소만 업데이트되고 다시 렌더링되지 않는다는 것입니다. 이를 부분 렌더링이라고 합니다.

부분렌더링

루트 레이아웃의 경우 글꼴을 추가할 때 사용했던 app폴더에 있는 layout을 말합니다. 해당 레이아웃을 사용하여 html 태그를 수정하고 메타데이터를 추가할 수 있습니다. 방금 생성한 새 레이아웃은 대시보드 페이지마다 고유하므로 위의 루트 레이아웃의 ui를 추가할 필요가 없습니다.

5장 페이지 간 탐색

탐색을 최적화하는 이유에 대해서

기본적으로 html에서 페이지를 이동할 때는 <a/> 태그를 이용합니다. 하지만 이는 문제가 있으니 바로 전체 페이지를 새로고침 한다는 것입니다.

Next.js에서는 <Link/> 구성요소를 이용하여 페이지 간의 연결을 할 수 있습니다.

사용은 <a/> 태그와 비슷하게 사용할 수 있습니다. <a href="..."/> 하듯이 <Link href="..."/> 하면 됩니다.

자동 코드 분할 및 프리패치

UI 경험을 향상 시키기 위해서 Next.js는 경로 세그먼트 별로 애플리케이션을 자동으로 코드 분할을 합니다. 기존 React SPA를 사용할 경우 초기에 모든 데이터를 전부 받았기에 초기 화면 로딩이 느릴 수 있었습니다. 하지만 코드를 분할 함으로서 필요한 부분만 로드해서 가져올 수 있습니다.

또한 오류가 발생하더라도 애플리케이션의 나머지 부분은 계속해서 동작합니다.

<Link/> 구성 요소가 브라우저 뷰 포트에 나타날 때 마다 Next.js 는 백그라운드에서 연결된 경로에 대한 코드를 자동으로 가져옵니다. 때문에 링크를 클릭하면 대상 페이지의 코드가 이미 백그라운드에 로드되어 페이지 전환이 즉각적으로 이루어집니다.

Link의 탐색 작동 방식에 대해 알 수 있습니다.

활성화된 링크를 표시하는 방법

일반적인 UI 패턴은 사용자가 어떤 페이지에 있는지 알려주는 활성 링크를 표시하는 것입니다.

아래 사진과 같이 현재 스포츠 페이지를 보고 있음을 알려줍니다.

활성화된 링크

Next.js에서는 usePathName() 을 지원하여 Next.js의 경로를 확인하고 이 패턴을 구현하는데 사용할 수 있는 후크를 제공합니다.

/app/ui/dashboard/nav-links.tsx

'use client';
 
import {
  UserGroupIcon,
  HomeIcon,
  InboxIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
 
// ...

이전에 알려주었던 clsx를 사용하면 다음과 같이 활성화 할 수 있습니다.

'use client';
 
import {
  UserGroupIcon,
  HomeIcon,
  DocumentDuplicateIcon,
} from '@heroicons/react/24/outline';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import clsx from 'clsx';
 
// ...
 
export default function NavLinks() {
  const pathname = usePathname();
 
  return (
    <>
      {links.map((link) => {
        const LinkIcon = link.icon;
        return (
          <Link
            key={link.name}
            href={link.href}
            className={clsx(
              'flex h-[48px] grow items-center justify-center gap-2',
              {
                'bg-sky-100 text-blue-600': pathname === link.href,
              },
            )}
          >
            <LinkIcon className="w-6" />
            <p className="hidden md:block">{link.name}</p>
          </Link>
        );
      })}
    </>
  );
}

6장 데이터베이스 설정

GitHub 저장소 만들기

기존에 Github 저장소가 없다면 추가합니다.

Vercel 계정 만들기

Github으로 계속하기를 통하여 Vercel 계정과 통합합니다.

프로젝트 연결 및 배포

다음으로, 방금 생성한 GitHub 저장소를 선택하고 가져올 수 있는 이 화면으로 이동하게 됩니다.

저장소 선택

프로젝트 이름을 지정하고 배포를 클릭합니다.

배포

이렇게 하면 프로젝트는 정상적으로 배포가 됩니다.

배포완료

Github 저장소를 연결해두었을 경우 default 브랜치에 push 가 될 때마다 Vercel에서 구성이 필요 없으 자동으로 애플리케이션을 재배포합니다.

Postgres 데이터베이스 만들기

다음으로, 데이터베이스를 설정하려면 대시보드로 계속을 클릭 하고 프로젝트 대시보드에서 스토리지 탭을 선택하세요. Connect Store → 새로 만들기 → Postgres → 계속을 선택합니다 .

저장소 만들기

약관에 동의하고, 데이터베이스에 이름을 할당하고, 데이터베이스 지역이 워싱턴 DC(iad1) 로 설정되어 있는지 확인하세요 . 이는 기본 지역 이기도 합니다.모든 새로운 Vercel 프로젝트에 대해. 데이터베이스를 동일한 지역에 배치하거나 애플리케이션 코드에 가깝게 배치하면 데이터 요청을 위한 대기 시간을 줄일 수 있습니다.

연결하기

💡 데이터베이스 영역이 초기화되면 변경할 수 없습니다. 다른 지역을 이용하고 싶다면, 데이터베이스를 생성하기 전에 설정해야 합니다.

연결이 되면 데이터베이스에 대한 정보를 가져오기위해 .env.local를 클릭합니다.

정보 가져오기

코드 편집기로 이동하여 .env.example파일 이름을 .env. Vercel에서 복사한 내용을 붙여넣습니다.

💡 GitHub에 푸시할 때 데이터베이스 비밀이 노출되는 것을 방지하려면 .gitignore 파일 로 이동하여 무시된 파일에 있는지 .env 파일을 확인합니다.

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

데이터베이스 시드

템플릿을 사용하여 프로젝트를 만들었다면 /scripts프로젝트 폴더 에 seed.jsinvoices 파일이 들어가 있습니다. 해당 파일는 테이블을 생성하고 시드를 생성하기 위한 코드들이 포함되어 있습니다.

/package.json

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

데이터베이스 탐색

데이터베이스가 어떻게 생겼는지 살펴보려면 Vercel로 돌아가 사이드 탐색에서 Data를 클릭하여 볼 수 있습니다.

이 섹션에서는 사용자, 고객, 송장 및 수익이라는 4개의 새로운 테이블을 찾을 수 있습니다.

데이터베이스 탐색

기존에 placeholder-data.js 파일이 들어간 것을 확인 할 수 있습니다.

쿼리 실행

Query 탭으로 전환하여 데이터베이스와 상호 작용할 수 있습니다. 이 섹션에서는 표준 SQL 명령을 지원합니다. 아래는 예시코드 입니다.

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

7장 데이터를 가져오는 방법

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

API 레이어 : API는 애플리케이션과 데이터 베이스 사이의 중간 계층입니다.

다음과 같은 경우에 사용합니다.

  • API를 제공하는 타사 서비스를 이용하는 경우
  • 클라이언트에서 데이터를 가져오는 경우 (ex : 데이터베이스 비밀이 클라이언트에 노출되는 것을 방지하기 위해 서버에서 실행되는 API 계층이 있어야 합니다.)

Next.js에서는 Route Handlers를 사용하여 API 엔드포인트를 생성할 수 있습니다.

데이터베이스 쿼리

풀 스택 애플리케이션을 만들때에는 데이터베이스와 상호 작용하는 코드를 작성해야합니다. Postgres나 SQL, ORM, Prisma 등을 이용하여 만들 수 있습니다.

다음과 같은 상황들을 고려해보아야 합니다.

  • API 엔드포인트를 생성할 때 데이터베이스와 상호 작용하는 논리를 작성해야 합니다.
  • React Server 구성 요소(서버에서 데이터 가져오기)를 사용하는 경우 API 계층을 건너뛰고 데이터베이스 정보가 클라이언트에 노출될 위험 없이 데이터베이스를 직접 쿼리할 수 있습니다.

서버 구성 요소를 사용하여 데이터를 가져오기

Next.js의 기본 Component는 Server Component입니다. 때문에 서버 구성요소를 사용하여 데이터를 가져올 수도 있습니다.

다음과 같은 장점이 있습니다.

  • useState, useEffect의 호출 없이 async/await 문법으로 데이터를 가져올 수 있습니다.
  • 서버 컴포넌트는 서버에서 실행이 되므로 비용이 많이 드는 데이터나 로직들을 서버에서 보관할 수 있고 결과만 클라이언트에 전달할 수 있다.
  • 서버에서 실행이 되기 때문에 직접 쿼리를 할 수 있다.

SQL를 사용합니다.

해당 프로젝트에서는 SQL을 사용하는데 다음과 같은 이점들이 있습니다.

  • SQL은 관계형 데이터베이스를 쿼리하기 위한 업계 표준입니다.
  • SQL에 대한 기본적인 이해가 있으면 관계형 데이터베이스의 기본사항을 이해..
  • SQL은 다목적이므로 특정 데이터를 가져오거나 조작할 수 있습니다.
  • Vercel Postgres SDK는 SQL 주입에 대한 보호기능을 제공합니다.

@vercel/postgressql 를 이용하면 쿼리할 수 있습니다.

// /app/lib/data.ts

export async function fetchLatestInvoices() {
  try {
    const data = await sql<LatestInvoiceRaw>`
      SELECT invoices.amount, customers.name, customers.image_url, customers.email, invoices.id
      FROM invoices
      JOIN customers ON invoices.customer_id = customers.id
      ORDER BY invoices.date DESC
      LIMIT 5`;

    const latestInvoices = data.rows.map((invoice) => ({
      ...invoice,
      amount: formatCurrency(invoice.amount),
    }));
    return latestInvoices;
  } catch (error) {
    console.error('Database Error:', error);
    throw new Error('Failed to fetch the latest invoices.');
  }
}
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 '@/app/lib/data';

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

하지만 문제점이 두 가지가 있습니다.

  1. 데이터 요청이 의도치 않게 서로를 차단하여 요청 폭포가 생성됩니다 .
  2. 기본적으로 Next.js는 성능 향상을 위해 경로를 미리 렌더링하며 이를 정적 렌더링 이라고 합니다 . 따라서 데이터가 변경되면 대시보드에 반영되지 않습니다.

요청 폭포수란 무엇일까요?

폭포수란 이전 요청의 완료에 따라 달라지는 일련의 네트워크 요청을 나타냅니다. 데이터 가져오기의 경우 각 요청의 이전 요청이 있어야지 해당 값을 반환한 후에 사용을 할 수 있습니다.

요청 폭포수

예를 들어, fetchLatestInvoices() 실행을 시작하려면 fetchRevenue() 먼저 실행될 때까지 기다려야 합니다.

그래도 async/await 을 이용하면 해당 문제를 완벽히는 아니지만 바로 잡을 수 있습니다.

/app/dashboard/page.tsx

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

이 패턴이 반드시 나쁜 것은 아닙니다. 다음 요청을 하기 전에 조건이 충족되기를 원하기 때문에 폭포수를 원하는 경우가 있을 수 있습니다. 예를 들어 사용자 ID와 프로필 정보를 먼저 가져오고 싶을 수 있습니다. ID가 있으면 친구 목록을 가져올 수 있습니다. 이 경우 각 요청은 이전 요청에서 반환된 데이터에 따라 달라집니다.

그러나 이 동작은 의도하지 않은 것일 수도 있으며 성능에 영향을 미칠 수도 있습니다.

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`;
 
    const data = await Promise.all([
      invoiceCountPromise,
      customerCountPromise,
      invoiceStatusPromise,
    ]);
    // ...
  }
}

하지만 한 가지 단점이 있습니다. 만약 데이터를 불러오는데 하나가 느리다면? 이러한 경우 나머지 api들도 느리게 불러와야할 수도 있습니다.

8장 정적 및 동적 렌더링

정적 렌더링이란?

빌드 또는 재렌더링시에 데이터를 가져오거나 렌더링이 서버에서 발생하는 것을 말합니다.

정적 렌더링

사용자가 애플리케이션을 방문할 때마다 캐시된 결과를 제공합니다.

다음과 같은 이점들이 있습니다.

  1. 빠른 웹 사이트 : 사전 렌더링된 콘텐츠를 캐시하고 전세계적으로 배포할 수 있습니다. 이를 통해 많은 사용자가 웹 사이트를 더욱 빠르고 안정적으로 액세스할 수 있습니다.
  2. 서버 로드 감소 : 콘텐츠가 캐시되기 때문에 서버는 각 사용자 요청에 대해 콘텐츠를 동적으로 생성할 필요가 없습니다.
  3. SEO : 사전 렌더링된 콘텐츠는 페이지가 로드될 때 이미 콘텐츠로서 사용할 수 있으므로 검색 엔진 크롤러가 색인을 생성하기가 더 쉬워 검색 엔진 순위가 올라갈 수 있습니다.

정적 렌더링은 정적 블로그 게시물이나 제품 페이지와 같이 사용자 간에 공유되는 데이터나 데이터가 없는 UI에 유용합니다 정기적으로 업데이트되는 개인화된 데이터가 있는 대시보드에는 적합하지 않을 수 있습니다.

동적 렌더링이란?

동적 렌더링을 사용하면 사용자가 데이터를 요청할 시 (페이지를 방문할 때) 각 사용자의 콘텐츠가 서버에서 렌더링 됩니다. 동적 렌더링과는 차이점이 있습니다.

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

대시보드를 동적으로 만들기

기본적으로 @vercel/postgres 자체 캐싱 의미 체계를 설정하지 않습니다. 이를 통해 프레임워크는 자체 정적 및 동적 동작을 설정할 수 있습니다.

서버 구성 요소 내부에서 호출되는 Next.js API unstable_noStore나 데이터 가져오기 기능을 사용하여 정적 렌더링을 거부할 수 있습니다. 이것을 추가해 보겠습니다.

data.ts 에서 next/cacheunstable_noStore 를 추가하여 데이터의 가져옴으로서 캐싱된 정보가 없으니 매번 새롭게 요청할 수 있습니다.

/app/lib/data.ts

// ...
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();
 
  // ...
}
 
export async function fetchLatestInvoices() {
  noStore();
  // ...
}
 
export async function fetchCardData() {
  noStore();
  // ...
}
 
export async function fetchFilteredInvoices(
  query: string,
  currentPage: number,
) {
  noStore();
  // ...
}
 
export async function fetchInvoicesPages(query: string) {
  noStore();
  // ...
}
 
export async function fetchFilteredCustomers(query: string) {
  noStore();
  // ...
}
 
export async function fetchInvoiceById(query: string) {
  noStore();
  // ...
}

💡 unstable_noStore 은 실험적인 api이며 추후 변경할 수도 있습니다. 안정적인 API를 사용하고 싶다면 다음과 같이 변경해줍니다. export const dynamic = "force-dynamic"

느린 데이터가 오는 경우

데이터가 오는데 일부로 3초가 걸리게 설정을 해주었습니다.

export async function fetchRevenue() {
  try {
    // We artificially delay a response for demo purposes.
    // Don't do this in a real application
    console.log('Fetching revenue data...');
    await new Promise((resolve) => setTimeout(resolve, 3000));
 
    const data = await sql<Revenue>`SELECT * FROM revenue`;
 
    console.log('Data fetch complete after 3 seconds.');
 
    return data.rows;
  } catch (error) {
    console.error('Database Error:', error);
    throw new Error('Failed to fetch revenue data.');
  }
}

다음과 같이 설정을 할 경우 해당 데이터를 가져오는 동안 전체 페이지가 차단됩니다. 따라서 개발자가 해결해야하는 공통 과제는 다음과 같습니다.

동적 렌더링을 사용하면 애플리케이션 속도는 가장 느린 데이터를 가져오는 속도에 맞춰집니다. (이후에 동작을 하기 때문에)

9장 스트리밍

스트리밍 : 인터넷에서 데이터를 연속적으로 전송하여 실시간으로 재생하는 것

스트리밍은 경로를 더 작은 경로로 나누고 서버에서 클라이언트로 점진적으로 스트리밍 할 수 있는 것을 이야기 합니다.

스트리밍

스트리밍을 하게 될 경우 하나의 데이터를 불러오는 것이 늦어져 페이지 전체가 멈추는 일을 사전에 차단할 수 있습니다.

Next.js에서는 다음과 같은 방법들로 구현을 할 수 있습니다.

  • 페이지 수준에서 loading.tsx 파일을 사용합니다.
  • 특정 구성 요소의 경우 <Suspense> 을 사용합니다.

전체 페이지를 스트리밍 하는법

최상위 폴더에 다음과 같은 파일을 만듭니다.

/app/dashboard/loading.tsx

export default function Loading() {
  return <div>Loading...</div>;
}
  1. loading.tsx Suspense를 기반으로 구축된 특별한 Next.js 파일로, 페이지 콘텐츠가 로드되는 동안 대체 UI로 표시할 fallback UI를 생성할 수 있습니다.
  2. 정적 컴포넌트인 <Sidebar/> 즉시 표시됩니다. 사용자는 동적 콘텐츠가 로드되는 동안 상호 작용할 수 있습니다.
  3. 사용자는 다른 페이지로 이동하기 전에 페이지 로드가 완료될 때까지 기다릴 필요가 없습니다(이를 중단 가능한 탐색이라고 함).

URL 경로에 영향을 주지 않고 폴더를 구성하는 방법

app에 중첩된 폴더는 일반적으로 URL 경로에 매핑이 됩니다. 그러나 폴더가 경로 URL에 포함되지 않도록 폴더를 경로 그룹으로 표시할 수 있습니다.

이를 통해 URL 경로 구조에 영향을 주지 않고 세그먼트와 프로젝트 파일을 논리 그룹으로 구성할 수 있습니다.

경로 그룹은 폴더 이름을 괄호로 묶어 생성할 수 있습니다.(folderName)

URL에 영향을 주지 않고 경로를 구성하려면 관련 경로를 함께 유지하는 그룹을 만드는 방법은 다음과 같습니다. 괄호 안의 폴더는 URL에서 생략됩니다(예: (marketing)또는 (shop).

라우트 분리

특정 세그먼트를 레이아웃으로 선택하는 방법

특정 경로를 레이아웃으로 선택하려면 새 경로 그룹 (shop)을 만들고 동일한 레이아웃을 공유하는 경로를 그룹(예: account및 cart)으로 이동합니다. 그룹 외부의 경로는 레이아웃을 공유하지 않습니다(예: checkout).

레이아웃 선택

여러 루트 레이아웃을 만드는 방법

여러 루트 레이아웃을 생성하려면 최상위 layout.js파일을 제거하고 layout.js각 경로 그룹 내에 파일을 추가합니다. 이는 완전히 다른 UI나 경험을 가진 섹션으로 애플리케이션을 분할하는 데 유용합니다. 각 루트 레이아웃에 및 **<html>태그 <body>를 추가해야 합니다.

여러 루트 레이아웃

위의 예에서 (marketing)및 둘 다 (shop)자체 루트 레이아웃을 갖습니다.

💡알아두면 좋을 팁
1. 경로 그룹을 설정하는 것은 그룹을 만드는 이외에 효과는 없습니다. URL 경로에 영향을 주지 않습니다.
2. 경로 그룹을 포함하는 경로는 다른 경로와 동일한 URL 경로로 해석이 되서는 안됩니다. 해당 그룹은 경로에 영향을 주지 않기때문에 동일한 경로가 있다면 오류가 발생합니다.
예시 (marketing)/about/page.js (shop)/about/page.js ⇒ /about 으로 접속하기에 오류발생
3. 최상위 layout.js 파일이 없이 여러 루트 레이아웃을 사용하는 경우 page.js 는 그룹 내에서 반드시 정의되어야 사용이 가능합니다. 예: app/(marketing)/page.js.
4. 다수의 루트 레이아웃이 있을 경우 루트 레이아웃a을 벗어나 다른 루트 레이아웃b으로 이동을 하고 다시 루트 레이아웃a으로 돌아왔을 때 루트 레이아웃 a 의 전체 페이지가 다시 로드가 됩니다. 이는 클라이언트에서 사용하는 네비게이션과 달리 서버 측에서 전체 페이지 로드를 발생시키는 것입니다.

특정 그룹 페이지만 로딩을 하는 경우

loading.tsx/invoices/page.tsx, /customers/page.tsx 에도 각각 적용을 할 수 있습니다.

특정 페이지만 로드

위와 같이 설정을 한 경우 dashboard.tsx 만 로딩 페이지가 적용이 됩니다.

구성 요소를 스트리밍 하는 경우 (Suspense)

지금까지 loading.tsx 를 이용하여 전체 페이지를 스트리밍을 하고 있습니다. 만약 작은 단위로 로딩 상태를 알려주고 싶다면 React Suspense를 사용하면 더욱 세분화되고 특정 구성 요소를 스트리밍할 수 있습니다.

Suspense를 사용하면 일부 조건이 충족될 때까지(예: 데이터 로드) 애플리케이션의 렌더링 부분을 연기할 수 있습니다. Suspense에서 동적 구성요소를 래핑할 수 있습니다. 그런 다음 동적 구성요소가 로드되는 동안 표시할 대체 구성요소를 전달합니다.

/app/dashboard/(overview)/page.tsx

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

기존에 느린 데이터 요청을 하는 RevenueChart의 props를 지워주고 해당 파일 내에서 요청을 하도록 변경하였습니다.

/app/ui/dashboard/revenue-chart.tsx

export default async function RevenueChart() {
  const chartHeight = 350;
  const revenue = await fetchRevenue();
	...
}

이제 새로고침을 할 경우 느리게 데이터를 가져오는 <RevenueChart/> 만 대체 컴포넌트 (fallback)을 보여줍니다.

서스펜스

Suspense 경계를 배치할 위치를 결정합니다.

Suspense 경계를 배치하는 위치는 다음 몇 가지 사항에 따라 달라집니다.

  1. 페이지가 스트리밍 될 때 사용자가 페이지를 어떻게 경험하게 할지
  2. 어떤 콘텐츠에 우선순위를 두고 싶은지.
  3. 구성 요소가 데이터 가져오기에 의존하는 경우.

정답은 없습니다.

  • 했던 것처럼 전체 페이지를loading.tsx스트리밍할 수 있지만 구성 요소 중 하나의 데이터 가져오기 속도가 느린 경우 로드 시간이 길어질 수 있습니다.
  • 컴포넌트 단위를 스트리밍하여 시차효과를 만들 수도 있습니다 . 하지만 래퍼 구성 요소를 만들어야 합니다.

서스펜스 경계를 배치하는 위치는 애플리케이션에 따라 달라집니다. 일반적으로 데이터 가져오기를 필요한 구성 요소로 이동한 다음 Suspense에서 해당 구성 요소를 래핑하는 것이 좋습니다. 그러나 애플리케이션에 필요한 경우 섹션이나 전체 페이지를 스트리밍하는 데 아무런 문제가 없습니다.

10장 부분 사전 렌더링 (Next14)

현재 경로에서 동적 함수 ex: noStroe(), cookies()를 호출 한다면 전체 경로가 동적이 됩니다.

이는 오늘날 대부분의 웹 앱이 구축되는 방식과 일치합니다. 전체 애플리케이션 또는 특정 경로 에 대해 정적 렌더링과 동적 렌더링 중에서 선택합니다 .

그러나 대부분의 경로는 완전히 정적이거나 동적이지 않습니다. 정적 콘텐츠와 동적 콘텐츠가 모두 포함된 경로가 있을 수 있습니다. 예를 들어 소셜 미디어 피드가 있고 게시물은 정적이지만 게시물에 대한 좋아요 수는 동적이라고 가정해 보겠습니다. 또는 제품 세부 정보는 정적이지만 사용자 장바구니는 동적인 전자 상거래 사이트입니다.

기존에 만들었던 페이지를 분리를 해보면다음과 같습니다.

컴포넌트 분리

• 구성 <SideNav>요소는 데이터에 의존하지 않으며 사용자에게 맞춤화되지 않으므로 정적 일 수 있습니다 .

• 구성 요소는 <Page>자주 변경되는 데이터에 의존하고 사용자에게 개인화되므로 동적 일 수 있습니다 .

부분 사전 렌더링이란

일부 부분을 동적으로 유지하면서 정적 로딩 셸을 사용하여 경로를 렌더링을 할 수 있는 실험적 기능입니다. 즉, 경로의 동적 부분을 분리할 수 있습니다.

부분 사전 렌더링

사용자가 경로를 방문할 때:

  • 정적 경로 셸이 제공되므로 초기 로드가 빨라집니다.
  • 셸에는 동적 콘텐츠가 비동기로 로드되는 구멍이 남습니다.
  • 비동기 홀이 병렬로 로드되어 페이지의 전체 로드 시간이 단축됩니다.

이는 전체 경로가 완전히 정적이거나 동적인 오늘날 애플리케이션의 작동 방식과 다릅니다.

부분 사전 렌더링은 매우 빠른 정적 에지 전달과 완전한 동적 기능을 결합하며 웹 애플리케이션의 기본 렌더링 모델이 될 가능성이 있다고 믿습니다. 최고의 정적 사이트 생성과 동적 전달을 결합합니다.

ppr을 활성화 한 것으로 부분 사전 렌더링을 할 수 있습니다.

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    ppr: true,
  },
}
 
module.exports = nextConfig

코드 출처

/app/page.tsx

export default function Page() {
  return (
    <main>
      <header>
        <h1>My Store</h1>
        <Suspense fallback={<CartSkeleton />}>
          <ShoppingCart />
        </Suspense>
      </header>
      <Banner />
      <Suspense fallback={<ProductListSkeleton />}>
        <Recommendations />
      </Suspense>
      <NewProducts />
    </main>
  );
}

ppr이 활성화되면, 이 페이지는 <Suspense /> 경계를 기반으로 정적 로딩 셸을 생성합니다. Suspense의 fallback을 사전 렌더링(prerendered)합니다.

요청을 할 경우 다음과 같은 정적 HTML 셸을 즉시 제공합니다.

<main>
  <header>
    <h1>My Store</h1>
    <div class="cart-skeleton">
      <!-- Hole -->
    </div>
  </header>
  <div class="banner" />
  <div class="product-list-skeleton">
    <!-- Hole -->
  </div>
  <section class="new-products" />
</main>

기존에 작성한 <ShoppingCart /> 에서 사용자의 세션을 확인하기 위해 Cookie를 읽지만 위 컴포넌트에서 정적 셸과 동일한 HTTP 요청의 일부로 스트리밍됩니다. 추가적인 네트워크 왕복이 필요하지 않습니다.

app/cart.tsx

import { cookies } from 'next/headers'
 
export default function ShoppingCart() {
  const cookieStore = cookies()
  const session = cookieStore.get('session')
  return ...
}

NextJS에서는 여기까지 애플리케이션에서 데이터 가져오기를 최적화하기 위해 몇 가지 작업을 수행했습니다.

  1. 서버와 데이터베이스 간의 대기 시간을 줄이기 위해 애플리케이션 코드와 동일한 지역에 데이터베이스를 만들었습니다.
  2. React Server Components를 사용하여 서버에서 데이터를 가져왔습니다. 이를 통해 서버에서 비용이 많이 드는 데이터 가져오기 및 논리를 유지하고, 이를 통해 클라이언트 측 JavaScript 번들을 줄이고 초기 렌더링 시간을 줄였으며, 데이터베이스 비밀이 클라이언트에 노출되는 것을 방지할 수 있습니다.
  3. SQL을 사용하여 필요한 데이터만 가져오므로 각 요청에 대해 전송되는 데이터의 양과 메모리 내 데이터를 변환하는 데 필요한 JavaScript의 양이 줄어듭니다.
  4. JavaScript를 사용하여 데이터 가져오기를 병렬화하는 것이 합리적입니다.
  5. 느린 데이터 요청이 전체 페이지를 차단하는 것을 방지하고 사용자가 모든 것이 로드될 때까지 기다리지 않고 UI와 상호 작용을 시작할 수 있도록 스트리밍을 구현했습니다.
  6. 데이터 가져오기를 필요한 구성 요소로 이동하여 부분 사전 렌더링을 준비할 때 경로의 어느 부분이 동적으로 이루어져야 하는지 분리합니다.

11장 검색 및 페이지 매김 추가하기

검색 기능을 사용할 때 유용한 hook 알아보기

검색 기능을 구현하는 데 사용할 Next.js 클라이언트 hook은 다음과 같습니다.

	'use client'
 
import { useEffect } from 'react'
import { usePathname, useSearchParams } from 'next/navigation'
 
export function NavigationEvents() {
  const pathname = usePathname()
  const searchParams = useSearchParams()
 
  useEffect(() => {
    const url = `${pathname}?${searchParams}`
    console.log(url)
    // You can now use the current URL
    // ...
  }, [pathname, searchParams])
 
  return null
}
  • useSearchParams()
    • 현재 URL의 매개변수에 액세스할 수 있습니다.예를 들어, /dashboard/invoices?page=1&query=pending 이 URL에 대한 검색 매개변수는 다음과 같습니다. {page: '1', query: 'pending'}
  • usePathname()
    • 현재 URL의 경로명을 읽을 수 있습니다. 예를 들어 /dashboard/invoicesuse 경로의 경우 Pathname'/dashboard/invoices'을 반환합니다
  • useRouter()
    • 프로그래밍 방식으로 클라이언트 구성 요소 내의 경로 간 탐색을 활성화합니다. 사용할 수 있는 방법은 여러가지가 있습니다 .
const router = useRouter();

router.push(href: string, { scroll: boolean })
// 제공된 경로(href)로 이동합니다. 브라우저 history의 기록이 남습니다.

router.replace(href: string, { scroll: boolean })
// 제공된 경로(href)로 이동합니다. 브라우저 history의 기록이 남지 않습니다.

router.prefetch(href: string)
// 더 빠른 클라이언트 측 전환을 위해 제공된 경로를 미리 가져옵니다. Link와 비슷합니다.

router.back()
// 브라우저 history의 이전 페이지로 이동합니다.

router.forword()
// 브라우저 기록 스택의 다음 페이지로 이동합니다.

💡scroll 같은 경우 기본적으로 Next에서 제공하는 페이지를 이동할 경우 scroll은 top 0 으로 이동하게 됩니다. 해당 값을 scroll : false 로 변경하게 될 경우 해당 동작을 막을 수 있습니다.

💡 App Router를 사용할 때에는 next/router 경로가 아닌 next/navigation 경로에서 가져와야 합니다.

검색 기능을 추가하는 방법

검색 기능을 구현하는 방법은 다음과 같습니다.

  1. 사용자의 입력을 캡처합니다.
  2. 검색 매개변수로 URL을 업데이트합니다.
  3. URL을 입력 필드와 동기화 상태로 유지하세요.
  4. 검색어를 반영하도록 테이블을 업데이트합니다.
'use client'; // 1.

import {MagnifyingGlassIcon} from '@heroicons/react/24/outline';
import {usePathname, useRouter, useSearchParams} from "next/navigation";

export default function Search({placeholder}: { placeholder: string }) {
    const searchParams = useSearchParams();
    const pathname = usePathname(); // 2
    const { replace } = useRouter();

    function handleSearch(term: string) {
        const params = new URLSearchParams(searchParams);
        if (term) {
            params.set('query', term); // 3
        } else {
            params.delete('query');
        }

        replace(`${pathname}?${params.toString()}`); // 4
    }

    return (
        <div className="relative flex flex-1 flex-shrink-0">
            <label htmlFor="search" className="sr-only">
                Search
            </label>
            <input
                className="peer block w-full rounded-md bord ..."
                placeholder={placeholder}
                onChange={(e) => {
                    handleSearch(e.target.value);
                }}
								defaultValue={searchParams.get('query')?.toString()} {// 5}
            />
            <MagnifyingGlassIcon
                className="absolute left-3 top-1/2 h-[18px] w-[18px] ..."/>
        </div>
    );
}
  1. client측 컴포넌트 입니다. 동적으로 변경이 될 수 있습니다.
  2. pathName의 경우 현재 경로인 "/dashboard/invoices" 를 가져옵니다.
  3. params의 경우 사용자의 검색 데이터로 URL 친화적인 형식으로 변환하여 가져옵니다.
  4. 사용자의 검색 데이터로 URL을 업데이트합니다. 예를 들어 /dashboard/invoices?query=lee사용자가 "Lee"를 검색하는 경우입니다.
  5. 입력 필드가 URL과 동기화가 되고 공유시 채워지도록 하려면 dafaultValue의 값에 전달할 수 있습니다.

💡 상태를 사용하는 경우 value 속성을 사용하여 제어되는 구성 요소를 만듭니다. 이는 React가 입력 상태 관리한다는 얘기입니다. 하지만 상태를 사용하지 않으므로 defalutValue를 사용합니다.

검색을 할 때에는 debounce를 사용하여 검색을 최적화 할 수 있습니다.

디바운싱 작동 방식

  1. Trigger Event : 디바운싱 되어야 하는 이벤트(검색창의 키 입력 등)가 발생하면 타이머가 시작됩니다.

    1. 대기 : 타이머가 만료되기 전에 새로운 이벤트가 발생하면 타이머가 재설정됩니다.
    2. 실행 : 타이머가 카운트다운 끝에 도달하면 디바운싱된 함수가 실행됩니다.

    use-debounce 라이브러리를 설치하여 쉽게 관리할 수 있습니다.

    // ...
    import { useDebouncedCallback } from 'use-debounce';
     
    // Inside the Search Component...
    const handleSearch = useDebouncedCallback((term) => {
      console.log(`Searching... ${term}`);
     
      const params = new URLSearchParams(searchParams);
      if (term) {
        params.set('query', term);
      } else {
        params.delete('query');
      }
      replace(`${pathname}?${params.toString()}`);
    }, 300);

일반 페이지에서 값 가져오기

만약 client가 아닌 일반적인 페이지라면 어떻게 가져와야하나 걱정을 할 수 있습니다. page의 기본 props를 이용해서 가져올 수 있습니다.

// ...
import { useDebouncedCallback } from 'use-debounce';
 
// Inside the Search Component...
const handleSearch = useDebouncedCallback((term) => {
  console.log(`Searching... ${term}`);
 
  const params = new URLSearchParams(searchParams);
  if (term) {
    params.set('query', term);
  } else {
    params.delete('query');
  }
  replace(`${pathname}?${params.toString()}`);
}, 300);

이 함수는 의 내용을 래핑 handleSearch하고 사용자가 입력을 중지한 후 특정 시간(300ms) 후에만 코드를 실행합니다.

이를 통하여 키를 누를 때마다 데이터베이스의 쿼리가 요청이 되는 것을 방지 할 수 있습니다.

[params](https://nextjs.org/docs/app/api-reference/file-conventions/page#params-optional) : 루트 세그먼트부터 해당 페이지까지의 동적 경로 매개변수를 포함하는 객체입니다.

예시URLparams
app/shop/[slug]/page.js/shop/1{ slug: '1' }
app/shop/[category]/[item]/page.js/shop/1/2{ category: '1', item: '2' }
app/shop/[...slug]/page.js/shop/1/2{ slug: ['1', '2'] }

searchParams : 현재 url의 검색 매개변수를 포함하는 객체입니다.

URLsearchParams
/shop?a=1{ a : ‘1’ }
/shop?a=1&b=2{ a : ‘1’, b : ‘2’ }
/shop?a=17&a=2{ a : [’1’, ‘2’] }
  1. searchParams 은 값을 미리 알 수 없는 동적 API 입니다 . 이를 사용하면 요청 시 페이지가 동적 렌더링 Server Component로 선택됩니다 .
  2. searchParams 은 일반 Javascript 개체를 반환합니다.

12장 데이터 변형 작업

서버 작업이란?

React Server Action을 사용하면 서버에서 직접 비동기 코드를 실행할 수 있습니다. 이를 위해 API 엔드 포인트를 따로 생성할 필요가 없습니다. 대신 서버에서 실행되고 클라이언트 또는 서버 구성 요소에서 호출될 수 있는 비동기 함수를 작성해야 합니다.

서버 작업과 함께 양식 사용

React에서는 요소 action의 속성을 사용하여 <form/> 작업을 호출할 수 있습니다.

// Server Component
export default function Page() {
  // Action
  async function create(formData: FormData) {
    'use server';
 
    // Logic to mutate data...
  }
 
  // Invoke the action using the "action" attribute
  return <form action={create}>...</form>;
}

서버 구성 요소 내에서 서버 작업을 호출하면 점진적인 향상이 가능하다는 장점이 있습니다. 클라이언트에서 JavaScript가 비활성화된 경우에도 양식이 작동합니다.

또한 서버 작업은 Next.js의 캐싱과도 긴밀하게 연결되어 있어 서버 작업을 통해 양식을 제출할 경우 데이터를 변경할 수 있을 뿐만이 아니라 같은 API를 사용하여 관련 캐시의 유효성을 다시 검사할 수도 있습니다. revalidatePath, revalidateTag 와 마찬가지로요.

서버 구성요소

13장 오류 처리

전체 에러 처리하기

error.tsx 파일로 오류 처리가 가능합니다. 예상치 못한 오류에 대한 포괄적인 역할을 하며 사용자에게 대체 UI를 표시할 수 있습니다.

'use client';
 
import { useEffect } from 'react';
 
export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Optionally log the error to an error reporting service
    console.error(error);
  }, [error]);
 
  return (
    <main className="flex h-full flex-col items-center justify-center">
      <h2 className="text-center">Something went wrong!</h2>
      <button
        className="mt-4 rounded-md bg-blue-500 px-4 py-2 text-sm text-w ..."
        onClick={
          // Attempt to recover by trying to re-render the invoices route
          () => reset()
        }
      >
        Try again
      </button>
    </main>
  );
}

Error.tsx는 다음과 같은 특징을 가지고 있습니다.

  1. “use client”를 사용합니다.
  2. 두 가지 props를 허용합니다.
    1. error : 자바스크립트의 Error로부터 파생된 기본 에러객체 입니다.
    2. reset : 오류 경계를 재설정하는 기능입니다. 실행되면 함수는 경로 세그먼트를 다시 렌더링 하려고 시도합니다.

에러를 고의적으로 발생시킬 경우 다음과 같은 UI가 노출이 됩니다.

export async function deleteInvoice(id: string) {
    throw new Error('Failed to Delete Invoice');
    try {
        await sql`DELETE FROM invoices WHERE id = ${id}`;
        revalidatePath('/dashboard/invoices');
        return { message: 'Deleted Invoice.' };
    } catch (error) {
        return {message: 'Database Error: Failed to Delete Invoice.'};
    }
}

에러처리

404 Page 처리하기

오류를 처리할 수 있는 다른 방법으로는 notFound() 함수를 사용하는 것입니다. 존재하지 않는 리소스를 불러오려고 할 때 사용할 수 있습니다.

다음과 같이 없는 not-found.tsx 파일을 만들어 줍니다.

404 페이지

관련된 데이터를 받아오지 못할 경우 빈 배열인 [] 를 받아옵니다. 따라서 notFound() 함수를 통해 오류를 발생 시킬 수 있습니다.

import { fetchInvoiceById, fetchCustomers } from '@/app/lib/data';
import { updateInvoice } from '@/app/lib/actions';
import { notFound } from 'next/navigation';
 
export default async function Page({ params }: { params: { id: string } }) {
  const id = params.id;
  const [invoice, customers] = await Promise.all([
    fetchInvoiceById(id),
    fetchCustomers(),
  ]);
 
  if (!invoice) {
    notFound();
  }
 
  // ...
}

이제 해당하는 경로를 못 찾을 시에는 404 Page가 뜨게 됩니다.

404페이지

오류 처리에 대한 API 모음

14장 접근성 개선

접근성이란?

접근하기 힘든 사용자를 포함하여 모든 사람들이 사용할 수 있는 웹 애플리케이션을 설계하고 구현하는 것을 의미합니다.

해당 과정에선 접근성 기능과 애플리케이션의 접근성을 높이는 방법에 대해서 알아갑니다.

Next.js에서 ESLint 접근성 플러그인을 사용하는 방법

Next.js에는 기본적으로 [eslint-plugin-jsx-a11y](https://www.npmjs.com/package/eslint-plugin-jsx-a11y) 접근성 문제를 조기에 발견하는 데 도움이 되는 플러그인이 있습니다. 예를 들어, 이 플러그인은 텍스트가 없는 이미지가 있는 경우 alt,  aria-* 및  role 속성을 잘못 사용하는 경우 등을 경고합니다.

양식 접근성 개선

양식의 접근성을 개선하기 위해 세 가지 작업을 수행중에 있습니다.

  • 시맨틱 HTML: <div/> 대신 <img/><option/> 태그들을 사용하고 있습니다. 이를 통해 보조 기술(AT)는 입력 요소에 집중하고 사용자에게 적절한 상황 정보를 제공하여 양식을 더 쉽게 탐색하고 이해할 수 있습니다.
  • 라벨링 : <label>htmlFor 속성을 포함하면 각 양식 필드에 설명 텍스트 라벨이 포함됩니다. 이는 컨텍스트를 제공하여 AT 지원을 향상시키고 사용자가 레이블을 클릭하여 해당 입력 필드에 집중할 수 있게 함으로써 유용성을 향상시킵니다.
  • 초점 윤곽선 : 초점이 맞춰졌을 때 윤곽선을 표시하도록 필드의 스타일이 적절하게 지정되었습니다. 이는 활성 요소를 시각적으로 표시하여 키보드와 화면 판독기, 사용자 모두 양식의 위치를 이해하는데 도움이 됩니다.

양식 유효성 검사

양식을 빈 상태로 요청을 보내면 에러가 발생합니다. 따라서 클라이언트에서 사전에 차단을 할 수 있습니다.

클라이언트 측 검증

클라이언트에서 양식의 유효성을 검사할 수 있는 몇 가지 방법이 있습니다. 가장 간단한 방법은 <input> , <select>양식 및 요소에 required 속성을 추가하여 브라우저에서 제공하는 양식 유효성 검사에 의존하는 것입니다 .

<input
    id="pending"
    name="status"
    type="radio"
    value="pending"
    className="h-4 w-4 cursor-pointer border-gray-300 bg-gray-100 text-gray-600 focus:ring-2"
    required
/>

서버 측 검증

서버에서 양식의 유효성을 검사할 수 있습니다.

  • 데이터를 데이터베이스로 보내기 전 데이터가 예상된 형식인지 확인할 수 있습니다.
  • 악의적인 사용자가 클라이언트 측 유효성 감사를 우회하는 위험을 줄입니다.
  • 유효한 데이터로 간주되는 정보에 대한 하나의 경로를 확보합니다.
const FormSchema = z.object({
    id: z.string(),
    customerId: z.string({invalid_type_error: 'Please select a customer.'}),
    // 타입에 오류가 있을 시에 에러를 발생 시킬 수 있습니다.
    amount: z.coerce.number().gt(0, {message: 'Please enter an amount greater than $0.'}),
    status: z.enum(['pending', 'paid'], 
		{invalid_type_error: 'Please select an invoice status.'}),
    date: z.string(),
});

const CreateInvoice = FormSchema.omit({id: true, date: true});

export type State = {
    errors?: {
        customerId?: string[];
        amount?: string[];
        status?: string[];
    };
    message?: string | null;
};
export async function createInvoice(prevState: State, formData: FormData) {
// prevState의 경우 hook에서 전달된 상태를 나타냅니다. 사용하지는 않지만 필수입니다.
    const validatedFields = CreateInvoice.safeParse({
        customerId: formData.get('customerId'),
        amount: formData.get('amount'),
        status: formData.get('status'),
    });

    // If form validation fails, return errors early. Otherwise, continue.
    if (!validatedFields.success) {
        // 에러가 있을 시에 조기 종료 해줍니다.
        return {
            // 에러 객체들만 꺼내와 줍니다. form 과 field의 에러들만 꺼내와줍니다.
            errors: validatedFields.error.flatten().fieldErrors,
            message: 'Missing Fields. Failed to Create Invoice.',
        };
    }

    // 에러가 없는 검증된 데이터들 입니다.
    const { customerId, amount, status } = validatedFields.data;

    const amountInCents = amount * 100;
    const date = new Date().toISOString().split('T')[0];

    try {
        await sql`
    INSERT INTO invoices (customer_id, amount, status, date)
    VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
  `;
    } catch (error) {
        return {
            message: 'Database Error: Failed to Create Invoice.',
        };
    }
    revalidatePath('/dashboard/invoices'); 
		// /dashboard/invoices 해당 경로의 유효성을 다시 검사합니다.
    redirect('/dashboard/invoices'); 
		// 해당 경로로 이동시킵니다.
}
const Component = () => {
	return <div className="relative">
	    <select
	        id="customer"
	        name="customerId"
	        className="peer block w-full cursor-pointer rounded-md border ..."
	        defaultValue={invoice.customer_id}
	        aria-describedby="customer-error"
	    >
					{// aria-describedby 속성으로 아래 div와 연결을 해줍니다.}
	        <option value="" disabled>
	            Select a customer
	        </option>
	        {customers.map((customer) => (
	            <option key={customer.id} value={customer.id}>
	                {customer.name}
	            </option>
	        ))}
	    </select>
	    <UserCircleIcon
	      className="pointer-events-none absolute left-3 top-1/2..."/>
	</div>
	<div id="customer-error" aria-live="polite" aria-atomic="true">
			{// aria-describedby 속성으로 연결이 됩니다.}
			{// aria-live="polite"를 통해서 변동된 사항을 사용자에게 알려줍니다.}
	    {state.errors?.customerId &&
	        state.errors.customerId.map((error: string) => (
	            <p className="mt-2 text-sm text-red-500" key={error}>
	                {error}
	            </p>
	        ))}
	</div>
}

15장 인증 추가

인증이란?

시스템이 사용자가 자신이 누구 인지를 확인하는 방법입니다.

웹 개발에서 인증과 권한 부여는 서로 다른 역할을 합니다.

  • 인증 사용자가 누구인지 확인하는 것입니다. 귀하는 사용자 이름과 비밀번호 등 귀하가 가지고 있는 정보를 통해 귀하의 신원을 증명하고 있습니다.
  • 승인 승인은 다음 단계입니다. 사용자의 신원이 확인되면 승인을 통해 사용자가 사용할 수 있는 애플리케이션 부분이 결정됩니다.

NextAuth.js

NextAuth를 사용합니다.애플리케이션에 인증을 추가하려면 세션관리, 로그인, 로그아웃, 기타 인증과 같이 복잡한 도구들을 관리해야하지만 NextAuth.js는 이러한 인증을 위한 통합 솔루션을 제공하여 프로세스를 단순화합니다.

NextAuth 설정

npm install next-auth@beta

Next.js 14와 호환되는 NextAuth.js 버전을 설치합니다 .

다음으로 애플리케이션에 대한 비밀 키를 생성합니다. 이 키는 쿠키를 암호화하여 사용자 세션의 보안을 보장하는 데 사용됩니다. 터미널에서 다음 명령을 실행하면 됩니다.

openssl rand -base64 32

그런 다음 생성된 키를 .env 파일 AUTH_SECRET 변수에 추가합니다.

AUTH_SECRET=your-secret-key

프로덕션에서 인증이 작동하려면 Vercel 프로젝트에서도 환경 변수를 업데이트해야 합니다. 이 가이드를 확인하세요Vercel에 환경 변수를 추가하는 방법에 대해 알아보세요.

페이지 옵션 추가

authConfig 객체를 내보내는 프로젝트 루트에 auth.config.ts 파일을 만듭니다. 이 객체에는 NextAuth.js에 대한 구성 옵션이 포함됩니다. 지금은 다음 pages 옵션만 포함됩니다.

/auth.config.ts

import type { NextAuthConfig } from 'next-auth';
 
export const authConfig = {
  pages: {
    signIn: '/login',
  },
};

pages옵션을 사용하여 사용자 정의 로그인, 로그아웃 및 오류 페이지에 대한 경로를 지정할 수 있습니다. 이는 필수는 아니지만 pagessignIn: '/login' 옵션을 추가하면 사용자는 NextAuth.js 기본 페이지가 아닌 사용자 정의 로그인 페이지로 리디렉션됩니다.

미들웨어로 경로 보호하기

다음으로 경로를 보호하는 로직을 추가합니다. 이렇게 하면 사용자가 로그인 하지않으면 대시보드 페이지에 액세스할 수 없습니다.

/auth.ts

import type { NextAuthConfig } from 'next-auth';
 
export const authConfig = {
  pages: {
    signIn: '/login',
  },
  callbacks: {
    authorized({ auth, request: { nextUrl } }) {
      const isLoggedIn = !!auth?.user;
			// 로그인이 되는 유저인지 확인합니다.
      const isOnDashboard = nextUrl.pathname.startsWith('/dashboard');
      if (isOnDashboard) {
        if (isLoggedIn) return true;
        return false; // Redirect unauthenticated users to login page
      } else if (isLoggedIn) {
        return Response.redirect(new URL('/dashboard', nextUrl));
      }
      return true;
    },
  },
  providers: [],
// 위 코드에서는 현재는 빈 배열로 providers를 설정하고 있습니다. 
// 여기에는 실제로 사용할 인증 제공자 (예: Google, Facebook 등)를 설정할 수 있습니다.
} satisfies NextAuthConfig;

callbacks 섹션에서는 authorized 콜백 함수를 정의하고 있습니다. 이 함수는 페이지에 액세스할 권한이 있는지를 확인합니다. 사용자가 대시보드 페이지(/dashboard)에 접근하려고 할 때 로그인 여부를 확인하고, 로그인되어 있지 않다면 로그인 페이지로 리디렉션합니다. 이미 로그인되어 있는 경우 대시보드 페이지로 리디렉션합니다.

/middleware.ts

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
 
export default NextAuth(authConfig).auth;
 
export const config = {
  // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
  matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
};

여기서는 객체로 NextAuth.js를 초기화하고 속성을 내보내고 있습니다.

이 작업에 미들웨어를 사용하면 미들웨어가 인증을 확인할 때까지 보호된 경로가 렌더링을 시작하지 않아 애플리케이션의 보안과 성능이 모두 향상된다는 이점이 있습니다.

비밀번호 해싱

비밀번호를 안전하게 저장하려면 비밀번호를 해시해야 합니다.

이전 seed.js파일에서 우리는 bcrypt비밀번호를 데이터베이스에 저장하기 전에 해시하는 데 사용했습니다. bcrypt 다시 사용하여 사용자가 입력한 비밀번호가 데이터베이스에 있는 비밀번호와 일치하는지 비교할 수 있습니다.

그러나 bcrypt Next.js 미들웨어에서는 사용할 수 없는 Node.js API를 사용합니다. 이문제를 해결하려면 bcrypt 를 가져오는 별도의 파일을 만들어야 합니다.

/auth.ts

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
 
export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
});

자격증명 공급자 추가하기

다음으로 NextAuth.js에 대한 providers 옵션을 추가해야 합니다. providers 는 Google 또는 GitHub와 같은 다양한 로그인 옵션을 나열하는 배열입니다. 이 과정에서는 자격 증명 공급자를 사용하는 데 중점을 둡니다.

자격 증명 공급자를 사용하면 사용자가 사용자 이름과 비밀번호를 사용하여 로그인할 수 있습니다.

/auth.ts

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import Credentials from 'next-auth/providers/credentials';
 
export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [Credentials({})],
});

💡 자격 증명 공급자를 사용하고 있지만 일반적으로 OAuth 와 같은 대체 공급자를 사용하는 것이 좋습니다. 전체 옵션 목록을 보려면 이메일공급자. NextAuth.js 문서를 참조하세요.

로그인 기능 추가하기

authorize이 함수를 사용하여 인증 논리를 처리 할 수 있습니다 . zod 라이브러리를 통하여 서버 작업과 마찬가지로 사용자가 데이터베이스에 존재하는지 확인하기 전에 이메일과 비밀번호의 유효성을 검사하는 데 사용할 수 있습니다 .

/auth.ts

import NextAuth from 'next-auth';
import { authConfig } from './auth.config';
import Credentials from 'next-auth/providers/credentials';
import { z } from 'zod';
 
export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      async authorize(credentials) {
        const parsedCredentials = z
          .object({ email: z.string().email(), password: z.string().min(6) })
          .safeParse(credentials);
      },
    }),
  ],
});

zod 라이브러리를 사용하여 1차로 자격 증명을 확인한 후에 데이터베이스에서 사용자를 쿼리하는 새 함수를 만듭니다.

/auth.ts

import type { User } from '@/app/lib/definitions';
import bcrypt from 'bcrypt';
 
async function getUser(email: string): Promise<User | undefined> {
  try {
    const user = await sql<User>`SELECT * FROM users WHERE email=${email}`;
    return user.rows[0];
  } catch (error) {
    console.error('Failed to fetch user:', error);
    throw new Error('Failed to fetch user.');
  }
}

export const { auth, signIn, signOut } = NextAuth({
  ...authConfig,
  providers: [
    Credentials({
      async authorize(credentials) {
        const parsedCredentials = z
          .object({ email: z.string().email(), password: z.string().min(6) })
          .safeParse(credentials);
 
        if (parsedCredentials.success) {
          const { email, password } = parsedCredentials.data;
          const user = await getUser(email);
          if (!user) return null; // 일치하는 유저가 없을시 내보냅니다.
					const passwordsMatch = await bcrypt.compare(password, user.password);
					 // 입력한 패스워드와 사용자의 패스워드가 일치하는지 확인합니다.
          if (passwordsMatch) return user;
					// 일치할 경우 사용자의 정보를 내보냅니다.
        }

				console.log('Invalid credentials');
        return null;
      },
    }),
  ],
});

로그인 양식 업데이트

인증 로직과 로그인 양식과 연결합니다.

import { signIn } from '@/auth';
 
// ...
 
export async function authenticate(
  prevState: string | undefined,
  formData: FormData,
) {
  try {
    await signIn('credentials', Object.fromEntries(formData));
  } catch (error) {
    if ((error as Error).message.includes('CredentialsSignin')) {
      return 'CredentialsSignin';
    }
    throw error;
  }
}
'use client';

export default function LoginForm() {
  const [state, dispatch] = useFormState(authenticate, undefined);
 
  return (
    <form action={dispatch} className="space-y-3">
      <div className="flex-1 rounded-lg bg-gray-50 px-6 pb-4 pt-8">
        <h1 className={`${lusitana.className} mb-3 text-2xl`}>
          Please log in to continue.
        </h1>
        <LoginButton />
        <div
          className="flex h-8 items-end space-x-1"
          aria-live="polite"
          aria-atomic="true"
        >
          {state === 'CredentialsSignin' && (
            <>
              <ExclamationCircleIcon className="h-5 w-5 text-red-500" />
              <p className="text-sm text-red-500">Invalid credentials</p>
            </>
          )}
        </div>
      </div>
    </form>
  );
}
 
function LoginButton() {
  const { pending } = useFormStatus();
 
  return (
    <Button className="mt-4 w-full" aria-disabled={pending}>
      Log in <ArrowRightIcon className="ml-auto h-5 w-5 text-gray-50" />
    </Button>
  );
}

로그아웃 추가하기

import { signOut } from '@/auth';
 
export default function SideNav() {
  return (
    <div className="flex h-full flex-col px-3 py-4 md:px-2">
      // ...
      <div className="flex grow flex-row justify-between space-x-2 md:flex-col md:space-x-0 md:space-y-2">
        <NavLinks />
        <div className="hidden h-auto w-full grow rounded-md bg-gray-50 md:block"></div>
        <form
          action={async () => {
            'use server';
            await signOut();
          }}
        >
          <button className="flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3">
            <PowerIcon className="w-6" />
            <div className="hidden md:block">Sign Out</div>
          </button>
        </form>
      </div>
    </div>
  );
}

16장 메타데이터 추가

메타데이터는 SEO 및 공유성에 매우 중요합니다.

메타데이터란?

웹 개발에서 메타데이터는 페이지를 방문하기 이전에 웹페이지에 대한 추가 세부정보를 제공합니다.  <head> 안에서 작동하므로 사용자에게 보이지는 않습니다. 이 숨겨진 정보는 웹페이지의 콘텐츠를 더 잘 이해해야 하는 검색 엔진 및 기타 시스템에 매우 중요합니다.

메타데이터가 왜 중요할까요?

메타데이터는 웹페이지의 SEO를 향상시키는 데 중요한 역할을 하여 검색 엔진과 소셜 미디어 플랫폼에서 웹페이지에 대한 접근성과 이해도를 높입니다. 검색 엔진이 웹 페이지를 효과적으로 색인화하여 검색 결과에서 순위를 높이는 데 도움이 됩니다. 또한 Open Graph와 같은 메타데이터는 소셜 미디어에서 공유 링크의 모양을 개선하여 사용자에게 콘텐츠를 더욱 매력적이고 유익하게 만듭니다.

메타데이터 유형

다양한 유형의 메타데이터가 있으며 각각 고유한 목적을 제공합니다. 몇 가지 일반적인 유형은 다음과 같습니다.

Title Metadata : 브라우저 탭에 표시되는 웹페이지의 제목을 담당합니다. 이는 검색 엔진이 웹페이지의 내용을 이해하는 데 도움이 되므로 SEO에 매우 중요합니다.

<title>Page Title</title>

설명 메타데이터 : 이 메타데이터는 웹페이지 콘텐츠에 대한 간략한 개요를 제공하며 검색 엔진 결과에 자주 표시됩니다.

<meta name="description" content="A brief description of the page content." />

키워드 메타데이터 : 이 메타데이터에는 웹페이지 콘텐츠와 관련된 키워드가 포함되어 있어 검색 엔진이 페이지를 색인화하는 데 도움이 됩니다.

<meta name="keywords" content="keyword1, keyword2, keyword3" />

오픈 그래프 메타데이터 : 이 메타데이터는 제목, 설명, 미리보기 이미지 등의 정보를 제공하여 소셜 미디어 플랫폼에서 공유할 때 웹페이지가 표시되는 방식을 향상시킵니다.

<meta property="og:title" content="Title Here" />
<meta property="og:description" content="Description Here" />
<meta property="og:image" content="image_url_here" />

파비콘 메타데이터 : 이 메타데이터는 파비콘(작은 아이콘)을 브라우저의 주소 표시줄이나 탭에 표시되는 웹페이지에 연결합니다.

<link rel="icon" href="path/to/favicon.ico" />

페이지 제목 및 설명

/app/layout.tsx

import { Metadata } from 'next';
 
export const metadata: Metadata = {
  title: 'Acme Dashboard',
  description: 'The official Next.js Course Dashboard, built with App Router.',
  metadataBase: new URL('https://next-learn-dashboard.vercel.sh'),
};
 
export default function Page() {
  // ...
}

Next.js는 애플리케이션에 제목과 메타데이터를 자동으로 추가합니다.

특정 페이지에 제목 추가하기

metadata페이지 자체에 개체를 추가하면 됩니다 . 중첩된 페이지의 메타데이터는 상위 페이지의 메타데이터보다 우선 적용됩니다.

예를 들어 /dashboard/invoices페이지에서 페이지 제목을 업데이트할 수 있습니다.

/app/dashboard/invoices/page.tsx

import { Metadata } from 'next';
 
export const metadata: Metadata = {
  title: 'Invoices | Acme Dashboard',
};

이것은 작동하지만 모든 페이지에서 애플리케이션 제목을 반복하고 있습니다. 회사 이름과 같은 사항이 변경되면 모든 페이지에서 이를 업데이트해야 합니다.

대신 title 객체 template의 필드를 사용하여 metadata 페이지 제목에 대한 템플릿을 정의할 수 있습니다. 이 템플릿에는 페이지 제목과 포함하려는 기타 정보가 포함될 수 있습니다.

루트 레이아웃에서 metadata템플릿을 포함하도록 객체를 업데이트합니다.

/app/layout.tsx

export const metadata: Metadata = {
  title: {
    template: '%s | Acme Dashboard',
    default: 'Acme Dashboard',
  },
  description: 'The official Next.js Learn Dashboard built with App Router.',
  metadataBase: new URL('https://next-learn-dashboard.vercel.sh'),
};

더 많은 metadata들을 추가할 수 있습니다.

더 많은 공부를 지속하려면 다음 문서를 참조하세요

공식문서

코드 보러가기

사이트 방문하기

Email: user@nextmail.com
Password: 123456

예제코드 출처 - Next.js 소개와 14 버전 변경사항 – React 인기 라이브러리

profile
개발자처럼 생각하기

0개의 댓글