165 lines
3.9 KiB
Markdown
165 lines
3.9 KiB
Markdown
# 2. Rendering 흐름 & Lifecycle
|
|
|
|
## SSR → Hydration → CSR 전환 흐름
|
|
|
|
Nuxt SSR은 세 단계로 구성된다.
|
|
|
|
```
|
|
[브라우저 요청]
|
|
↓
|
|
[서버] setup() 실행 → HTML 생성 → 클라이언트로 전송
|
|
↓
|
|
[브라우저] HTML 즉시 표시 (사용자에게 보임)
|
|
↓
|
|
[브라우저] Vue JS 로드 → Hydration (서버 HTML에 이벤트 연결)
|
|
↓
|
|
[브라우저] onMounted() 실행 → 완전한 SPA처럼 동작
|
|
```
|
|
|
|
---
|
|
|
|
## Lifecycle 훅 실행 위치
|
|
|
|
| 훅 | 서버 | 클라이언트 | 설명 |
|
|
|----|------|------------|------|
|
|
| `setup()` | ✅ | ✅ | 컴포넌트 초기화, 양쪽 모두 실행 |
|
|
| `onServerPrefetch()` | ✅ | ❌ | 서버에서만 실행, SSR용 데이터 페칭 |
|
|
| `onBeforeMount()` | ❌ | ✅ | DOM 마운트 직전 |
|
|
| `onMounted()` | ❌ | ✅ | DOM 마운트 완료, 브라우저 API 사용 가능 |
|
|
| `onBeforeUnmount()` | ❌ | ✅ | 컴포넌트 제거 직전 |
|
|
| `onUnmounted()` | ❌ | ✅ | 컴포넌트 제거 완료 |
|
|
|
|
---
|
|
|
|
## 실전 코드 예시
|
|
|
|
### setup()에서의 서버/클라이언트 분기
|
|
|
|
```vue
|
|
<script setup>
|
|
// setup()은 서버와 클라이언트 양쪽에서 실행됨
|
|
const config = useRuntimeConfig()
|
|
|
|
// 서버/클라이언트 환경 확인
|
|
if (import.meta.server) {
|
|
console.log('서버에서만 출력')
|
|
}
|
|
|
|
if (import.meta.client) {
|
|
console.log('클라이언트에서만 출력')
|
|
}
|
|
</script>
|
|
```
|
|
|
|
### onServerPrefetch — 서버에서 데이터 미리 준비
|
|
|
|
```vue
|
|
<script setup>
|
|
const data = ref(null)
|
|
|
|
// 서버에서 먼저 데이터를 가져오고, 클라이언트로 상태 전달
|
|
onServerPrefetch(async () => {
|
|
data.value = await $fetch('/api/products')
|
|
})
|
|
</script>
|
|
```
|
|
|
|
> `useAsyncData` / `useFetch`가 내부적으로 `onServerPrefetch`를 사용한다.
|
|
> 직접 쓸 일은 드물지만, 동작 원리 이해에 중요하다.
|
|
|
|
### onMounted — 브라우저 전용 작업
|
|
|
|
```vue
|
|
<script setup>
|
|
// ❌ 잘못된 예: setup()에서 window 접근 → 서버에서 오류 발생
|
|
// const width = window.innerWidth
|
|
|
|
// ✅ 올바른 예: onMounted에서 브라우저 API 사용
|
|
const windowWidth = ref(0)
|
|
|
|
onMounted(() => {
|
|
windowWidth.value = window.innerWidth
|
|
|
|
// 이벤트 리스너, setTimeout, localStorage 등 모두 여기서
|
|
window.addEventListener('resize', handleResize)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
// 반드시 정리해야 메모리 누수를 막는다
|
|
window.removeEventListener('resize', handleResize)
|
|
})
|
|
</script>
|
|
```
|
|
|
|
---
|
|
|
|
## Hydration이란?
|
|
|
|
서버가 생성한 정적 HTML에 Vue의 반응형 시스템과 이벤트 핸들러를 연결하는 과정이다.
|
|
|
|
```
|
|
서버 HTML: <button>클릭</button> ← 이벤트 없음
|
|
↓ Hydration
|
|
클라이언트: <button @click="handleClick">클릭</button> ← 이벤트 연결됨
|
|
```
|
|
|
|
### Hydration 불일치(Mismatch) 주의
|
|
|
|
서버와 클라이언트에서 다른 결과를 렌더링하면 경고가 발생한다.
|
|
|
|
```vue
|
|
<script setup>
|
|
// ❌ 문제: 서버(undefined)와 클라이언트(실제값) 결과가 다름
|
|
const isClient = ref(typeof window !== 'undefined')
|
|
|
|
// ✅ 해결: ClientOnly 컴포넌트 사용
|
|
</script>
|
|
|
|
<template>
|
|
<!-- 브라우저에서만 렌더링해야 하는 컴포넌트 -->
|
|
<ClientOnly>
|
|
<BrowserOnlyComponent />
|
|
<template #fallback>
|
|
<p>로딩 중...</p>
|
|
</template>
|
|
</ClientOnly>
|
|
</template>
|
|
```
|
|
|
|
---
|
|
|
|
## Nuxt 전용 훅 (useNuxtApp)
|
|
|
|
```vue
|
|
<script setup>
|
|
const nuxtApp = useNuxtApp()
|
|
|
|
// 페이지 전환 이벤트
|
|
nuxtApp.hook('page:start', () => {
|
|
console.log('페이지 전환 시작')
|
|
})
|
|
|
|
nuxtApp.hook('page:finish', () => {
|
|
console.log('페이지 전환 완료')
|
|
})
|
|
</script>
|
|
```
|
|
|
|
---
|
|
|
|
## 요약
|
|
|
|
```
|
|
서버 실행: setup() → onServerPrefetch()
|
|
↓
|
|
HTML 생성 및 전송
|
|
↓
|
|
클라이언트 수신: HTML 즉시 표시
|
|
↓
|
|
Hydration (JS 연결)
|
|
↓
|
|
setup() → onBeforeMount() → onMounted()
|
|
↓
|
|
이후 SPA처럼 동작
|
|
```
|