μ˜€λŠ˜μ€ λΈŒλΌμš°μ € λ Œλ”λ§ 방식인 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 λ‚΄λΆ€μ—μ„œ μΌμ–΄λ‚˜λŠ” κ³Όμ •

  1. νŽ˜μ΄μ§€ μŠ€μΊ”
    • Next.jsλŠ” λΉŒλ“œ κ³Όμ •μ—μ„œ ν”„λ‘œμ νŠΈ λ‚΄ λͺ¨λ“  νŽ˜μ΄μ§€λ₯Ό ν™•μΈν•©λ‹ˆλ‹€.
    • 예λ₯Ό λ“€μ–΄:
      • app/page.tsx → ν™ˆ νŽ˜μ΄μ§€
      • app/about/page.tsx → μ†Œκ°œ νŽ˜μ΄μ§€
      • app/blog/[slug]/page.tsx → 동적 λΈ”λ‘œκ·Έ κΈ€ νŽ˜μ΄μ§€
    • μ΄λ ‡κ²Œ λͺ¨λ“  νŽ˜μ΄μ§€λ₯Ό μžλ™μœΌλ‘œ μ°Ύμ•„μ„œ 처리 λŒ€μƒμœΌλ‘œ λ“±λ‘ν•©λ‹ˆλ‹€.
  2. νŽ˜μ΄μ§€λ³„ 처리
    • 동적 라우트 처리: generateStaticParams() ν•¨μˆ˜κ°€ ν˜ΈμΆœλ˜μ–΄, 동적 URL(slug λ“±)에 ν•„μš”ν•œ λͺ¨λ“  쑰합을 μƒμ„±ν•©λ‹ˆλ‹€.
    • 데이터 κ°€μ Έμ˜€κΈ°: 각 νŽ˜μ΄μ§€μ˜ 데이터 페칭 ν•¨μˆ˜(API 호좜 λ“±)λ₯Ό μ‹€ν–‰ν•΄μ„œ ν•„μš”ν•œ 데이터λ₯Ό λΆˆλŸ¬μ˜΅λ‹ˆλ‹€.
    • React μ»΄ν¬λ„ŒνŠΈ λ Œλ”λ§: κ°€μ Έμ˜¨ 데이터λ₯Ό μ΄μš©ν•΄ React μ»΄ν¬λ„ŒνŠΈλ₯Ό HTML둜 λ Œλ”λ§ν•©λ‹ˆλ‹€.
    • HTML 파일 생성: λ Œλ”λ§λœ κ²°κ³Όλ₯Ό 정적 HTML 파일둜 λ³€ν™˜ν•©λ‹ˆλ‹€.
  3. 좜λ ₯λ¬Ό 생성
    • μƒμ„±λœ 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;
}

 

 

 

+ Recent posts