โท Next.js
ย
Next.js๋ ๊ธฐ๋ณธ์ ์ผ๋ก ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง(SSR)์ ์ง์ํ๋ค.
๋๋ฌธ์ ์ด๊ธฐ ๋ ๋๋ง ์๋์ ๊ฒ์ ์์ง ์ต์ ํ(SEO)๋ฅผ ๊ฐ์ ํ ์ ์๋ค.
๋ํ ํ์ด์ง ์ด๋ ์ ๊น๋นก์์ด ์๋ค.(CSR)
๋น๋ ์์ ์ ํ์ด์ง๋ฅผ ์ฌ์ ์ ์์ฑํ์ฌ ์ ์ ์ธ HTMLํ์ผ๋ก ์ ๊ณตํ๋ค.(SSG)
์ฆ, Next.js๋ ๋น ๋ฅธ ๋ผ์ฐํ
๊ณผ ์ ์ ๋คํธ์ํฌ๋ฅผ ์ง์ํ๋ค๋ ๊ฒ.
ย
ย
๐ ์ค์น
# ์ค์น npx create-next-app@latest # ์คํ npm run 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
ย
ย
๐ 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 }
: ์ผ์ ๊ฐ๊ฒฉ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฌ๊ฒ์ฆ(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" />
ย
๋ฐฐํฌ ํ๊ฒฝ์์ ์ธ๋ถ ๊ฒฝ๋ก ์ด๋ฏธ์ง๊ฐ ์๋ฐ ๋ ์๋ ์ต์
์ถ๊ฐ
(Next Image๋ ์๋์ผ๋ก ์ด๋ฏธ์ง ์ต์ ํ๋ฅผ ์ง์ํ๊ธฐ ๋๋ฌธ์ ์ด๋ฏธ์ง ๊ฒฝ๋ก๊ฐ ๋ฌ๋ผ์ง. ๋ฐฐํฌํ๊ฒฝ์์๋ ์ด๋ฏธ์ง๊ฐ ๊นจ์ง)
unoptimized={true} // ์ต์ ํ๊ฐ ์ด๋ฃจ์ด์ง์ง ์๋๋ค
ย
๐ 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> ); }
ย
(( ์ถ๊ฐ ))
์ง์ ๋ค์ด๋ก๋ํ 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: ['์ด๋ฏธ์ง ๊ฒฝ๋ก'] // /images/openGraph.jpg }, 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: 'Lyway', metadataBase: new URL('<https://www.lyway.ai>'), description: 'Educational AI for All', 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.svg', type: 'image/svg+xml' }, ], 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}`, }; }
ย
ย
๐ ์ฒซ ์์ํ์ด์ง ๋ณ๊ฒฝํ๊ธฐ
ย
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); ...
ย
ย
๐ redirect
ย
server components์์ ๊ฒฝ๋ก ๋ณ๊ฒฝ ๊ฐ๋ฅ
import { Metadata } from 'next'; import { redirect } from 'next/navigation'; export const metadata: Metadata = { title: 'TATA-V :: ํ์ด์ง๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค', description: 'ํ์ด์ง๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค', }; function TestPage() { redirect('/test2'); } export default TestPage;
ย
ย
๐ sitemap
ย
ย
๐ robots
ย
ย