티스토리 뷰

더보기

다시 돌아온 Next.js,,, 프로젝트 미팅에서 Next.js를 선택하기로 결정하여(정보 또는 레퍼런스 이슈 등등) 훑어보기만 했던 Next.js를 깊어 파보기로 했다..!!

1. 새 프로젝트 만들기

공식문서에서 주어진 명령어를 입력해 Next.js 프로젝트 폴더를 생성했다.

https://nextjs.org/learn/dashboard-app/getting-started#creating-a-new-project

 

Learn Next.js: Getting Started | Next.js

Create a new Next.js application using the dashboard starter example and explore the project.

nextjs.org

npx create-next-app@latest nextjs-dashboard --use-npm --example "https://github.com/vercel/next-learn/tree/main/dashboard/starter-example"

 

 

 

1-2. 프로젝트 탐색하기

💜 폴더 구조

생성된 프로젝트 폴더의 구조를 알아보자.

((개인적으로 개발 공부를 시작하고 나서 제일 헷갈렸던 게 폴더 구조였다.. 공식적으로 다루어주다니 정말 친절하구먼! ))

 

  • /app : 애플리케이션에 대한 모든 경로, 컴포넌트 및 로직이 포함되어 있음. 주로 작업하는 곳
  • /app/lib : 애플리케이션에서 사용되는 함수가 포함되어 있음.
    • 재사용이 가능한 유틸리티 함수, 데이터 가져오기 함수 등
  • /public : 이미지와 같은 애플리케이션의 모든 정적 자산을 포함
  • /scripts : 데이터베이스를 채우는 데 사용할 시드 스크립트가 포함되어 있음
  • Config Files : 애플리케이션 루트에 있는 next.config.js와 같은 config 파일. 대부분 create-next-app을 사용해 프로젝트를 생성할 때 사전 구성됨.

 

 

💜 Placeholder data

유저 인터페이스를 구축할 때 일부 placeholder data가 있으면 데이터베이스나 API를 아직 사용할 수 없는 경우 도움이 된다.

  • JSON 형식 또는 JavaScript 객체로 placeholder data를 사용
  • mockAPI와 같은 서드 파티 서비스 사용

app/lib/palceholder-data.js 파일의 각 JavaScript 객체는 데이터베이스의 테이블을 나타낸다.

const customers = [
  {
    id: '3958dc9e-712f-4377-85e9-fec4b6a6442a',
    name: 'Delba de Oliveira',
    email: 'delba@oliveira.com',
    image_url: '/customers/delba-de-oliveira.png',
  },
  {
    id: '3958dc9e-742f-4377-85e9-fec4b6a6442a',
    name: 'Lee Robinson',
    email: 'lee@robinson.com',
    image_url: '/customers/lee-robinson.png',
  },
  {
    id: '3958dc9e-737f-4377-85e9-fec4b6a6442a',
    name: 'Hector Simpson',
    email: 'hector@simpson.com',
    image_url: '/customers/hector-simpson.png',
  },
  ...
];

 

 

 

💜 타입스크립트

현재 프로젝트는 타입스크립트로 작성되었기 때문에 app/lib/definitions.ts에서는 데이터베이스에서 반환될 유형을 정의한다.

export type Customer = {
  id: string;
  name: string;
  email: string;
  image_url: string;
};

 

 

 


2. CSS 스타일링

2-1. Global styles

/app/ui 폴더 안의 global.css 파일에서 CSS 재설정 규칙, 링크와 같은 HTML 요소에 대한 사이트 전체 스타일을 추가할 수 있다.

일반적으로 global.css는 최상위 컴포넌트에 추가하는 것이 좋고 Next.js에서 최상위 컴포넌트인 root layout에서 추가한다.

import '@/app/ui/global.css';
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  //...
}

 

 

 

2-2. Tailwind

Tailwind에서는 클래스 이름을 추가해 요소의 스타일을 지정한다.

create-next-app을 사용해 프로젝트를 시작하면 Next.js가 Tailwind를 사용할 것인지 묻고 선택할 경우 Next.js가 자동으로 필요한 패키지를 설치하고 구성한다.

import AcmeLogo from '@/app/ui/acme-logo';
import { ArrowRightIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
 
export default function Page() {
  return (
    <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">
    // ...
  )
}

tailwind를 아직 사용해본 적은 없지만 적응하면 정말 편하다고 익히 들어 한 번은 사용해보고 싶었는데 이번 기회에 잘 써먹어봐야지!

 

 

2-3. CSS 모듈

CSS 모듈을 사용하면 고유한 클래스 이름을 자동으로 생성해 CSS 범위를 컴포넌트로 지정할 수 있어 스타일 충돌이 일어나지 않는다.

((프로젝트에서는 tailwind를 사용할 예정이므로 패스하겠다!))

 

 

2-4. clsx 라이브러리를 사용해 className 전환하기

상태나 다른 조건에 따라 요소의 스타일을 조건부로 지정해야 하는 경우 clsx를 사용한다.

 

📍 clsx

더보기

className을 쉽게 전환할 수 있는 라이브러리

clsx('foo', true && 'bar', 'baz'); // => 'foo bar baz'

clsx({ foo:true, bar:false, baz:isTrue() }); // => 'foo baz'

clsx({ foo:true }, { bar:false }, null, { '--foobar':'hello' }); // => 'foo --foobar'

 

⇩ 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',
        },
      )}
    >
    // ...
)}

 

 

 


3. 글꼴 및 이미지 최적화

3-1. 글꼴을 최적화하는 이유?

프로젝트에서 글꼴을 사용할 경우 글꼴 파일을 가져와 로드해야 하는 경우 성능에 영향을 미칠 수 있다.

 

글꼴을 사용하면 브라우저가 처음에 대체 글꼴이나 시스템 글꼴로 텍스트를 렌더링한 다음 로드된 후 사용자 지정 글꼴로 교체할 때 레이아웃이 변경이 발생한다. 이 교체로 인해 텍스트 크기, 간격 또는 레이아웃이 변경되고 그 주위의 요소가 이동될 수 있다.

 

Next.js는 next/font 모듈을 사용할 때 애플리케이션의 글꼴을 자동으로 최적화한다.

추가 네트워크 요청이 없도록 빌드 시 글꼴 파일을 다운로드하고 다른 정적 자산과 함께 호스팅한다.

따라서 사용자가 애플리케이션을 방문할 때 성능에 영향을 미치지 않는다.

 

 

3-2. 기본 글꼴 추가

/app/ui 폴더에 fonts.ts 파일을 생성해 애플리케이션 전체에서 사용될 글꼴을 유지한다.

 

기본 글꼴이 될 next/font/google 모듈에서 Inter 글꼴을 가져오고 로드하고 싶은 subsets(여기서는 'latin')를 열거한다.

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

 

/app/layout.tsx의 <body>요소에 글꼴을 추가한다.

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

 

 

+ 보조 글꼴 추가하기

Lusitana이라는 보조 글꼴을 추가해보자.

https://fonts.google.com/specimen/Lusitana?query=lusi

 

Lusitana - Google Fonts

Lusitana is inspired by the type found in the 1572 first edition of "The Lusiads", a Portuguese epic poem by Luís Vaz de Camões. This typeface is made for long

fonts.google.com

구글 폰트의 Lusitana를 검색해 사용할 수 있는 옵션을 확인을 한 후 두께와 subsets를 지정한다.

import { Lusitana } from 'next/font/google';

export const lusitana = Lusitana({
  weight: ['400', '700'],
  subsets: ['latin'],
});

 

/app/page.tsx의 <p>에 전달한다.

import { lusitana } from '@/app/ui/fonts';
 
export default function Page() {
  return (
    // ...
    <p
      className={`${lusitana.className} text-xl text-gray-800 md:text-3xl md:leading-normal`}
    >
    	...
    </p>
    // ...
  );
}

 

추가로 Lusitana를 사용하고 있는 <AcmLogo />컴포넌트의 주석을 해제한다.

 

글꼴 추가하기 완료!

 

 

 

3-3. 이미지를 최적화하는 이유?

Next.js는 최상위 /public 폴더 아래에서 이미지와 같은 정적 자산을 제공하고 /public 내부 파일은 애플리케이션에서 참조될 수 있다.

일반 HTML을 사용해 이미지를 추가하는 경우 이미지 최적화를 위해서 수행해야 할 일이 많다.

  • 이미지가 다양한 화면 크기에 반응하는가
  • 다양한 장치에 대한 이미지 크기를 지정했는가
  • 이미지가 로드될 때 레이아웃이 바뀌는가
  • 유저 뷰포트 외부에 있는 지연 로드 이미지인가

이러한 최적화를 수동으로 구현하는 대신 next/image 컴포넌트를 사용해 이미지를 최적화할 수 있다.

 

 

 

3-4. <Image> 컴포넌트

HTML <img>태그의 확장이며 자동 이미지 최적화 기능을 제공한다.

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

 

 

3-5. 데스크탑 hero image 추가하기

/public 폴더 안에 있는 이미지 파일 hero-desktop.png와 hero-mobile.png는 유저의 기기에 따라 표시된다.

/app/page.tsx 파일에서 next/image로부터 Image 컴포넌트를 불러오고 이미지를 전달한다.

import Image from 'next/image';
 
export default function Page() {
  return (
    // ...
    <div className="flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12">
      {/* Add Hero Images Here */}
      <Image
        src="/hero-desktop.png"
        width={1000}
        height={760}
        className="hidden md:block"
        alt="Screenshots of the dashboard project showing desktop version"
      />
    </div>
    //...
  );
}

레이아웃 변경을 방지하기 위해 width와 height를 설정한다. 이 경우 소스 이미지와 가로세로 비율이 동일해야 한다.

또한 모바일 화면의 DOM에서는 이미지를 제거해야하고 데스크톱 화면에는 이미지를 표시해야 하므로 hidden과 md(최소 너비: 768px) 중단점 접두사를 사용해 768px이상의 화면에서는 block으로 표시한다.

 

 

+ 모바일 hero image 추가하기

hero-mobile.png에 대한 또 다른 <Image> 컴포넌트를 추가한다.

  • width는 560px, height는 620px
  • 모바일 화면에는 표시되고 데스크탑에서는 숨겨져야함
import Image from 'next/image';
 
export default function Page() {
  return (
    // ...
    <div className="flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12">
      {/* Add Hero Images Here */}
      <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"
      />
    </div>
    //...
  );
}

 

 

 

 


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

4-1. 중첩된 라우팅

Next.js는 폴더가 중첩된 경로를 만드는 데 사용되는 파일 시스템 라우팅을 사용한다.

각 폴더는 URL 세그먼트에 맵핑되는 경로 세그먼트를 나타낸다.

 

layout.tsx 및 page.tsx 파일을 사용해 각 경로에 대해 별도의 UI를 만들 수 있다.

page.tsx는 React 컴포넌트를 내보내는 특수 Next.js 파일이며 경로에 액세스하려면 필요하다.

애플리케이션에 이미 존재하는 /app/page.tsx 파일은 / 경로와 연결된 홈 페이지이다.

 

 

4-2. 대시보드 페이지 만들기

Next.js에서 다양한 페이지를 생성하는 방법은 폴더를 사용해 새 경로 세그먼트를 생성하고 그 안에 page 파일을 추가하는 것이다.

/app 폴더 안에 dashboard 폴더를 생성한다. 그 다음 새로운 page.tsx 파일을 dashboard 폴더 안에 생성한다.

// /app/dashboard/page.tsx
export default function Page() {
  return <p>Dashboard Page</p>;
}

 

page 파일에 특별한 이름을 지정함으로써 Next.js는 UI 컴포넌트, 테스트 파일 및 기타 관련 코드를 경로와 함께 배치할 수 있도록 한다. 페이지 파일 내부의 콘텐츠에만 공개적으로 액세스할 수 있다. 예를 들어, /ui와 /lib 폴더는 라우트와 함께 /app 폴더 내에 같은 위치에 있다.

 

 

+ 대시보드 페이지 생성하기

  1. customers 페이지 : http://localhost:3000/dashboard/customers에서 액세스. <p>Customers Page</p> 요소 반환
  2. invoices 페이지 : http://localhost:3000/dashboard/invoices에서 액세스. <p>Invoices Page</p> 요소 반환

// /app/dashboard/customers/page.tsx
export default function Page() {
  return <p>Customers Page</p>;
}

// /app/dashboard/invoices/page.tsx
export default function Page() {
  return <p>Invoices Page</p>;
}

 

 

 

4-3. 대시보드 레이아웃 만들기

Next.js에서는 layout.tsx 파일을 사용해 여러 페이지 간에 공유되는 UI를 만들 수 있다.

 

/dashboard 폴더 안에 layout.tsx 파일을 추가한다.

import SideNav from '@/app/ui/dashboard/sidenav';
 
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <div className="flex h-screen flex-col md:flex-row md:overflow-hidden">
      <div className="w-full flex-none md:w-64">
        <SideNav />
      </div>
      <div className="flex-grow p-6 md:overflow-y-auto md:p-12">{children}</div>
    </div>
  );
}

 

<Layout /> 컴포넌트는 children prop을 받는다. 이 하위 항목은 페이지일 수도 또는 다른 레이아웃일 수도 있는데 여기서는 /dashboard 내부의 페이지는 자동으로 <Layout /> 내부에 중첩된다.

 

Next.js에서 레이아웃을 사용할 때의 이점 중 하나는 탐색 시 페이지 컴포넌트만 업데이트되고 레이아웃은 다시 렌더링되지 않는다는 것이다.

이를 부분 렌더링이라 한다.

 

 

4-4. 루트 레이아웃

/app/layout.tsx 파일의 루트 레이아웃

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

루트 레이아웃은 필수이며 여기에 추가하는 모든 UI는 애플리케이션의 모든 페이지에서 공유된다. 

<html> 및 <body> 태그를 수정하고 메타데이터를 추가할 수 있다.

 

 


5. 페이지 간 탐색

5-1. 탐색을 최적화하는 이유는?

페이지를 연결하려면 일반적으로 HTML <a> 요소를 사용한다.

<a>를 사용해 브라우저에서 페이지 사이를 탐색할 때 전체 페이지 새로 고침이 발생한다.

이를 방지하기 위해 최적화를 시행한다.

 

 

5-2. <Link> 컴포넌트

Next.js에서는 <Link /> 컴포넌트를 사용해 애플리케이션의 페이지 간을 연결할 수 있다.

/app/ui/dashboard/nav-links.tsx를 열고 next/link에서 Link 컴포넌트를 가져와 사용한다.

<a>태그를 <Link> 컴포넌트로 교체한다.

import Link from 'next/link';
 
// ...
 
export default function NavLinks() {
  return (
    <>
      {links.map((link) => {
        const LinkIcon = link.icon;
        return (
          <Link
            key={link.name}
            href={link.href}
            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"
          >
            <LinkIcon className="w-6" />
            <p className="hidden md:block">{link.name}</p>
          </Link>
        );
      })}
    </>
  );
}

 

전체 새로고침 없이 페이지 사이를 탐색할 수 있다!

 

💜 자동 코드 분할 및 프리패치

애플리케이션의 일부가 서버에서 렌더링되지만 전체 페이지 새로 고침이 없어 웹 앱처럼 느껴진다.

이는 탐색 경험을 향상시키기 위해 Next.js는 경로 세그먼트별로 애플리케이션을 자동으로 코드 분할을 하기 때문이다.

 

경로별로 코드를 분할한다는 것은 페이지가 격리된다는 의미로, 특정 페이지에서 오류가 발생하더라도 애플리케이션의 나머지 부분은 계속 작동한다.

또한 프로덕션 환경에서 <Link>컴포넌트가 브라우저의 뷰포트에 나타날 때마다 Next.js는 백그라운드에서 연결된 경로에 대한 코드를 자동으로 미리 가져온다. 유저가 링크를 클릭하면 대상 페이지의 코드가 이미 백그라운드에 로드되어 페이지 전환이 거의 즉각적으로 이루어진다.

 

 

 

5-3. Pattern: 활성 링크 표시

유저가 현재 어떤 페이지에 있는지 활성 링크를 표시하기 위해서는 URL에서 유저의 현재 경로를 가져와야 한다.

Next.js는 경로를 확인하고 이 패턴을 구현하는 데 사용할 수 있는 usePathname( ) 훅을 제공한다.

 

usePathname( )은 훅이기 때문에 nav-links.tsx를 클라이언트 컴포넌트로 전환해야 한다.

'use client';

import { usePathname } from 'next/navigation';
 
// ...

 

<NavLinks /> 컴포넌트 내부에서 pathname 변수를 선언해 경로를 할당한다.

export default function NavLinks() {
  const pathname = usePathname();
  // ...
}

 

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 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',
              {
                'bg-sky-100 text-blue-600': pathname === link.href,
              },
            )}
          >
            <LinkIcon className="w-6" />
            <p className="hidden md:block">{link.name}</p>
          </Link>
        );
      })}
    </>
  );
}

 

 

 


6. 데이터베이스 설정

(Github 레포지토리에 푸시한 후 Vercel로 프로젝트를 배포한 이후의 과정만 정리했다!)

 

6-1. Postgres 데이터베이스 만들기

데이터베이스를 설정하기 위해 대시보드에서 스토리지 탭을 누른다.

 

Postgres를 선택하고 데이터베이스 이름을 할당한다. 데이터베이스 지역이 기본 지역인 워싱턴으로 설정되어 있는데 데이터베이스를 동일한 지역에 배치하거나 애플리케이션 코드에 가깝게 배치하면 데이터 요청 대기 시간을 줄일 수 있다.

 

연결이 되면 .env.local 탭으로 이동해 코드를 복사한다.

 

.env.example 파일의 이름을 .env로 수정한 후 복사한 내용을 붙여넣는다.

📍 Github에 푸시할 때 데이터베이스 비밀이 노출되지 않도록 .gitignore에 .env가 있는지 확인한다.

 

마지막으로 Vervcel Postgres SDK를 설치하는 명령어를 실행한다.

npm i @vercel/postgres

 

 

 

6-2. 데이터베이스 시드

대시보드를 구축할 때 작업할 데이터를 확보하기 위해 일부 초기 데이터를 사용해 시드한다.

 

/scripts 폴더 seed.js 파일에는 invoices, customers, user, revenue 테이블을 생성하고 시드하기 위한 지침이 포함되어 있다.

script는 SQL을 사용해 테이블을 생성하고 만들어진 placeholder-data.js 파일의 데이터를 사용해 테이블을 채운다.

 

package.json 파일에 다음 코드를 추가한다.

"scripts": {
  ...,
  "seed": "node -r dotenv/config ./scripts/seed.js"
},

 

npm run seed 명령어를 실행하면 script가 실행 중임을 알려주는 메세지가 표시된다.

 

 

 

6-3. 데이터베이스 탐색

Vercel의 Data탭을 클릭하면 4개의 테이블이 나온다.

각 테이블을 선택하면 해당 레코드를 보고 항목이 placeholder-data.js 파일의 데이터와 일치한다.

 

 

 

6-4. 쿼리 실행

Query 탭으로 전환해 데이터베이스와 상호 작용할 수 있다. 여기서는 표준 SQL 명령을 지원한다.

Vercel 인터페이스에 SQL 코드를 붙여넣고 실행한다.

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

 

728x90