โท Next.js
ย
Next.js๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง(SSR)์ ์ง์ํ๋ค.
๋๋ฌธ์ ์ด๊ธฐ ๋ ๋๋ง ์๋์ ๊ฒ์ ์์ง ์ต์ ํ(SEO)๋ฅผ ๊ฐ์ ํ ์ ์๋ค.
ย
๋ํ ํ์ด์ง ์ด๋ ์ ๊น๋นก์์ด ์๋ค.(CSR)
๋น๋ ์์ ์ ํ์ด์ง๋ฅผ ์ฌ์ ์ ์์ฑํ์ฌ ์ ์ ์ธ HTMLํ์ผ๋ก ์ ๊ณตํ๋ค.(SSG)
์ ์ ํ์ด์ง๋ฅผ ๋น๋ ํ์๋ ์ผ์ ์ฃผ๊ธฐ๋ง๋ค ์๋์ผ๋ก ๊ฐฑ์ ํด ์ต์ ๋ฐ์ดํฐ๋ฅผ ์ ๊ณตํ๋ค.(ISR)
ย
์ฆ, Next.js๋ ๋น ๋ฅธ ๋ผ์ฐํ
๊ณผ ์ต์ํ์ ๋คํธ์ํฌ ์์ฒญ์ ์ง์ํ๋ค๋ ๊ฒ.
ย
ย
โท Next.js ๋ ๋๋ง ์ ์ด
ย
Next.js App Router์ ํ์ด์ง๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋ฒ์์ ๋ ๋๋ง(SSR)๋์ง๋ง
dynamic์ด๋ revalidate ์ค์ ์ ํตํด ์ ์ (SSG) ๋๋ ISR๋ก ๋์์ ๋ฐ๊ฟ ์ ์๋ค.ย
๐ ย SSG
ย
ํ์ด์ง๋ฅผ ํญ์ ์ ์ (SSG)์ผ๋ก ๋น๋ํ๋๋ก ์ง์
ย
/* page.tsx */ const dynamic = 'force-static';
ย
ย
๐ ย ISR
ย
600์ด(10๋ถ)๋ง๋ค ์ ์ ํ์ด์ง๋ฅผ ์๋ ๊ฐฑ์
ย
/* page.tsx */ const revalidate = 600;
ย
ย
๐ ย generateStaticParams
ย
generateStaticParams: ๋น๋ ์์ ์ ๋์ ๋ผ์ฐํธ์ฉ ์ ์ ๊ฒฝ๋ก(slug)๋ฅผ ๋ฏธ๋ฆฌ ์์ฑย
// ๋น๋ ํ์์ ๋ชจ๋ slug๋ฅผ ์์ฑ async function generateStaticParams() { const ids = await getIds({ limit: 20 }); // string[] return ids.map((id) => ({ slug: id })); } // (์ ํ) ๋ฏธ๋ฆฌ ์์ฑ๋์ง ์์ slug ํ์ฉ ์ฌ๋ถ // const dynamicParams = false; // ๋น๋๋ ๊ฒ๋ง ํ์ฉ(404) const dynamicParams = true; // ๋น๋ ์๋ ๊ฒ๋ ISR๋ก ์์ฑ ํ์ฉ
ย
ย
โท Next.js ์์ํ๊ธฐ
ย
๐ ์ค์น
# ์ค์น npx create-next-app@latest # ์คํ pnpm dev
ย
ย
๐ layout
ย
์ฌ๋ฌ ํ์ด์ง์์ ๊ณต์ ๋๋ ์ค์ฒฉ ๊ฐ๋ฅํ ๋ ์ด์์์ ๋ง๋ค ์ ์๋ค.

ย
โ loading ๋ก๋ฉํ์ด์ง - loading.tsx
ย
ย
๐ template
ย
ย
๐ 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 ย
ย
๐ Link
ย
๊ฒฝ๋ก ์ด๋์ด ๊ฐ๋ฅํ๋ค.
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ย
ย
๐ fetch ์ต์
ย
{ next: { revalidate: 3600 } : ์ผ์ ๊ฐ๊ฒฉ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฌ๊ฒ์ฆ(ISR...ํ์ด์ง๋ฅผ ์ ์ ์ผ๋ก ์์ฑํ๋, ์ต์ด ์์ฑ ํ 3600์ด(1์๊ฐ)์ด ์ง๋๋ฉด, ๋ค์ ์์ฒญ ์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ํ์ด์ง๋ฅผ ๋ค์ ์์ฑ)
{ cache: 'force-cache' } : ์บ์๋ฅผ ๊ฐ์ , ์ ์ ์ธ html์ ๋ฏธ๋ฆฌ ์์ฑ (ssg...๊ธฐ์กด์ getStaticProps)
{ cache: 'no-store' } : ์์ฒญ์ด ์์ ๋๋ง๋ค ๋ฐ์ดํฐ๋ฅผ ๋์ ์ผ๋ก ๊ฐ์ ธ์จ๋ค.(ssr...๊ธฐ์กด์ getServerSideProps)
{ cache: { tag: ['msg'] } } : fetch ๊ฒฐ๊ณผ์ ์บ์ ํ๊ทธ๋ฅผ ๋ถ์ด๋ ๊ธฐ๋ฅ์ด๊ณ , ๋์ค์ revalidateTag('msg')๋ฅผ ํธ์ถํ๋ฉด ์ด ํ๊ทธ๋ฅผ ๊ฐ์ง ์บ์๊ฐ ๋ฌดํจํ๋์ด ์๋ก ์์ฒญ๋๋ค.ย
ย
๐ 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
<Image sizes="(max-width: 768px) 100%, (min-width: 768px) 1920px 1000px" src={section1Bg} className="object-cover" alt="๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง" placeholder="blur" />
ย
unoptimized={true} // ์ต์ ํ๊ฐ ์ด๋ฃจ์ด์ง์ง ์๋๋ค
ย
๐ next/font
ย
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> ); }
ย
(( ์ถ๊ฐ ))
์ง์ ๋ค์ด๋ก๋ํ ttf ํ์ผ์ ์ ์ฉํ๋ ๋ฐฉ๋ฒ
import localFont from 'next/font/local'; const pretendard = localFont({ src: '../../public/font/PretendardVariable.ttf', display: 'swap', weight: '45 920', variable: '--font-pretendard', }); ... <html lang="en"> <body className={pretendard.className}>{children}</body> </html>
/* tailwind.config.css */ extend: { fontFamily: { pretendard: ['var(--font-pretendard)'], }, },
ย
ย
๐ CSS Modules
ย
ํ์ผ๋ช
์
ํ์ผ์ด๋ฆ.module.css๋ก ์ฌ์ฉํด์ผ ํ๋ค.module.css๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ํด๋์ค ๊ฐ์ ์ถฉ๋์ ํผํ ์ ์๋ค.
.nav-wrap { display: flex; }
import styles from './css/nav.module.css'; <div className={styles['nav-wrap']} />
ย
ย
๐ svg ์ปดํฌ๋ํธ
ย
# ์ค์น npm install @svgr/webpack
ย
next.config.js
/* next.config.js */ /** @type {import('next').NextConfig} */ webpack(config) { const fileLoaderRule = config.module.rules.find((rule) => rule.test?.test?.('.svg')); config.module.rules.push( { ...fileLoaderRule, test: /\\.svg$/i, resourceQuery: /url/, }, { test: /\\.svg$/i, issuer: fileLoaderRule.issuer, resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, use: ['@svgr/webpack'], }, ); fileLoaderRule.exclude = /\\.svg$/i; return config; }
ย
svgํ์ผ๋ค์ publicํด๋์ ๋ฃ์ด์ฃผ๊ณ ์ฌ์ฉ
import MoonBlue from '@/public/moonBlue.svg'; <MoonBlue /> // โญ๏ธ ์ปดํฌ๋ํธ์ฒ๋ผ ์ฌ์ฉ ๊ฐ๋ฅ
ย
ย
๐ 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 ์ค๋ฅ
ย
/** @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;
ย
ย
๐ metadata, viewport
ย
import type { Metadata, Viewport } from 'next'; export const metadata: Metadata = { manifest: '/manifest.json', title: 'ํ์ดํ', description: '์ค๋ช ', icons: { icon: '/favicon.ico', }, openGraph: { images: ['์ด๋ฏธ์ง ๊ฒฝ๋ก'] }, verification: { google: process.env.NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION, other: { 'naver-site-verification': `${process.env.NEXT_PUBLIC_NAVER_SITE_VERIFICATION}`, } }, }; // ๋ชจ๋ฐ์ผ์์ ์์ผ๋ก ํ๋ฉด ํ๋ ์๋๊ฒ export const viewport: Viewport = { width: 'device-width', initialScale: 1, maximumScale: 1, userScalable: false, };
ย
favicon ์ต์ ํ
favicon์ผ๋ก ์ธ ์ด๋ฏธ์ง 512px 512px ํ๋๋ฅผ ์ค๋น
์๋ ์ฌ์ดํธ์์ favicon ์ด๋ฏธ์ง ์์ฑ
export const metadata: Metadata = { title: 'Title', metadataBase: new URL('...'), description: 'description', openGraph: { images: ['/images/openGraph.jpg'], }, icons: { icon: [ { url: '/favicon/favicon.ico', sizes: 'any' }, { url: '/favicon/favicon-32x32.png', type: 'image/png', sizes: '32x32' }, { url: '/favicon/favicon.ico', type: 'image/x-icon' }, ], apple: [ { url: '/favicon/apple-touch-icon.png', sizes: '180x180' }, ], }, manifest: '/favicon/site.webmanifest', verification: { google: '๊ตฌ๊ธํค', other: { 'naver-site-verification': '๋ค์ด๋ฒํค', }, }, keywords: ['ํ๊ทธ1', 'ํ๊ทธ2'], robots: 'index, follow', };
ย
site.webmanifest๋ ์๋์ ๊ฐ์ด ์๊ฒผ๋ค.
{ "name": "Your App", "short_name": "YourApp", "icons": [ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" }
ย
ย
๐ generateMetadata
ย
interface Params { params: Promise<{ projectId: string }>; } // ๋์ ์ผ๋ก ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์์ฑํ๋ ํจ์ export async function generateMetadata({ params }: Params): Promise<Metadata> { const { projectId } = await params; return { title: `PROJECTS - ${projectId}`, description: `project - ${projectId}`, }; }
ย
ย
๐ redirect
ย
next.config.mjs
const nextConfig = { async redirects() { return [ { source: "/", destination: "/app", // ์ฒซ ํ์ด์ง๊ฐ '/app' ๊ฒฝ๋ก์์ ์์ํจ permanent: true, }, ]; }, };
ย
ย
๐ middleware ๋ฏธ๋ค์จ์ด
ย
middleware.ts
/* src/middleware.ts */ import { NextRequest, NextResponse } from 'next/server'; export function middleware(request: NextRequest) { const requestHeaders = new Headers(request.headers); requestHeaders.set('x-pathname', request.nextUrl.pathname); requestHeaders.set('x-test', 'test'); return NextResponse.next({ request: { headers: requestHeaders, }, }); }
ย
layout.tsx
/* src/app/layout.tsx */ import { headers } from 'next/headers'; export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { const headersList = headers(); const headerPathname = headersList.get('x-pathname') || ''; const test = headersList.get('x-test') || ''; console.log('headersList:', headersList); console.log('headerPathname:', headerPathname); console.log('test:', test); ...
ย
ย
๐ sitemap
ย
ย
๐ robots
ย