[Next] ๐Ÿ’ Next ์‚ฌ์šฉ๋ฒ• (v14)

TATAยท2024๋…„ 1์›” 20์ผ
0

Next.js

๋ชฉ๋ก ๋ณด๊ธฐ
1/1

โ–ท Next.js

Next.js๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง(SSR)์„ ์ง€์›ํ•œ๋‹ค.
๋•Œ๋ฌธ์— ์ดˆ๊ธฐ ๋ Œ๋”๋ง ์†๋„์™€ ๊ฒ€์ƒ‰ ์—”์ง„ ์ตœ์ ํ™”(SEO)๋ฅผ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

๋˜ํ•œ ํŽ˜์ด์ง€ ์ด๋™ ์‹œ ๊นœ๋นก์ž„์ด ์—†๋‹ค.(CSR)
๋นŒ๋“œ ์‹œ์ ์— ํŽ˜์ด์ง€๋ฅผ ์‚ฌ์ „์— ์ƒ์„ฑํ•˜์—ฌ ์ •์ ์ธ HTMLํŒŒ์ผ๋กœ ์ œ๊ณตํ•œ๋‹ค.(SSG)

์ฆ‰, Next.js๋Š” ๋น ๋ฅธ ๋ผ์šฐํŒ…๊ณผ ์ ์€ ๋„คํŠธ์›Œํฌ๋ฅผ ์ง€์›ํ•œ๋‹ค๋Š” ๊ฒƒ.


๐Ÿ’  ์„ค์น˜

# ์„ค์น˜
npx create-next-app@latest --typescript

# ์‹คํ–‰
npm run dev

# ํ”„๋ฆฌํ‹ฐ์–ด ์„ค์น˜
npm install --save-dev --save-exact prettier
npm install --save-dev eslint-config-prettier

๐Ÿ’  eslint / prettier

.eslintrc.json

{
  "extends": ["next/core-web-vitals", "prettier"]
}

.prettierrc.json

{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "es5",
}

๐Ÿ’  layout

์—ฌ๋Ÿฌ ํŽ˜์ด์ง€์—์„œ ๊ณต์œ ๋˜๋Š” ์ค‘์ฒฉ ๊ฐ€๋Šฅํ•œ ๋ ˆ์ด์•„์›ƒ์„ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

โž• loading ๋กœ๋”ฉํŽ˜์ด์ง€ - loading.tsx


๐Ÿ’  not-found

not-found.tsx

/* app/profile/not-found.tsx */
const ProfileNotFound = () => {
  return <div>Not Found</div>
}

export default ProfileNotFound;

next/navigation

/* app/profile/page.tsx */
import { notFound } from "next/navigation"

const ProfilePage = ({ params: { params: { id: number } } }) => {
  if (params.id > 100) notFound();
  
  return <Profile />
}

export default ProfilePage;

๐Ÿ’  error

error.tsx

/* app/profile/error.tsx */
"use client"

interface Props {
  error: Error;
  reset: () => void;
}

const ProfileErrorPage = ({ error, reset } : Props) => {
  console.log('error:', error)
  
  return (
  	<div>์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.</div>
    <button onClick={() => reset()}>reset</button>
  )
}

export default ProfileErrorPage;

(app/layout.tsx์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜๋Š” global-error.tsx ํŒŒ์ผ์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.)


๐Ÿ’  Routing

Next.js๋Š” appํด๋”๋ฅผ ๊ธฐ์ค€์œผ๋กœ ๋ผ์šฐํŒ…๋œ๋‹ค.
(src/app/page.tsx๊ฐ€ "/" ๋ฉ”์ธ ๊ฒฝ๋กœ์ž„)

/ โ†’ app/page.tsx
/about โ†’ app/about/page.tsx


๊ฒฝ๋กœ ์ด๋™์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

import Link from 'next/link'
 
export default function Page() {
  return <Link href="/dashboard">Dashboard</Link>
}

๐Ÿ’  useRouter

router.push(href): ๊ฒฝ๋กœ ์ด๋™ (๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ก์— ์ƒˆ ํ•ญ๋ชฉ ์ถ”๊ฐ€)
router.replace(href): ๊ฒฝ๋กœ ์ด๋™ (๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ก์— ์ƒˆ ํ•ญ๋ชฉ ์ถ”๊ฐ€ ์•ˆํ•จ)
router.refresh(): ํ˜„์žฌ ๊ฒฝ๋กœ๋ฅผ ์ƒˆ๋กœ ๊ณ ์นจ
router.prefetch(href): ๋” ๋น ๋ฅธ ํด๋ผ์ด์–ธํŠธ ์ธก ์ „ํ™˜์„ ์œ„ํ•ด ์ œ๊ณต๋œ ๊ฒฝ๋กœ๋ฅผ ๋ฏธ๋ฆฌ ๊ฐ€์ ธ์˜ด
router.back(): ์ด์ „ ๊ฒฝ๋กœ๋กœ ์ด๋™
router.forward(): ๋‹ค์Œ ํŽ˜์ด์ง€๋กœ ์ด๋™

/* useRouter ์‚ฌ์šฉ ์˜ˆ์‹œ */
import { useRouter } from 'next/navigation'
 
export default function Page() {
  const router = useRouter();
  const { id } = router.query; // useParams์™€ ๊ฐ™์€ ์—ญํ• ์„ ํ•จ.
 
  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  )
}

๐Ÿ’  Catch All Segments

ํ•œ ๊ฐœ ์ด์ƒ์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ์žˆ์„ ๋•Œ ๋ Œ๋”๋ง ๋œ๋‹ค.

/profile/[...username]/page.tsx

๋งค๊ฐœ๋ณ€์ˆ˜ 1๊ฐœ - /profile/tata
๋งค๊ฐœ๋ณ€์ˆ˜ 2๊ฐœ - /profile/tata/post1
๋งค๊ฐœ๋ณ€์ˆ˜ 3๊ฐœ - /profile/tata/setting/security

(/profile๋„ ๋ Œ๋”๋ง ๋˜๊ฒŒ ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ๋Œ€๊ด„ํ˜ธ ๋‘์Œ์œผ๋กœ)
/profile/[[...username]]/page.tsx


๐Ÿ’  generateStaticParams

12๋ฒ„์ „์—์„œ๋Š” getStaticPaths์™€ getStaticProps์„ ์‚ฌ์šฉํ•ด์„œ ๋™์  ๊ฒฝ๋กœ๋ฅผ ๋งŒ๋“ค์—ˆ๋‹ค๋ฉด, 13๋ฒ„์ „์˜ app ๋ผ์šฐํ„ฐ์—์„œ๋Š” generateStaticParams์„ ์‚ฌ์šฉํ•˜์—ฌ ๋™์  ๊ฒฝ๋กœ๋ฅผ ๋งŒ๋“ค๋ฉด ๋œ๋‹ค.

app ๋ผ์šฐํ„ฐ์—์„œ๋Š” getStaticProps๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์œผ๋‹ˆ params์— ๋งž์ถฐ์„œ ๋ฐ์ดํ„ฐ๋ฅผ fetch ํ•ด์˜ค๋ฉด ๋จ.

/* app/blog/[slug]/page.js */
export async function generateStaticParams() {
  const posts = await fetch('https://.../posts').then((res) => res.json())
 
  return posts.map((post) => ({
    slug: post.slug,
  }))
}

export default function Page({ params }) {
  const { slug } = params
  ...
}

๋Œ€์‹ , fetch ์˜ต์…˜ ๊ฐ์ฒด๋ฅผ ํ™•์žฅํ•ด์„œ ๊ฐ ์š”์ฒญ์— ๋Œ€ํ•œ ์บ์‹ฑ ๋ฐ ์žฌ๊ฒ€์ฆ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

fetch ์˜ต์…˜
{ next: { revalidate: 3600 } : ์ผ์ • ๊ฐ„๊ฒฉ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ๊ฒ€์ฆ(revalidate๋กœ ์ •ํ•ด์ค€ ์‹œ๊ฐ„ ์•ˆ์— ์ƒˆ๋กœ์šด ์š”์ฒญ์ด ์˜ค๋ฉด ๋ฐ์ดํ„ฐ ์—…๋ฐ์ดํŠธ)
{ cache: 'force-cache' } : ์บ์‹œ๋ฅผ ๊ฐ•์ œ, ์ •์ ์ธ html์„ ๋ฏธ๋ฆฌ ์ƒ์„ฑ (ssg...๊ธฐ์กด์˜ getStaticProps)
{ cache: 'no-store' } : ์š”์ฒญ์ด ์žˆ์„ ๋•Œ๋งˆ๋‹ค ๋ฐ์ดํ„ฐ๋ฅผ ๋™์ ์œผ๋กœ ๊ฐ€์ ธ์˜จ๋‹ค.(ssr...๊ธฐ์กด์˜ getServerSideProps)


๐Ÿ’  dynamic import

์ปดํฌ๋„ŒํŠธ๋ฅผ ๋™์ ์œผ๋กœ ๊ฐ€์ ธ์™€์„œ ํ•„์š”ํ•œ ์‹œ์ ์— ๋ Œ๋”๋งํ•œ๋‹ค.

ssr: ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง(SSR)์—์„œ ์ œ์™ธ ํ•  ์ˆ˜ ์žˆ๋Š” ์˜ต์…˜

import dynamic from 'next/dynamic';

// ๋™์ ์œผ๋กœ ๊ฐ€์ ธ์˜ฌ ์ปดํฌ๋„ŒํŠธ
const DynamicComponent = dynamic(() => import('../components/DynamicComponent'), {
  loading: () => <div>...loading</div>, // ๋กœ๋”ฉ
  ssr: false, // ํ•ด๋‹น ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง์—์„œ ์ œ์™ธ
});

๐Ÿ’  next/image

์ด๋ฏธ์ง€๋ฅผ webp type์œผ๋กœ ์šฉ๋Ÿ‰ ์ตœ์ ํ™”๋ฅผ ํ•ด์ค€๋‹ค.

import Image from "next/image";
import example from "../../public/example.jpg";

<figure>
  <figcaption>์˜ˆ์‹œ ์ด๋ฏธ์ง€</figcaption>
  <Image
    src={example}
    alt="์˜ˆ์‹œ ์ด๋ฏธ์ง€"
    width={500}
    height={500}
    quality={100} {/* ์ด๋ฏธ์ง€๋ฅผ ์–ผ๋งˆ๋‚˜ ์••์ถ•ํ• ๊ฑด์ง€. ๊ธฐ๋ณธ๊ฐ’์€ 75์ž„. */}
    placeholder="blur" {/* ์ด๋ฏธ์ง€ ๋กœ๋”ฉ ์‹œ ๋ธ”๋Ÿฌ ์ฒ˜๋ฆฌ๋œ ์ด๋ฏธ์ง€๋ฅผ ๋ณด์—ฌ์คŒ */}
    blurDataURL="" {/* ๋กœ์ปฌ ์ด๋ฏธ์ง€๊ฐ€ ์•„๋‹ˆ๋ฉด์„œ placeholder์†์„ฑ์„ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ์ถ”๊ฐ€ํ•ด์•ผ ํ•จ */}
  />
</figure>

๋ฐฐํฌ ํ™˜๊ฒฝ์—์„œ ์™ธ๋ถ€ ๊ฒฝ๋กœ ์ด๋ฏธ์ง€๊ฐ€ ์•ˆ๋œฐ ๋•Œ ์•„๋ž˜ ์˜ต์…˜ ์ถ”๊ฐ€
(Next Image๋Š” ์ž๋™์œผ๋กœ ์ด๋ฏธ์ง€ ์ตœ์ ํ™”๋ฅผ ์ง€์›ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฏธ์ง€ ๊ฒฝ๋กœ๊ฐ€ ๋‹ฌ๋ผ์ง. ๋ฐฐํฌํ™˜๊ฒฝ์—์„œ๋Š” ์ด๋ฏธ์ง€๊ฐ€ ๊นจ์ง)

unoptimized={true} // ์ตœ์ ํ™”๊ฐ€ ์ด๋ฃจ์–ด์ง€์ง€ ์•Š๋Š”๋‹ค

์ด๋ฏธ์ง€๊ฐ€ ์™ธ๋ถ€ ๊ฒฝ๋กœ์ผ ๊ฒฝ์šฐ ๋„๋ฉ”์ธ์„ ์ถ”๊ฐ€

/* next.config.ts */
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: '**.example.com', // ์™ธ๋ถ€ ์ด๋ฏธ์ง€ ๋„๋ฉ”์ธ ์ถ”๊ฐ€
        port: '',
      },
    ],
  },
}

๐Ÿ’  next/font

next/font๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋ธŒ๋ผ์šฐ์ €์—์„œ Google๋กœ ํฐํŠธ ์š”์ฒญ์„ ํ•˜์ง€ ์•Š์•„๋„ ๋œ๋‹ค.

src/font/font.ts

/* src/font/font.ts */
// ์›ํ•˜๋Š” ๊ตฌ๊ธ€ ํฐํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ
import { Inter, DynaPuff } from "next/font/google";

export const inter = Inter({ subsets: ["latin"] });

export const carlito = DynaPuff({
  subsets: ["latin"], // ์˜์–ด๋ฅผ ์ง€์›
  weight: ["400", "500", "600", "700"], // ๋ฐฐ์—ด ํ˜•ํƒœ๋กœ ์—ฌ๋Ÿฌ ๋‘๊ป˜๋ฅผ ์ง€์ •
  variable: "--carlito", // css์—์„œ var(--carlito)๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
});

src/app/layout.tsx

/* src/app/layout.tsx */
import "./globals.css";
import { carlito } from "@/font/font";

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      {/* ํฐํŠธ์„ค์ •์ด๋ฆ„.className์„ ๋„ฃ์œผ๋ฉด ์ ์šฉ ์™„๋ฃŒ */}
      <body className={carlito.className}>{children}</body>
    </html>
  );
}

๐Ÿ’  CSS Modules

ํŒŒ์ผ๋ช…์€ ํŒŒ์ผ์ด๋ฆ„.module.css๋กœ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค.

module.css๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ํด๋ž˜์Šค ๊ฐ„์˜ ์ถฉ๋Œ์„ ํ”ผํ•  ์ˆ˜ ์žˆ๋‹ค.


๐Ÿ’  styled-components

app/layout.tsx

/* app/layout.tsx */
'use client';
import Layout from '../components/Layout/Layout';
import StyledComponentsRegistry from '../lib/registry'; // โญ๏ธ์ถ”๊ฐ€

const notoSans = Noto_Sans({ weight: '400', subsets: ['latin'] });

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ko">
      <body>
        <StyledComponentsRegistry> // โญ๏ธ์ถ”๊ฐ€
          <Layout>{children}</Layout>
        </StyledComponentsRegistry> // โญ๏ธ์ถ”๊ฐ€
      </body>
    </html>
  );
}

lib/registry ์ฐธ๊ณ 

lib/registry.tsx

'use client';

import React, { useState } from 'react';
import { useServerInsertedHTML } from 'next/navigation';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';

export default function StyledComponentsRegistry({
  children,
}: {
  children: React.ReactNode;
}) {
  // Only create stylesheet once with lazy initial state
  // x-ref: https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
  const [styledComponentsStyleSheet] = useState(() => new ServerStyleSheet());

  useServerInsertedHTML(() => {
    const styles = styledComponentsStyleSheet.getStyleElement();
    styledComponentsStyleSheet.instance.clearTag();
    return <>{styles}</>;
  });

  if (typeof window !== 'undefined') return <>{children}</>;

  return (
    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
      {children}
    </StyleSheetManager>
  );
}

next.config.js

/* next.config.js */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,
  compiler: {
    styledComponents: true,
  },
};
module.exports = nextConfig;
...

๐Ÿ’  svg ์ปดํฌ๋„ŒํŠธ

svgr

# ์„ค์น˜
npm install @svgr/webpack

next.config.js

/* next.config.js */
/** @type {import('next').NextConfig} */
const nextConfig = {
  webpack: (config) => {
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    });
    return config;
  },
};

module.exports = nextConfig;

svgํŒŒ์ผ๋“ค์„ publicํด๋”์— ๋„ฃ์–ด์ฃผ๊ณ  ์‚ฌ์šฉ

import MoonBlue from '../../../public/moonBlue.svg';

<MoonBlue /> // โญ๏ธ ์ปดํฌ๋„ŒํŠธ์ฒ˜๋Ÿผ ์‚ฌ์šฉ ๊ฐ€๋Šฅ

๐Ÿ’  Suspense

suspense

import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
 
export default function Posts() {
  return (
    <section>
      <Suspense fallback={<p>Loading feed...</p>}>
        <PostFeed />
      </Suspense>
      <Suspense fallback={<p>Loading weather...</p>}>
        <Weather />
      </Suspense>
    </section>
  )
}

๐Ÿ’  CORS ์˜ค๋ฅ˜

rewrites

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: false,
  images: {
    domains: ['...'],
  },
  async rewrites() {  // โญ๏ธ ์ถ”๊ฐ€
    return [
      {
        source: '/api/:path*',  // fetchํ•˜๋Š” ๊ณณ์—์„œ '/api'๋ฅผ ์•ž์— ๋ถ™์ด๋ฉด ๊ฒฝ๋กœ๋ฅผ destination์œผ๋กœ ์žก์•„์คŒ
        destination: 'http://localhost:3001/:path*',
      },
    ];
  },
};

module.exports = nextConfig;



๐Ÿ‘‰ (๊ณต์‹๋ฌธ์„œ)generateMetadata
๐Ÿ‘‰ (๊ณต์‹๋ฌธ์„œ)generateStaticParams
๐Ÿ‘‰ (๊ณต์‹๋ฌธ์„œ)RevalidatingData
๐Ÿ‘‰ (๊ณต์‹๋ฌธ์„œ)api ๋งŒ๋“ค๊ธฐ
๐Ÿงฟ SSR, CSR, SSG

profile
๐Ÿพ

0๊ฐœ์˜ ๋Œ“๊ธ€