서버 사이드 렌더링
서버 사이드 렌더링 (SSR)
출처: 문서
본문
Element Plus를 SSR 개발에 사용할 때는 hydrate 오류를 피하기 위해 SSR 중 특별한 처리가 필요해요.
TIP Nuxt 사용자라면 이러한 특수 처리를 담은 Nuxt 모듈을 제공하니 설치만 하면 돼요.
ID 제공
이 값은 Element Plus에서 고유 ID를 생성하는 데 사용돼요. 서로 다른 ID는 SSR에서 hydrate 오류를 일으키기 쉬운데, 서버와 클라이언트가 같은 ID를 생성하도록 보장하기 위해 ID_INJECTION_KEY를 Vue에 주입해야 해요.
main.ts
// irrelevant code omitted
import { createApp } from 'vue'
import { ID_INJECTION_KEY } from 'element-plus'
import App from './App.vue'
const app = createApp(App)
app.provide(ID_INJECTION_KEY, {
prefix: 1024,
current: 0,
})
ZIndex 제공
SSR로 개발할 때 z-index 때문에 hydrate 오류가 생길 수 있어요. 이런 경우 초기 값을 주입해 오류를 피하는 걸 권장해요.
main.ts
// irrelevant code omitted
import { createApp } from 'vue'
import { ZINDEX_INJECTION_KEY } from 'element-plus'
import App from './App.vue'
const app = createApp(App)
app.provide(ZINDEX_INJECTION_KEY, { current: 0 })
Teleports
ElDialog, ElDrawer, ElTooltip, ElDropdown, ElSelect, ElDatePicker를 비롯한 여러 Element Plus 컴포넌트가 내부적으로 Teleport를 사용하므로 SSR 중 특별한 처리가 필요해요.
mount 시 Teleport 렌더링
더 쉬운 해결책은 mount 시점에 Teleport를 조건부로 렌더링하는 거예요. 예를 들어 Nuxt에서는 ClientOnly 컴포넌트를 사용합니다.
<client-only>
<el-tooltip content="tooltip">
<button>tooltip</button>
</el-tooltip>
</client-only>
또는
<script setup lang="ts">
import { ref } from 'vue'
const isClient = ref(false)
onMounted(() => {
isClient.value = true
})
</script>
<template>
<el-tooltip v-if="isClient" content="tooltip">
<button>tooltip</button>
</el-tooltip>
</template>
teleport 마크업 주입
또 다른 방법은 최종 페이지 HTML의 올바른 위치에 teleport 마크업을 주입하는 거예요. teleport 마크업은 <body> 태그 근처에 주입해야 합니다.
TIP Namespace나 append-to 속성을 수정했다면
#el-popper-container-값을 조정해야 해요.
src/entry-server.js
// irrelevant code omitted
import { renderToString } from 'vue/server-renderer'
import { createApp } from './main'
export async function render(url, manifest) {
// ...
const ctx = {}
const html = await renderToString(app, ctx)
const preloadLinks = renderPreloadLinks(ctx.modules, manifest)
const teleports = renderTeleports(ctx.teleports)
return [html, preloadLinks, teleports]
}
function renderTeleports(teleports) {
if (!teleports) return ''
return Object.entries(teleports).reduce((all, [key, value]) => {
if (key.startsWith('#el-popper-container-')) {
return `${all}${value}
`
}
return all
}, teleports.body || '')
}
server.js or prerender.js
// irrelevant code omitted
const [appHtml, preloadLinks, teleports] = await render(url, manifest)
const html = template
.replace('<head>', preloadLinks)
.replace('<div id="app">', appHtml)
.replace(/(\n|\r\n)\s*/, teleports)