서버 사이드 렌더링

서버 사이드 렌더링 (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)

더 알아보기 (Learn more)