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
์ฌ๋ฌ ํ์ด์ง์์ ๊ณต์ ๋๋ ์ค์ฒฉ ๊ฐ๋ฅํ ๋ ์ด์์์ ๋ง๋ค ์ ์๋ค.
โ loading ๋ก๋ฉํ์ด์ง - loading.tsx
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.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 ํ์ผ์ ์ฌ์ฉํด์ผ ํ๋ค.)
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>
}
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>
)
}
ํ ๊ฐ ์ด์์ ๋งค๊ฐ๋ณ์๊ฐ ์์ ๋ ๋ ๋๋ง ๋๋ค.
/profile/[...username]/page.tsx
๋งค๊ฐ๋ณ์ 1๊ฐ - /profile/tata
๋งค๊ฐ๋ณ์ 2๊ฐ - /profile/tata/post1
๋งค๊ฐ๋ณ์ 3๊ฐ - /profile/tata/setting/security
(/profile
๋ ๋ ๋๋ง ๋๊ฒ ํ๊ณ ์ถ๋ค๋ฉด ๋๊ดํธ ๋์์ผ๋ก)
/profile/[[...username]]/page.tsx
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)
์ปดํฌ๋ํธ๋ฅผ ๋์ ์ผ๋ก ๊ฐ์ ธ์์ ํ์ํ ์์ ์ ๋ ๋๋งํ๋ค.
ssr
: ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง(SSR)์์ ์ ์ธ ํ ์ ์๋ ์ต์
import dynamic from 'next/dynamic';
// ๋์ ์ผ๋ก ๊ฐ์ ธ์ฌ ์ปดํฌ๋ํธ
const DynamicComponent = dynamic(() => import('../components/DynamicComponent'), {
loading: () => <div>...loading</div>, // ๋ก๋ฉ
ssr: false, // ํด๋น ์ปดํฌ๋ํธ๋ฅผ ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง์์ ์ ์ธ
});
์ด๋ฏธ์ง๋ฅผ 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.config.ts */
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '**.example.com', // ์ธ๋ถ ์ด๋ฏธ์ง ๋๋ฉ์ธ ์ถ๊ฐ
port: '',
},
],
},
}
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.variable}>{children}</body>
</html>
/* tailwind.config.css */
extend: {
fontFamily: {
pretendard: ['var(--font-pretendard)'],
},
},
ํ์ผ๋ช
์ ํ์ผ์ด๋ฆ.module.css
๋ก ์ฌ์ฉํด์ผ ํ๋ค.
module.css๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ํด๋์ค ๊ฐ์ ์ถฉ๋์ ํผํ ์ ์๋ค.
.nav-wrap {
display: flex;
}
import styles from './css/nav.module.css';
<div className={styles['nav-wrap']} />
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.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;
...
# ์ค์น
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 /> // โญ๏ธ ์ปดํฌ๋ํธ์ฒ๋ผ ์ฌ์ฉ ๊ฐ๋ฅ
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>
)
}
/** @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;
import type { Metadata, Viewport } from 'next';
export const metadata: Metadata = {
manifest: '/manifest.json',
title: 'ํ์ดํ',
description: '์ค๋ช
',
icons: {
icon: '/favicon.ico',
},
openGraph: {
images: ['์ด๋ฏธ์ง ๊ฒฝ๋ก']
},
alternates: {
canonical: '๋ฐฐํฌํ ์ฌ์ดํธ ์ฃผ์',
},
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,
};
interface Params {
params: { projectId: string }
}
// ๋์ ์ผ๋ก ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์์ฑํ๋ ํจ์
export async function generateMetadata({ params }: Params): Promise<Metadata> {
const { projectId } = params;
return {
title: `PROJECTS - ${projectId}`,
description: `project - ${projectId}`,
};
}
next.config.mjs
const nextConfig = {
async redirects() {
return [
{
source: "/",
destination: "/app", // ์ฒซ ํ์ด์ง๊ฐ '/app' ๊ฒฝ๋ก์์ ์์ํจ
permanent: true,
},
];
},
};
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);
...
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;
๐ (๊ณต์๋ฌธ์)generateMetadata
๐ (๊ณต์๋ฌธ์)generateStaticParams
๐ (๊ณต์๋ฌธ์)RevalidatingData
๐ (๊ณต์๋ฌธ์)api ๋ง๋ค๊ธฐ
๐งฟ SSR, CSR, SSG