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
.eslintrc.json
{
"extends": ["next/core-web-vitals", "prettier"]
}
.prettierrc.json
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
}
์ฌ๋ฌ ํ์ด์ง์์ ๊ณต์ ๋๋ ์ค์ฒฉ ๊ฐ๋ฅํ ๋ ์ด์์์ ๋ง๋ค ์ ์๋ค.
โ 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>
๋ฐฐํฌ ํ๊ฒฝ์์ ์ธ๋ถ ๊ฒฝ๋ก ์ด๋ฏธ์ง๊ฐ ์๋ฐ ๋ ์๋ ์ต์
์ถ๊ฐ
(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>
);
}
ํ์ผ๋ช
์ ํ์ผ์ด๋ฆ.module.css
๋ก ์ฌ์ฉํด์ผ ํ๋ค.
module.css๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ํด๋์ค ๊ฐ์ ์ถฉ๋์ ํผํ ์ ์๋ค.
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} */
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 /> // โญ๏ธ ์ปดํฌ๋ํธ์ฒ๋ผ ์ฌ์ฉ ๊ฐ๋ฅ
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;
๐ (๊ณต์๋ฌธ์)generateMetadata
๐ (๊ณต์๋ฌธ์)generateStaticParams
๐ (๊ณต์๋ฌธ์)RevalidatingData
๐ (๊ณต์๋ฌธ์)api ๋ง๋ค๊ธฐ
๐งฟ SSR, CSR, SSG