์ค๋์ ๋ธ๋ผ์ฐ์ ๋ ๋๋ง ๋ฐฉ์์ธ SSG, CSR, SSR, ISR ๋ด๋ถ ์๋์๋ฆฌ์ ๋ํด ์ดํด๋ณผ ๊ฒ์ด๋ค.
1. SSG (Static Site Generation) ์๋์๋ฆฌ
์ ์ฒด ํ๋ฆ๋
[๋น๋ ํ์] [๋ฐํ์]
๊ฐ๋ฐ์ ์ฝ๋ ์ฌ์ฉ์ ์์ฒญ
↓ ↓
npm run build CDN/Server
↓ ↓
๋ฐ์ดํฐ ํ์นญ (API ํธ์ถ) ์ ์ HTML ํ์ผ ์ ์ก
↓ ↓
HTML ์์ฑ (๋ชจ๋ ํ์ด์ง) ๋ธ๋ผ์ฐ์ ์์ ์ฆ์ ๋ ๋๋ง
↓ ↓
.next/static ํด๋์ ์ ์ฅ (Hydration์ผ๋ก ์ธํฐ๋ํฐ๋ธ)
1. ๋น๋ํ์(Build Time)
- ๊ฐ๋ฐ์๊ฐ ์ฝ๋๋ฅผ ์์ฑํ๊ณ nun run build ๋ช ๋ น์ด๋ฅผ ์คํํ๋ฉด, ๋ชจ๋ ํ์ด์ง๊ฐ ๋ฏธ๋ฆฌ HTML๋ก ์์ฑ๋๋ค.
- ์ด ๊ณผ์ ์์ ํ์ํ ๋ฐ์ดํฐ๊ฐ ์๋ค๋ฉด API๋ฅผ ํธ์ถํด ๊ฐ์ ธ์ค๊ณ , ๊ฐ์ ธ์จ ๋ฐ์ดํฐ๋ฅผ ๋ฐํ์ผ๋ก HTML ํ์ผ์ ๋ง๋ค์ด .next/static (Next๋ผ๋ฉด) ๊ฐ์ ํด๋์ ์ ์ฅํ๋ค.
- ์ฆ, ์ฌ์ฉ์๊ฐ ํ์ด์ง๋ฅผ ์์ฒญํ๊ธฐ ์ ๋ฏธ๋ฆฌ ๋ชจ๋ ํ์ด์ง๋ฅผ ์ค๋นํ๋ ๋จ๊ณ์ด๋ค.
2. ๋ฐํ์(Runtime)
- ์ฌ์ฉ์๊ฐ ๋ธ๋ผ์ฐ์ ์์ ํ์ด์ง๋ฅผ ์์ฒญํ๋ฉด, ์๋ฒ๋ CDN์ด ๋ฏธ๋ฆฌ ๋ง๋ค์ด๋ ์ ์ HTMLํ์ผ์ ์ฆ์ ์ ์กํ๋ค.
- ๋ธ๋ผ์ฐ์ ์์๋ ๋ฐ์ HTML์ ๋ฐ๋ก ํ๋ฉด์ ๋ณด์ฌ์ฃผ๊ณ , ํ์ํ๋ฉด React์ Hydration ๊ณผ์ ์ ํตํด ์ธํฐ๋ํฐ๋ธ ๊ธฐ๋ฅ(๋ฒํผ ํด๋ฆญ, ํผ ์ ๋ ฅ ๋ฑ)์ ์ฌ์ฉํ ์ ์๋ค.
์์ธ ์๋ ๊ณผ์
1๋จ๊ณ: ๋น๋ ํ์ (Build Time)
npm run build
๋ด๋ถ์์ ์ผ์ด๋๋ ์ผ
1. Next.js๊ฐ ๋ชจ๋ ํ์ด์ง ์ค์บ
โโ app/page.tsx
โโ app/about/page.tsx
โโ app/blog/[slug]/page.tsx
โโ ...
2. ๊ฐ ํ์ด์ง๋ณ๋ก ์คํ:
โโ generateStaticParams() ํธ์ถ (๋์ ๋ผ์ฐํธ)
โโ ๋ฐ์ดํฐ ํ์นญ ํจ์ ์คํ
โโ React ์ปดํฌ๋ํธ ๋ ๋๋ง
โโ HTML ํ์ผ ์์ฑ
3. ์ถ๋ ฅ๋ฌผ:
โโ .next/server/app/page.html
โโ .next/server/app/about.html
โโ .next/server/app/blog/post-1.html
โโ JavaScript ๋ฒ๋ค (.next/static/chunks/)
Next.js SSG ๋ด๋ถ์์ ์ผ์ด๋๋ ๊ณผ์
- ํ์ด์ง ์ค์บ
- Next.js๋ ๋น๋ ๊ณผ์ ์์ ํ๋ก์ ํธ ๋ด ๋ชจ๋ ํ์ด์ง๋ฅผ ํ์ธํฉ๋๋ค.
- ์๋ฅผ ๋ค์ด:
- app/page.tsx → ํ ํ์ด์ง
- app/about/page.tsx → ์๊ฐ ํ์ด์ง
- app/blog/[slug]/page.tsx → ๋์ ๋ธ๋ก๊ทธ ๊ธ ํ์ด์ง
- ์ด๋ ๊ฒ ๋ชจ๋ ํ์ด์ง๋ฅผ ์๋์ผ๋ก ์ฐพ์์ ์ฒ๋ฆฌ ๋์์ผ๋ก ๋ฑ๋กํฉ๋๋ค.
- ํ์ด์ง๋ณ ์ฒ๋ฆฌ
- ๋์ ๋ผ์ฐํธ ์ฒ๋ฆฌ: generateStaticParams() ํจ์๊ฐ ํธ์ถ๋์ด, ๋์ URL(slug ๋ฑ)์ ํ์ํ ๋ชจ๋ ์กฐํฉ์ ์์ฑํฉ๋๋ค.
- ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ: ๊ฐ ํ์ด์ง์ ๋ฐ์ดํฐ ํ์นญ ํจ์(API ํธ์ถ ๋ฑ)๋ฅผ ์คํํด์ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ต๋๋ค.
- React ์ปดํฌ๋ํธ ๋ ๋๋ง: ๊ฐ์ ธ์จ ๋ฐ์ดํฐ๋ฅผ ์ด์ฉํด React ์ปดํฌ๋ํธ๋ฅผ HTML๋ก ๋ ๋๋งํฉ๋๋ค.
- HTML ํ์ผ ์์ฑ: ๋ ๋๋ง๋ ๊ฒฐ๊ณผ๋ฅผ ์ ์ HTML ํ์ผ๋ก ๋ณํํฉ๋๋ค.
- ์ถ๋ ฅ๋ฌผ ์์ฑ
- ์์ฑ๋ HTML ํ์ผ๊ณผ JavaScript ๋ฒ๋ค์ด ํ๋ก์ ํธ ๋ด๋ถ .next ํด๋์ ์ ์ฅ๋ฉ๋๋ค.
- ์์:
- .next/server/app/page.html → ํ ํ์ด์ง
- .next/server/app/about.html → ์๊ฐ ํ์ด์ง
- .next/server/app/blog/post-1.html → ๋ธ๋ก๊ทธ ๊ธ
- .next/static/chunks/ → React ๊ธฐ๋ฅ๊ณผ ์ธํฐ๋ํฐ๋ธ๋ฅผ ์ํ JS ๋ฒ๋ค
Next.js ์ฝ๋ ์์
// app/blog/[slug]/page.tsx
// 1. ๋น๋ ์ ์์ฑํ ๊ฒฝ๋ก ์ ์
export async function generateStaticParams() {
console.log('๐๏ธ ๋น๋ ํ์: ๊ฒฝ๋ก ์์ฑ ์ค...');
// API์์ ๋ชจ๋ ํฌ์คํธ ๊ฐ์ ธ์ค๊ธฐ
const posts = await fetch('<https://api.example.com/posts>')
.then(res => res.json());
// ๊ฐ ํฌ์คํธ๋ง๋ค ์ ์ ํ์ด์ง ์์ฑ
return posts.map((post: any) => ({
slug: post.slug, // /blog/post-1, /blog/post-2 ๋ฑ
}));
}
// 2. ๊ฐ ํ์ด์ง์ ๋ฐ์ดํฐ ํ์นญ
export default async function BlogPost({
params
}: {
params: Promise<{ slug: string }>
}) {
console.log(`๐๏ธ ๋น๋ ํ์: ${params.slug} ํ์ด์ง ์์ฑ ์ค...`);
const { slug } = await params;
// ์ด fetch๋ ๋น๋ ์ ํ ๋ฒ๋ง ์คํ๋จ
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
cache: 'force-cache' // SSG์ ํต์ฌ ์ค์ (DEFAULT)
}).then(res => res.json());
// React ์ปดํฌ๋ํธ๋ฅผ HTML๋ก ๋ณํ
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
2๋จ๊ณ: ๋น๋ ๊ฒฐ๊ณผ๋ฌผ
.next/
โโโ static/
โ โโโ chunks/ # JavaScript ์ฝ๋ ์กฐ๊ฐ๋ค
โ โ โโโ main.js # React ๋ฐํ์
โ โ โโโ pages/ # ํ์ด์ง๋ณ JS
โ โโโ css/ # CSS ํ์ผ๋ค
โโโ server/
โ โโโ app/
โ โโโ page.html # ๋ฏธ๋ฆฌ ์์ฑ๋ HTML
โ โโโ about.html
โ โโโ blog/
โ โโโ post-1.html # ๊ฐ ํฌ์คํธ์ HTML
โ โโโ post-2.html
์์ฑ๋ HTML ์์
<!DOCTYPE html>
<html>
<head>
<title>My Blog Post</title>
<link rel="stylesheet" href="/_next/static/css/app.css" />
</head>
<body>
<!-- ์ด๋ฏธ ๋ ๋๋ง๋ ์ฝํ
์ธ -->
<article>
<h1>๋ด ์ฒซ ๋ฒ์งธ ํฌ์คํธ</h1>
<div>
<p>์ด๊ฒ์ ๋ด์ฉ์
๋๋ค...</p>
</div>
</article>
<!-- JavaScript๋ Hydration์ ์ํด ๋ก๋ -->
<script src="/_next/static/chunks/main.js"></script>
<script src="/_next/static/chunks/pages/blog/post-1.js"></script>
</body>
</html>
3๋จ๊ณ: ์ฌ์ฉ์ ์์ฒญ (Request Time)
์ฌ์ฉ์๊ฐ /blog/post-1 ์ ์
↓
CDN/Server๊ฐ ๋ฏธ๋ฆฌ ์์ฑ๋ HTML ์ ์ก
↓
๋ธ๋ผ์ฐ์ ๊ฐ HTML ์ฆ์ ๋ ๋๋ง (0.1์ด)
↓
JavaScript ๋ค์ด๋ก๋ & ์คํ
↓
Hydration (HTML์ ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ฐ๊ฒฐ)
↓
์ธํฐ๋ํฐ๋ธํ ํ์ด์ง ์์ฑ
Hydration ๊ณผ์
// 1. ์๋ฒ์์ ์์ฑ๋ HTML (์ ์ )
<button>ํด๋ฆญํ์ธ์</button>
// 2. JavaScript ๋ก๋ ํ Hydration
<button onClick={() => alert('ํด๋ฆญ!')}>ํด๋ฆญํ์ธ์</button>
// ↑ ์ด๋ฒคํธ ๋ฆฌ์ค๋๊ฐ ์ถ๊ฐ๋จ
Hydration ์ฝ๋ ํ๋ฆ:
// React๊ฐ ๋ด๋ถ์ ์ผ๋ก ์ํ
ReactDOM.hydrateRoot(
document.getElementById('root'),
<App />
);
// ๊ณผ์ :
// 1. ๊ธฐ์กด HTML ์์ ์ฐพ๊ธฐ
// 2. Virtual DOM ์์ฑ
// 3. ๊ธฐ์กด DOM๊ณผ ๋น๊ต
// 4. ์ด๋ฒคํธ ๋ฆฌ์ค๋ ์ฐ๊ฒฐ
// 5. ์ํ ๊ด๋ฆฌ ํ์ฑํ
2. ISR (Incremental Static Regeneration) ์๋์๋ฆฌ
์ ์ฒด ํ๋ฆ๋
[๋น๋ ํ์] [์ฒซ ์์ฒญ] [revalidate ํ]
๋ชจ๋ ํ์ด์ง ์์ฑ → ์ ์ HTML ์ ๊ณต → ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฌ์์ฑ
↓ ↓
์บ์๋ ๋ฒ์ ์ ๊ณต ์ ๋ฒ์ ์ผ๋ก ๊ต์ฒด
ISR ์์ธ ์๋ ๊ณผ์
์๋๋ฆฌ์ค: revalidate: 60 ์ค์
// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`, {
next: {
revalidate: 60 // 60์ด๋ง๋ค ์ฌ๊ฒ์ฆ
}
}).then(res => res.json());
return <article>{post.title}</article>;
}
ํ์๋ผ์ธ ๋ถ์
์๊ฐ ์์ฒญ ์๋ต ๋ด๋ถ ๋์
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
00:00 ๋น๋ ์๋ฃ - HTML v1 ์์ฑ
(์ด๊ธฐ ์ํ)
01:00 ์ฌ์ฉ์ A HTML v1 ์ ๊ณต โ
์บ์ ํํธ
๋ฐฉ๋ฌธ (0.1์ด) (ํ์์คํฌํ: 00:00)
01:30 ์ฌ์ฉ์ B HTML v1 ์ ๊ณต โ
์บ์ ํํธ
๋ฐฉ๋ฌธ (0.1์ด) (์์ง 60์ด ์๋จ)
02:00 ์ฌ์ฉ์ C HTML v1 ์ ๊ณต ๐ ์ฌ์์ฑ ํธ๋ฆฌ๊ฑฐ!
๋ฐฉ๋ฌธ (0.1์ด) (60์ด ๊ฒฝ๊ณผ)
๋ฐฑ๊ทธ๋ผ์ด๋์์:
- API ์ฌํธ์ถ
- HTML v2 ์์ฑ
- ์บ์ ๊ต์ฒด (10์ด ์์)
02:05 ์ฌ์ฉ์ D HTML v1 ์ ๊ณต โณ ์ฌ์์ฑ ์งํ ์ค
๋ฐฉ๋ฌธ (0.1์ด)
02:10 ์ฌ์์ฑ ์๋ฃ - โ
์บ์ ์
๋ฐ์ดํธ
HTML v2 ์ค๋น ์๋ฃ
02:15 ์ฌ์ฉ์ E HTML v2 ์ ๊ณต โ
์ ๋ฒ์ !
๋ฐฉ๋ฌธ (0.1์ด)
03:00 ์ฌ์ฉ์ F HTML v2 ์ ๊ณต โ
์บ์ ํํธ
๋ฐฉ๋ฌธ (0.1์ด)
ISR ๋ด๋ถ ์บ์ ๋ฉ์ปค๋์ฆ
// Next.js ๋ด๋ถ ์บ์ ๊ตฌ์กฐ (๋จ์ํ)
const cache = {
'/blog/post-1': {
html: '<article>...</article>',
timestamp: 1704067200000, // ์์ฑ ์๊ฐ
revalidateAfter: 60, // ์ฌ๊ฒ์ฆ ๊ฐ๊ฒฉ (์ด)
isRevalidating: false // ์ฌ์์ฑ ์ค์ธ์ง
}
};
// ์์ฒญ ์ฒ๋ฆฌ ๋ก์ง
async function handleRequest(path) {
const cached = cache[path];
const now = Date.now();
const age = (now - cached.timestamp) / 1000; // ์ด ๋จ์
// 1. ์บ์๊ฐ ์ ํจํ ๊ฒฝ์ฐ
if (age < cached.revalidateAfter) {
console.log('โ
์บ์ ํํธ: ์ ์ ํ ๋ฐ์ดํฐ');
return cached.html;
}
// 2. ์บ์๊ฐ ๋ง๋ฃ๋์์ง๋ง ์ฌ์์ฑ ์ค์ด ์๋ ๊ฒฝ์ฐ
if (!cached.isRevalidating) {
console.log('๐ Stale-While-Revalidate: ์ค๋๋ ์บ์ ์ ๊ณต + ๋ฐฑ๊ทธ๋ผ์ด๋ ์ฌ์์ฑ');
// ์ค๋๋ ์บ์ ์ฆ์ ๋ฐํ
const staleResponse = cached.html;
// ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ฌ์์ฑ
cached.isRevalidating = true;
regeneratePage(path).then(newHtml => {
cache[path] = {
html: newHtml,
timestamp: Date.now(),
revalidateAfter: 60,
isRevalidating: false
};
console.log('โ
์ฌ์์ฑ ์๋ฃ: ์บ์ ์
๋ฐ์ดํธ๋จ');
});
return staleResponse;
}
// 3. ์ด๋ฏธ ์ฌ์์ฑ ์ค์ธ ๊ฒฝ์ฐ
console.log('โณ ์ฌ์์ฑ ์งํ ์ค: ์ค๋๋ ์บ์ ์ ๊ณต');
return cached.html;
}
ISR์ Stale-While-Revalidate ์ ๋ต
ISR์ ํต์ฌ์ "์ค๋๋ ์ฝํ ์ธ ๋ฅผ ์ ๊ณตํ๋ฉด์ ๋์์ ์ ์ฝํ ์ธ ๋ฅผ ์ค๋น"ํ๋ ๊ฒ์ ๋๋ค.
์์ฒญ → ์บ์ ํ์ธ
โโ ์ ์ ํจ → ์ฆ์ ๋ฐํ โ
โโ ์ค๋๋จ → ์ค๋๋ ๊ฒ ๋ฐํ + ๋ฐฑ๊ทธ๋ผ์ด๋ ๊ฐฑ์ ๐
(๋ค์ ์์ฒญ๋ถํฐ ์ ๋ฒ์ )
On-Demand Revalidation
์๋์ผ๋ก ์บ์๋ฅผ ๋ฌดํจํํ ์๋ ์์ต๋๋ค.
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const path = request.nextUrl.searchParams.get('path');
if (path) {
// ํน์ ๊ฒฝ๋ก ์ฌ๊ฒ์ฆ
revalidatePath(path);
console.log(`๐ ์๋ ์ฌ๊ฒ์ฆ: ${path}`);
return Response.json({ revalidated: true });
}
return Response.json({ revalidated: false });
}
// ์ฌ์ฉ ์:
// POST /api/revalidate?path=/blog/post-1
ISR ๋ด๋ถ ๋์:
// revalidatePath ๋ด๋ถ ๋ก์ง
function revalidatePath(path) {
// 1. ์บ์์์ ํด๋น ๊ฒฝ๋ก ์ฐพ๊ธฐ
const cached = cache[path];
if (cached) {
// 2. ํ์์คํฌํ๋ฅผ ๊ณผ๊ฑฐ๋ก ์ค์ (๋ง๋ฃ์ํด)
cached.timestamp = 0;
console.log(`โ
${path} ์บ์ ๋ฌดํจํ๋จ`);
// 3. ๋ค์ ์์ฒญ ์ ์๋์ผ๋ก ์ฌ์์ฑ๋จ
}
}
3. SSR (Server-Side Rendering) ์๋์๋ฆฌ
์ ์ฒด ํ๋ฆ๋
์ฌ์ฉ์ ์์ฒญ
↓
Next.js ์๋ฒ
↓
๋ฐ์ดํฐ ํ์นญ (API ํธ์ถ)
↓
React ์ปดํฌ๋ํธ ๋ ๋๋ง
↓
HTML ์์ฑ
↓
๋ธ๋ผ์ฐ์ ๋ก ์ ์ก
↓
Hydration
์์ธ ์๋ ๊ณผ์
์์ฒญ๋ณ ์ฒ๋ฆฌ ํ๋ฆ
// app/dashboard/page.tsx
export default async function Dashboard() {
console.log('๐ฅ๏ธ ์๋ฒ์์ ์คํ ์ค...');
const data = await fetch('<https://api.example.com/user>', {
cache: 'no-store', // SSR ํ์ฑํ
headers: {
cookie: cookies().toString() // ์ฟ ํค ํฌํจ
}
}).then(res => res.json());
return (
<div>
<h1>ํ์ํฉ๋๋ค, {data.name}๋</h1>
<p>๋ง์ง๋ง ๋ก๊ทธ์ธ: {data.lastLogin}</p>
</div>
);
}
SSR ํ์๋ผ์ธ ๋ถ์
์ฌ์ฉ์ A๊ฐ /dashboard ์์ฒญ (10:00:00)
โ
โโ [10:00:00.000] ์์ฒญ ๋์ฐฉ
โ โโ ์๋ฒ: "์ ์์ฒญ ๋ฐ์"
โ
โโ [10:00:00.050] ์ฟ ํค/ํค๋ ํ์ฑ
โ โโ ์๋ฒ: "์ธ์ฆ ํ ํฐ ํ์ธ"
โ
โโ [10:00:00.100] API ํธ์ถ ์์
โ โโ ์๋ฒ → API: "์ฌ์ฉ์ ๋ฐ์ดํฐ ์์ฒญ"
โ
โโ [10:00:00.500] API ์๋ต ๋๊ธฐ ์ค...
โ โโ ์๋ฒ: "์๋ต ๋๊ธฐ ์ค"
โ
โโ [10:00:00.800] API ์๋ต ๋์ฐฉ
โ โโ ์๋ฒ: "๋ฐ์ดํฐ ์์ ์๋ฃ"
โ
โโ [10:00:00.850] React ๋ ๋๋ง
โ โโ ์๋ฒ: "์ปดํฌ๋ํธ๋ฅผ HTML๋ก ๋ณํ"
โ
โโ [10:00:00.900] HTML ์์ฑ ์๋ฃ
โ โโ ์๋ฒ: "์ต์ข
HTML ์ค๋น"
โ
โโ [10:00:00.950] ๋ธ๋ผ์ฐ์ ๋ก ์ ์ก
โโ ์ฌ์ฉ์: "ํ์ด์ง ํ์"
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
์ฌ์ฉ์ B๊ฐ /dashboard ์์ฒญ (10:00:01)
โ
โโ ์ ๊ณผ์ ๋ฐ๋ณต (์บ์ ์์!)
โโ ์ด ์์์๊ฐ: ~1์ด
์๋ฒ ๋ด๋ถ ์ฒ๋ฆฌ
// Next.js ์๋ฒ์ ์์ฒญ ํธ๋ค๋ฌ (๋จ์ํ)
async function handleSSRRequest(req, res) {
console.log('๐ฅ๏ธ SSR ์์ฒญ ์ฒ๋ฆฌ ์์');
const startTime = Date.now();
try {
// 1. ๋ผ์ฐํธ ๋งค์นญ
const route = matchRoute(req.url);
console.log(`๐ ๋ผ์ฐํธ: ${route.path}`);
// 2. ํ์ด์ง ์ปดํฌ๋ํธ ๊ฐ์ ธ์ค๊ธฐ
const PageComponent = await import(route.componentPath);
// 3. ์๋ฒ ์ปดํฌ๋ํธ ์คํ (๋ฐ์ดํฐ ํ์นญ ํฌํจ)
console.log('๐ ๋ฐ์ดํฐ ํ์นญ ์์...');
const props = await PageComponent.getServerSideProps?.(req, res);
// 4. React ๋ ๋๋ง
console.log('๐จ React ๋ ๋๋ง ์์...');
const html = ReactDOMServer.renderToString(
<PageComponent {...props} />
);
// 5. HTML ํ
ํ๋ฆฟ์ ์ฝ์
const fullHtml = `
<!DOCTYPE html>
<html>
<head>
<title>${props.title}</title>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<div id="root">${html}</div>
<script src="/runtime.js"></script>
<script>
window.__INITIAL_DATA__ = ${JSON.stringify(props)};
</script>
</body>
</html>
`;
const endTime = Date.now();
console.log(`โ
๋ ๋๋ง ์๋ฃ (${endTime - startTime}ms)`);
// 6. ์๋ต ์ ์ก
res.setHeader('Content-Type', 'text/html');
res.send(fullHtml);
} catch (error) {
console.error('โ SSR ์๋ฌ:', error);
res.status(500).send('Server Error');
}
}
SSR ์ธ์ฆ/์ฟ ํค ์ฒ๋ฆฌ
// app/profile/page.tsx
import { cookies, headers } from 'next/headers';
export default async function Profile() {
// 1. ์ฟ ํค ์ฝ๊ธฐ
const cookieStore = cookies();
const token = cookieStore.get('auth_token');
console.log('๐ ์ธ์ฆ ํ ํฐ:', token?.value);
if (!token) {
redirect('/login');
}
// 2. ํค๋ ์ฝ๊ธฐ
const headersList = headers();
const userAgent = headersList.get('user-agent');
console.log('๐ค User-Agent:', userAgent);
// 3. ์ธ์ฆ๋ API ํธ์ถ
const user = await fetch('<https://api.example.com/profile>', {
headers: {
'Authorization': `Bearer ${token.value}`,
'User-Agent': userAgent || ''
},
cache: 'no-store'
}).then(res => res.json());
return
์ฑ๋ฅ ์ต์ ํ: Streaming SSR
// app/dashboard/page.tsx
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
<h1>๋์๋ณด๋</h1>
{/* ์ฆ์ ๋ ๋๋ง */}
<QuickStats />
{/* ๋๋ฆฐ ๋ถ๋ถ์ ๋์ค์ */}
<Suspense fallback={<Loading />}>
<SlowComponent />
</Suspense>
</div>
);
}
async function SlowComponent() {
// 3์ด ๊ฑธ๋ฆฌ๋ API ํธ์ถ
const data = await fetch('https://slow-api.com/data', {
cache: 'no-store'
});
return <HeavyChart data={data} />;
}
Streaming ๋์:
ํด๋ผ์ด์ธํธ๋ก ์ ์ก๋๋ HTML:
[0.1์ด]
<div>
<h1>๋์๋ณด๋</h1>
<div>๋น ๋ฅธ ํต๊ณ: ...</div>
<div id="suspense-boundary">๋ก๋ฉ ์ค...</div>
</div>
[3.1์ด - ์ถ๊ฐ ์ฒญํฌ]
<script>
// Suspense ๊ฒฝ๊ณ ์
๋ฐ์ดํธ
replaceSuspenseBoundary('suspense-boundary', `
<div>๋๋ฆฐ ์ปดํฌ๋ํธ ๋ด์ฉ...</div>
`);
</script>
4. CSR (Client-Side Rendering) ์๋์๋ฆฌ
์ ์ฒด ํ๋ฆ๋
์ฌ์ฉ์ ์์ฒญ
↓
๋น HTML + JavaScript ๋ฒ๋ค ์ ์ก
↓
๋ธ๋ผ์ฐ์ ์์ JavaScript ์คํ
↓
React ์ฑ ์ด๊ธฐํ
↓
API ํธ์ถ (fetch/axios)
↓
์ปดํฌ๋ํธ ๋ ๋๋ง
↓
ํ๋ฉด์ ํ์
์์ธ ์๋ ๊ณผ์
์ด๊ธฐ HTML (๊ฑฐ์ ๋น์ด์์)
<!DOCTYPE html>
<html>
<head>
<title>My App</title>
</head>
<body>
<!-- ๊ฑฐ์ ๋น ๊ป๋ฐ๊ธฐ -->
<div id="root"></div>
<!-- JavaScript ๋ฒ๋ค๋ง ๋ก๋ -->
<script src="/static/js/runtime.js"></script>
<script src="/static/js/vendors.js"></script>
<script src="/static/js/main.js"></script>
</body>
</html>
JavaScript ์คํ ํ๋ฆ
// app/dashboard/page.tsx
'use client'; // CSR ๋ช
์
import { useState, useEffect } from 'react';
export default function Dashboard() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
console.log('๐ ๋ธ๋ผ์ฐ์ ์์ ์คํ ์ค...');
console.log('๐ก API ํธ์ถ ์์...');
fetch('/api/user')
.then(res => {
console.log('โ
API ์๋ต ๋ฐ์');
return res.json();
})
.then(data => {
console.log('๐ฆ ๋ฐ์ดํฐ ํ์ฑ ์๋ฃ');
setUser(data);
setLoading(false);
})
.catch(error => {
console.error('โ ์๋ฌ:', error);
setLoading(false);
});
}, []);
if (loading) {
return <div>๋ก๋ฉ ์ค...</div>;
}
return (
<div>
<h1>ํ์ํฉ๋๋ค, {user.name}๋!</h1>
</div>
);
}
CSR ํ์๋ผ์ธ ๋ถ์
[0.0์ด] ์ฌ์ฉ์๊ฐ /dashboard ์์ฒญ
โ
โโ [0.1์ด] HTML ๋ค์ด๋ก๋ (5KB)
โ โโ ํ๋ฉด: ๋น ํ์ด์ง
โ
โโ [0.2์ด] JavaScript ๋ค์ด๋ก๋ ์์
โ โโ runtime.js (50KB)
โ โโ vendors.js (200KB - React ๋ฑ)
โ โโ main.js (100KB - ์ฑ ์ฝ๋)
โ โโ ํ๋ฉด: ์ฌ์ ํ ๋น ํ์ด์ง
โ
โโ [1.0์ด] JavaScript ํ์ฑ & ์คํ
โ โโ ํ๋ฉด: ์ฌ์ ํ ๋น ํ์ด์ง
โ
โโ [1.2์ด] React ์ฑ ์ด๊ธฐํ
โ โโ ReactDOM.render(<App />)
โ โโ ํ๋ฉด: "๋ก๋ฉ ์ค..." ํ์
โ
โโ [1.3์ด] useEffect ์คํ
โ โโ fetch('/api/user') ํธ์ถ
โ โโ ํ๋ฉด: "๋ก๋ฉ ์ค..."
โ
โโ [1.8์ด] API ์๋ต ๋์ฐฉ
โ โโ ํ๋ฉด: "๋ก๋ฉ ์ค..."
โ
โโ [2.0์ด] setState ์๋ฃ & ๋ฆฌ๋ ๋๋ง
โโ ํ๋ฉด: ์ค์ ์ฝํ
์ธ ํ์ โ
CSR ๋ธ๋ผ์ฐ์ ๋ด๋ถ ์ฒ๋ฆฌ
// React์ CSR ์ด๊ธฐํ (๋จ์ํ)
window.addEventListener('DOMContentLoaded', () => {
console.log('๐ DOM ๋ก๋ ์๋ฃ');
// 1. React ๋ฃจํธ ์์ฑ
const root = ReactDOM.createRoot(document.getElementById('root'));
console.log('๐ฑ React ๋ฃจํธ ์์ฑ');
// 2. ์ฑ ๋ ๋๋ง
console.log('๐จ ์ฑ ๋ ๋๋ง ์์');
root.render(<App />);
// 3. ์ปดํฌ๋ํธ ๋ง์ดํธ
// App → Dashboard → useEffect ์์๋ก ์คํ
});
CSR ์ํ ๊ด๋ฆฌ์ ๋ฆฌ๋ ๋๋ง
'use client';
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
console.log('๐ Counter ์ปดํฌ๋ํธ ๋ ๋๋ง:', count);
const increment = () => {
console.log('โ ์ฆ๊ฐ ๋ฒํผ ํด๋ฆญ');
setCount(prev => {
const newCount = prev + 1;
console.log(`์ํ ๋ณ๊ฒฝ: ${prev} → ${newCount}`);
return newCount;
});
// ์ํ ๋ณ๊ฒฝ ํ ์๋์ผ๋ก ๋ฆฌ๋ ๋๋ง ํธ๋ฆฌ๊ฑฐ
};
return (
<div>
<p>์นด์ดํธ: {count}</p>
<button onClick={increment}>์ฆ๊ฐ</button>
</div>
);
}
์คํ ํ๋ฆ:
[์ด๊ธฐ ๋ ๋๋ง]
โโ ๐ Counter ์ปดํฌ๋ํธ ๋ ๋๋ง: 0
[๋ฒํผ ํด๋ฆญ]
โโ โ ์ฆ๊ฐ ๋ฒํผ ํด๋ฆญ
โโ ์ํ ๋ณ๊ฒฝ: 0 → 1
โโ ๐ Counter ์ปดํฌ๋ํธ ๋ ๋๋ง: 1
โโ Virtual DOM ์์ฑ
โโ ์ค์ DOM๊ณผ ๋น๊ต (Reconciliation)
โโ ๋ณ๊ฒฝ๋ ๋ถ๋ถ๋ง ์
๋ฐ์ดํธ
โโ <p>์นด์ดํธ: 1</p> โ
React์ Reconciliation (์ฌ์กฐ์ )
// React ๋ด๋ถ ๋ก์ง (๋จ์ํ)
function updateComponent(Component, newProps) {
// 1. ์๋ก์ด Virtual DOM ์์ฑ
const newVDOM = Component(newProps);
console.log('๐ ์ Virtual DOM:', newVDOM);
// 2. ์ด์ Virtual DOM๊ณผ ๋น๊ต
const diff = compareVDOM(oldVDOM, newVDOM);
console.log('๐ ๋ณ๊ฒฝ ์ฌํญ:', diff);
// 3. ๋ณ๊ฒฝ๋ ๋ถ๋ถ๋ง ์ค์ DOM ์
๋ฐ์ดํธ
diff.forEach(change => {
if (change.type === 'UPDATE_TEXT') {
const element = document.querySelector(change.selector);
element.textContent = change.newValue;
console.log(`โ๏ธ ํ
์คํธ ์
๋ฐ์ดํธ: ${change.oldValue} → ${change.newValue}`);
}
});
// 4. ์ด์ VDOM์ ์ VDOM์ผ๋ก ๊ต์ฒด
oldVDOM = newVDOM;
}
'Developing๐ฉโ๐ป > Front-end' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [์๋ฌธ ๋ฒ์ญ] ์๋ฒ ์ํ์์ ํด๋ผ์ด์ธํธ ์ํ ๋์ด์ค๊ธฐ - tkdodo (0) | 2025.10.07 |
|---|---|
| [์๋ฌธ ๋ฒ์ญ] useCallback์ ์ธ๋ชจ์์ - tkdodo (0) | 2025.10.07 |
| [Next.js] nuqs์ useQueryState (0) | 2025.10.06 |
| [FE] ๋น๋๊ธฐ ๋์ ๋์์ฑ์ ๊ตฌํํ๋ Web Worker, WebAssembly ์์๋ณด๊ธฐ(๋ฉํฐ์ค๋ ๋) (0) | 2025.08.24 |
| [FE] Webpack, Rollup.js, ๊ทธ๋ฆฌ๊ณ Vite (0) | 2025.08.22 |

