Initial implementation of the project structure and basic functionality.
This commit is contained in:
103
app/pages/hybrid.vue
Normal file
103
app/pages/hybrid.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<template>
|
||||
<PageLayout badge="Hybrid" badge-color="#8b5cf6" title="Hybrid / ISR">
|
||||
<InfoRow label="렌더링 방식" value="ISR (Incremental Static Regeneration)" />
|
||||
<InfoRow label="캐시 TTL" value="60초" />
|
||||
<InfoRow label="서버 렌더링 시간 (캐시됨)" :value="data?.time ?? '로딩 중...'" highlight />
|
||||
<InfoRow label="클라이언트 현재 시간 (실시간)" :value="clientTime" highlight />
|
||||
<InfoRow label="캐시 경과" :value="elapsedText" />
|
||||
|
||||
<!-- 환경 안내 배너 -->
|
||||
<div
|
||||
:style="{
|
||||
marginTop: '12px',
|
||||
padding: '10px 14px',
|
||||
borderRadius: '8px',
|
||||
fontSize: '13px',
|
||||
background: isDev ? '#fef3c7' : '#d1fae5',
|
||||
color: isDev ? '#92400e' : '#065f46',
|
||||
border: isDev ? '1px solid #fcd34d' : '1px solid #6ee7b7',
|
||||
}"
|
||||
>
|
||||
<strong>{{ isDev ? '⚠ 개발 모드' : '✓ 프로덕션 모드' }}</strong>
|
||||
{{ isDev
|
||||
? ' — ISR은 개발 서버에서 동작하지 않습니다. nuxt build 후 nuxt preview로 확인하세요.'
|
||||
: ' — ISR이 활성화되어 있습니다. 서버 렌더링 시간이 60초간 고정됩니다.'
|
||||
}}
|
||||
</div>
|
||||
|
||||
<template #explain>
|
||||
<li>
|
||||
<strong>ISR 동작 흐름:</strong> 첫 요청 시 서버 렌더링 및 캐시 저장
|
||||
→ 60초 이내 재방문 시 캐시된 HTML 즉시 반환
|
||||
→ 60초 경과 후 첫 요청에서 캐시 반환 + 백그라운드 재생성
|
||||
→ 다음 요청부터 새 HTML 반환
|
||||
</li>
|
||||
<li>
|
||||
<strong>확인 방법:</strong>
|
||||
<ol style="margin: 4px 0 0; padding-left: 20px;">
|
||||
<li><code>npm run build</code> → <code>npm run preview</code> 실행</li>
|
||||
<li>이 페이지를 열고 <strong>서버 렌더링 시간</strong> 기록</li>
|
||||
<li>60초 이내 새로고침 → 서버 시간이 <strong>동일</strong>함 (캐시)</li>
|
||||
<li>60초 후 새로고침 → 여전히 이전 값 (캐시 반환 + 백그라운드 재생성 시작)</li>
|
||||
<li>한 번 더 새로고침 → 서버 시간이 <strong>변경</strong>됨 (새 HTML)</li>
|
||||
</ol>
|
||||
</li>
|
||||
<li><code>swr: true</code>는 TTL 없이 항상 캐시를 반환하고 매 요청마다 백그라운드 갱신합니다.</li>
|
||||
<li>한 앱에서 SSR / SSG / SPA / ISR을 경로마다 다르게 혼합할 수 있습니다.</li>
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<pre>{{ codeExample }}</pre>
|
||||
</template>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { data } = await useFetch('/api/time')
|
||||
|
||||
// 클라이언트 실시간 시계
|
||||
const clientTime = ref('')
|
||||
const elapsedText = ref('-')
|
||||
|
||||
let interval: ReturnType<typeof setInterval>
|
||||
|
||||
onMounted(() => {
|
||||
const serverTime = data.value?.time ? new Date(data.value.time) : null
|
||||
|
||||
interval = setInterval(() => {
|
||||
const now = new Date()
|
||||
clientTime.value = now.toISOString()
|
||||
|
||||
if (serverTime) {
|
||||
const elapsed = Math.floor((now.getTime() - serverTime.getTime()) / 1000)
|
||||
const remaining = Math.max(0, 60 - elapsed)
|
||||
elapsedText.value = elapsed < 60
|
||||
? `${elapsed}초 경과 — 캐시 유효 (${remaining}초 남음)`
|
||||
: `${elapsed}초 경과 — 캐시 만료, 다음 요청 시 재생성`
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => clearInterval(interval))
|
||||
|
||||
// 개발/프로덕션 구분
|
||||
const isDev = import.meta.dev
|
||||
|
||||
const codeExample = `// nuxt.config.ts — 경로별 전략 혼합
|
||||
export default defineNuxtConfig({
|
||||
routeRules: {
|
||||
'/': { prerender: true }, // SSG
|
||||
'/blog/**': { isr: 3600 }, // ISR (1시간 캐시)
|
||||
'/products/**': { swr: true }, // Stale-While-Revalidate
|
||||
'/dashboard/**':{ ssr: false }, // SPA
|
||||
'/hybrid': { isr: 60 }, // ISR (60초 캐시) ← 이 페이지
|
||||
},
|
||||
})
|
||||
|
||||
// ISR 확인 방법
|
||||
// 1. npm run build
|
||||
// 2. npm run preview
|
||||
// 3. /hybrid 접속 후 서버 렌더링 시간 기록
|
||||
// 4. 60초 이내 새로고침 → 동일한 시간 (캐시)
|
||||
// 5. 60초 후 새로고침 2회 → 시간 변경 (재생성 완료)`
|
||||
</script>
|
||||
105
app/pages/index.vue
Normal file
105
app/pages/index.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div style="font-family: sans-serif; max-width: 720px; margin: 60px auto; padding: 0 24px;">
|
||||
<h1>Nuxt 4 렌더링 전략 데모</h1>
|
||||
<p style="color: #666;">각 페이지에서 렌더링 방식의 차이를 확인해보세요.</p>
|
||||
|
||||
<div style="display: grid; gap: 16px; margin-top: 32px;">
|
||||
<NuxtLink
|
||||
v-for="page in pages"
|
||||
:key="page.to"
|
||||
:to="page.to"
|
||||
style="display: block; padding: 20px 24px; border: 1px solid #e5e7eb; border-radius: 10px; text-decoration: none; color: inherit;"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 6px;">
|
||||
<span
|
||||
style="font-size: 12px; font-weight: 600; padding: 2px 10px; border-radius: 99px; color: white;"
|
||||
:style="{ background: page.color }"
|
||||
>
|
||||
{{ page.badge }}
|
||||
</span>
|
||||
<strong>{{ page.title }}</strong>
|
||||
</div>
|
||||
<p style="margin: 0; font-size: 14px; color: #6b7280;">{{ page.description }}</p>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
||||
<!-- 전략 비교표 -->
|
||||
<div style="margin-top: 48px;">
|
||||
<h2 style="font-size: 16px; font-weight: 600; margin-bottom: 16px;">전략 비교</h2>
|
||||
<div style="overflow-x: auto;">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 13px;">
|
||||
<thead>
|
||||
<tr style="background: #f9fafb;">
|
||||
<th v-for="col in table.cols" :key="col" style="padding: 10px 14px; text-align: left; border: 1px solid #e5e7eb; font-weight: 600; white-space: nowrap;">
|
||||
{{ col }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in table.rows" :key="row[0]">
|
||||
<td
|
||||
v-for="(cell, i) in row"
|
||||
:key="i"
|
||||
style="padding: 10px 14px; border: 1px solid #e5e7eb; white-space: nowrap;"
|
||||
:style="getCellStyle(cell, i)"
|
||||
v-html="cell"
|
||||
/>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
function getCellStyle(cell: string, colIndex: number) {
|
||||
if (colIndex === 0) return { fontWeight: '600' }
|
||||
if (cell.includes('필요')) return { color: '#dc2626', fontWeight: '600' }
|
||||
if (cell === '✓') return { color: '#10b981', fontWeight: '700', textAlign: 'center' as const }
|
||||
if (cell === '✗') return { color: '#9ca3af', textAlign: 'center' as const }
|
||||
return { color: '#374151' }
|
||||
}
|
||||
|
||||
const table = {
|
||||
cols: ['전략', '콘텐츠 최신성', '재배포', 'SEO', '서버 부하', '적합한 경우'],
|
||||
rows: [
|
||||
['SSR', '항상 최신', '불필요', '✓', '높음', '실시간 데이터, 개인화 페이지'],
|
||||
['SSG', '빌드 시 고정', '변경 시 필요', '✓', '없음', '블로그, 문서, 변경이 드문 콘텐츠'],
|
||||
['ISR', 'N초마다 갱신', '불필요', '✓', '낮음', '상품 목록, 뉴스 (준실시간)'],
|
||||
['SWR', '매 요청 후 갱신', '불필요', '✓', '낮음', '대시보드 (약간의 지연 허용)'],
|
||||
['SPA', '항상 최신', '불필요', '✗', '없음', '인증 후 관리자 페이지'],
|
||||
],
|
||||
}
|
||||
|
||||
const pages = [
|
||||
{
|
||||
to: '/ssr',
|
||||
badge: 'SSR',
|
||||
color: '#3b82f6',
|
||||
title: 'Server-Side Rendering',
|
||||
description: '요청마다 서버에서 HTML을 생성합니다. 페이지를 새로고침할 때마다 서버 시간이 바뀝니다.',
|
||||
},
|
||||
{
|
||||
to: '/ssg',
|
||||
badge: 'SSG',
|
||||
color: '#10b981',
|
||||
title: 'Static Site Generation',
|
||||
description: '빌드 시점에 HTML을 미리 생성합니다. 새로고침해도 빌드 시간이 고정되어 있습니다.',
|
||||
},
|
||||
{
|
||||
to: '/spa',
|
||||
badge: 'SPA',
|
||||
color: '#f59e0b',
|
||||
title: 'Single Page Application',
|
||||
description: '클라이언트에서만 렌더링합니다. 초기 HTML은 비어있고, JS 실행 후 데이터가 나타납니다.',
|
||||
},
|
||||
{
|
||||
to: '/hybrid',
|
||||
badge: 'Hybrid',
|
||||
color: '#8b5cf6',
|
||||
title: 'Hybrid / ISR',
|
||||
description: 'nuxt.config의 routeRules로 경로마다 전략을 다르게 설정합니다. 이 페이지는 ISR(60초 캐시)입니다.',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
39
app/pages/spa.vue
Normal file
39
app/pages/spa.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<PageLayout badge="SPA" badge-color="#f59e0b" title="Single Page Application">
|
||||
<InfoRow label="렌더링 위치" value="브라우저 (클라이언트)" />
|
||||
<InfoRow
|
||||
label="클라이언트 시간"
|
||||
:value="data?.time ?? (pending ? '클라이언트에서 로딩 중...' : '-')"
|
||||
highlight
|
||||
/>
|
||||
<InfoRow label="메시지" :value="data?.message ?? ''" />
|
||||
<InfoRow label="서버 HTML 확인" value="페이지 소스 보기 → 데이터가 비어있음" />
|
||||
|
||||
<template #explain>
|
||||
<li><code>defineRouteRules({ ssr: false })</code>로 서버 렌더링을 비활성화합니다.</li>
|
||||
<li>서버는 <strong>빈 HTML 껍데기</strong>만 전달하고, 데이터는 브라우저에서 가져옵니다.</li>
|
||||
<li>페이지 소스(Ctrl+U)를 보면 데이터가 없습니다.</li>
|
||||
<li>인증 이후 대시보드, 관리자 페이지처럼 SEO가 불필요한 곳에 적합합니다.</li>
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<pre>{{ codeExample }}</pre>
|
||||
</template>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// nuxt.config.ts에서 설정됨: routeRules: { '/spa': { ssr: false } }
|
||||
// useFetch는 클라이언트에서만 실행됨
|
||||
const { data, pending } = await useFetch('/api/time')
|
||||
|
||||
const codeExample = `// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
routeRules: {
|
||||
'/spa': { ssr: false },
|
||||
},
|
||||
})
|
||||
|
||||
// useFetch → 서버에서 실행되지 않음
|
||||
// 브라우저 로드 후 클라이언트에서 /api/time 호출`
|
||||
</script>
|
||||
62
app/pages/ssg.vue
Normal file
62
app/pages/ssg.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<PageLayout badge="SSG" badge-color="#10b981" title="Static Site Generation">
|
||||
<InfoRow label="렌더링 위치" value="빌드 시 서버 (nuxt generate 1회)" />
|
||||
<InfoRow label="빌드 시 생성된 시간" :value="data?.time ?? '로딩 중...'" highlight />
|
||||
<InfoRow label="메시지" :value="data?.message ?? ''" />
|
||||
<InfoRow label="클라이언트 API 요청" value="없음 — payload에서 hydration" />
|
||||
|
||||
<template #explain>
|
||||
<li>
|
||||
<strong>nuxt.config</strong>의 <code>routeRules: { '/ssg': { prerender: true } }</code>로 설정합니다.
|
||||
</li>
|
||||
<li>
|
||||
<code>nuxt generate</code> 실행 시 <code>/api/build-time</code>을 <strong>딱 1회 호출</strong>하고,
|
||||
응답값을 HTML 안의 Nuxt payload에 직렬화합니다.
|
||||
<br />
|
||||
<code style="font-size: 12px; color: #374151;"><script>window.__NUXT__ = { data: { time: "2026-..." } }</script></code>
|
||||
</li>
|
||||
<li>
|
||||
사용자가 페이지를 방문하면 이미 완성된 HTML이 반환됩니다.
|
||||
클라이언트의 JS는 payload를 읽어 hydration하므로 <strong>추가 네트워크 요청이 발생하지 않습니다.</strong>
|
||||
</li>
|
||||
<li>새로고침해도 시간이 고정됩니다 — CDN에서 동일한 정적 파일을 제공하기 때문입니다.</li>
|
||||
<li>
|
||||
<strong>⚠ SSG의 한계:</strong> 콘텐츠가 바뀌면 <code>nuxt generate</code> 재실행 + 재배포가 필요합니다.
|
||||
자주 바뀌는 데이터라면 재배포 없이 자동 재생성하는 <strong>ISR(<code>isr: N</code>)</strong> 또는
|
||||
<strong>SWR(<code>swr: true</code>)</strong>을 고려하세요.
|
||||
</li>
|
||||
<li>
|
||||
<strong>확인 방법:</strong> <code>npm run generate</code> 후
|
||||
<code>.output/public/ssg/index.html</code>을 열어 <code>__NUXT__</code>를 검색하면
|
||||
빌드 시 생성된 시간값이 HTML에 포함된 것을 볼 수 있습니다.
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<pre>{{ codeExample }}</pre>
|
||||
</template>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// nuxt.config.ts에서 설정됨: routeRules: { '/ssg': { prerender: true } }
|
||||
// nuxt generate 시 /api/build-time을 1회 호출 → HTML payload에 포함
|
||||
const { data } = await useFetch('/api/build-time')
|
||||
|
||||
const codeExample = `// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
routeRules: { '/ssg': { prerender: true } },
|
||||
})
|
||||
|
||||
// nuxt generate 실행 흐름:
|
||||
// 1. Nitro 서버 기동
|
||||
// 2. /ssg 페이지 pre-render
|
||||
// 3. useFetch('/api/build-time') → 1회 호출
|
||||
// 4. 응답값을 HTML payload에 직렬화
|
||||
// <script>window.__NUXT__ = { data: { time: "..." } }<\/script>
|
||||
// 5. .output/public/ssg/index.html 파일 저장
|
||||
|
||||
// 사용자 방문 시:
|
||||
// → 정적 HTML 즉시 반환 (서버 렌더링 없음)
|
||||
// → JS가 payload 읽어 hydration (API 요청 없음)`
|
||||
</script>
|
||||
30
app/pages/ssr.vue
Normal file
30
app/pages/ssr.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<PageLayout badge="SSR" badge-color="#3b82f6" title="Server-Side Rendering">
|
||||
<InfoRow label="렌더링 위치" value="서버 (요청마다)" />
|
||||
<InfoRow label="서버 시간" :value="data?.time ?? '로딩 중...'" highlight />
|
||||
<InfoRow label="메시지" :value="data?.message ?? ''" />
|
||||
|
||||
<template #explain>
|
||||
<li>페이지 요청 시 서버에서 <code>/api/time</code>을 호출하고 HTML을 완성해 전달합니다.</li>
|
||||
<li><strong>새로고침할 때마다 시간이 바뀝니다.</strong> — 서버가 매번 렌더링하기 때문입니다.</li>
|
||||
<li>SEO에 유리하고, 항상 최신 데이터를 보여줍니다.</li>
|
||||
<li>별도 설정 없이 Nuxt의 기본 동작입니다.</li>
|
||||
</template>
|
||||
|
||||
<template #code>
|
||||
<pre>{{ codeExample }}</pre>
|
||||
</template>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// SSR은 Nuxt 기본값 — 별도 설정 불필요
|
||||
const { data } = await useFetch('/api/time')
|
||||
|
||||
const codeExample = `// nuxt.config.ts — 기본값이므로 별도 설정 없음
|
||||
export default defineNuxtConfig({})
|
||||
|
||||
// pages/ssr.vue
|
||||
const { data } = await useFetch('/api/time')
|
||||
// 요청마다 서버에서 /api/time을 호출 → 항상 최신 데이터`
|
||||
</script>
|
||||
Reference in New Issue
Block a user