Vue3과 같은 라이프사이클이 존재하는 프레임워크를 SSR과 같이 사용할 때 어떤 이슈가 있을까요?
공식 문서의 번역입니다.
https://vuejs.org/guide/scaling-up/ssr.html
SSR이란?
SSR을 사용하는 이유
더 빠른 콘텐츠 경험:
느린 인터넷이나 느린 장치에서 더 두드러집니다.
서버 렌더링 마크업은 모든 JavaScript가 다운로드되고 실행되어 표시될 때까지 기다릴 필요가 없으므로
사용자는 완전히 렌더링된 페이지를 더 빨리 볼 수 있습니다.
또한 데이터 가져오기는 클라이언트보다 데이터베이스에 더 빠르게 연결할 수 있는 서버 측에서 수행됩니다.
이는 일반적으로 Core Web Vitals 지표를 개선하고 사용자 경험을 향상시키며
콘텐츠 도달 시간이 전환율과 직접적으로 연관되는 애플리케이션에 중요할 수 있습니다.
통합된 멘탈 모델:
백엔드 템플릿 시스템과 프런트엔드 프레임워크 사이를 오가는 대신
전체 앱을 개발하기 위해 동일한 언어와 동일한 선언적 구성 요소 지향 멘탈 모델을 사용할 수 있습니다.
더 나은 SEO:
사실 현재 Google과 Bing은 동기식 JavaScript 애플리케이션을 잘 인덱싱할 수 있습니다.
핵심 단어는 동기식입니다.
앱이 로딩 스피너로 시작한 다음 Ajax를 통해 콘텐츠를 가져오면 크롤러는 완료될 때까지 기다리지 않습니다.
즉, SEO가 중요한 페이지에서 비동기적으로 가져온 콘텐츠가 있는 경우 SSR이 필요할 수 있습니다.
개발 제약
- 특정 브라우저에서만 사용할 수 있는 코드는 특정 라이프사이클 훅에서만 사용 가능합니다.
- 일부 외부 라이브러리는 서버 렌더링 앱에서 실행할 수 있도록 특별한 처리가 필요할 수 있습니다.
복잡한 빌드 설정 및 배포 요구 사항
서버 측 부하
주 : nuxt를 쓰냐 next를 쓰냐랑은 다른 차원의 이야기입니다. (nuxt generate) 쓸 수 있으면 쓰세요
SSR vs SSG
주 : 그러니까 nuxt 쓰셈
Vue3 SSR 튜토리얼
앱 렌더링
- 새 디렉토리를 만들고 cd
- npm init -y 실행
- Node.js가 ES 모듈 모드에서 실행되도록 package.json에 "type": "module"을 추가합니다.
- npm install vue
- example.js 파일을 만듭니다.
// this runs in Node.js on the server.
import { createSSRApp } from 'vue'
// Vue's server-rendering API is exposed under `vue/server-renderer`.
import { renderToString } from 'vue/server-renderer'
const app = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
renderToString(app).then((html) => {
console.log(html)
})
> node example.js
<button>1</button>
renderToString()은 Vue 앱 인스턴스를 가져오고
앱의 렌더링된 HTML로 resolve되는 Promise를 반환합니다.
Node.js Stream API 또는 Web Streams API를 사용하여 스트리밍 렌더링도 가능합니다.
자세한 내용은 SSR API Reference를 확인하십시오.
그런 다음 Vue SSR 코드를 전체 페이지 HTML로 애플리케이션 마크업을 래핑하는 서버 요청 핸들러로 옮길 수 있습니다.
다음 단계에서는 익스프레스를 사용합니다.
- npm install express 실행
- server.js 파일을 만듭니다.
import express from 'express'
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
const server = express()
server.get('/', (req, res) => {
const app = createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
renderToString(app).then((html) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Vue SSR Example</title>
</head>
<body>
<div id="app">${html}</div>
</body>
</html>
`)
})
})
server.listen(3000, () => {
console.log('ready')
})
마지막으로 node server.js를 실행하고 http://localhost:3000을 방문합니다.
버튼과 함께 동작하는 페이지가 표시되어야 합니다.
클라이언트 측 수화
버튼을 클릭하면 숫자가 변경되지 않는 것을 알 수 있습니다.
브라우저에서 Vue를 로드하지 않기 때문에 HTML은 클라이언트에서 완전히 정적입니다.
// this runs in the browser.
import { createSSRApp } from 'vue'
const app = createSSRApp({
// ...same app as on server
})
// mounting an SSR app on the client assumes
// the HTML was pre-rendered and will perform
// hydration instead of mounting new DOM nodes.
app.mount('#app')
코드 구조
서버에서와 동일한 앱 구현을 어떻게 재사용할까요?
여기에서 SSR 앱의 코드 구조에 대해 생각해야 합니다.
서버와 클라이언트 간에 동일한 애플리케이션 코드를 공유하는 방법은 무엇일까요?
가장 기본적인 설정을 봅시다.
먼저 앱 생성 로직을 전용 파일인 app.js로 분할해 보겠습니다.
// app.js (shared between server and client)
import { createSSRApp } from 'vue'
export function createApp() {
return createSSRApp({
data: () => ({ count: 1 }),
template: `<button @click="count++">{{ count }}</button>`
})
}
이 파일과 해당 종속성은 서버와 클라이언트 간에 공유되며 이를 universal code라고 합니다.
아래에서 논의하겠지만 범용 코드를 작성할 때 주의해야 할 사항이 많이 있습니다.
// client.js
import { createApp } from './app.js'
createApp().mount('#app')
// server.js (irrelevant code omitted)
import { createApp } from './app.js'
server.get('/', (req, res) => {
const app = createApp()
renderToString(app).then(html => {
// ...
})
})
- server.js에 server.use(express.static('.'))를 추가하여 client.js 파일을 제공합니다.
- <script type="module" src="/client.js"></script>를 HTML에 추가하여 client.js 항목을 로드합니다.
- Import Map을 HTML 셸에 추가하여 브라우저에서 import * from 'vue'와 같은 사용을 지원합니다.
더 수준 높은 솔루션
- Vue SFC 및 기타 빌드 단계 요구 사항을 지원합니다.
- 사실상 동일한 앱에 대해 두 개의 빌드를 조정해야 합니다. 하나는 클라이언트용이고 다른 하나는 서버용입니다.
Vue 컴포넌트는 SSR에 사용될 때 다르게 컴파일됩니다.
템플릿은 보다 효율적인 렌더링 성능을 위해 가상 DOM 렌더 함수 대신 문자열로 컴파일됩니다.
- 서버 요청 핸들러에서 올바른 클라이언트 측 자산 링크 및 최적의 리소스 힌트를 사용하여 HTML을 렌더링합니다.
- 또한 SSR과 SSG 모드 사이를 전환하거나 동일한 앱에서 둘을 혼합해야 할 수도 있습니다.
- 범용 방식으로 라우팅, 데이터 가져오기 및 상태 관리 저장소를 관리합니다.
SSR 친화적인 코드 작성
서버에서의 반응성
SSR 중에 각 요청 URL은 애플리케이션의 원하는 상태에 매핑됩니다.
사용자 상호 작용 및 DOM 업데이트가 없으므로 서버에서는 반응성이 필요하지 않습니다.
기본적으로 성능 향상을 위해 SSR 중에는 반응성이 비활성화됩니다.
컴포넌트 생명주기
동적 업데이트가 없기 때문에 onMounted 또는 onUpdated와 같은 라이프사이클 훅은
SSR 중에 호출되지 않으며 클라이언트에서만 실행됩니다.
범용 코드는 플랫폼별 API에 대한 액세스를 가정할 수 없으므로
코드가 window 또는 document와 같은 브라우저 전용 전역을 직접 사용하는 경우 Node.js에서 실행될 때 오류가 발생하고
그 반대의 경우도 마찬가지입니다.
Cross-Request State Pollution(교차 요청 상태 오염)
브라우저에서와 마찬가지로 요청마다 모든 JavaScript 모듈을 기술적으로 다시 초기화할 수 있습니다.
하지만 JavaScript 모듈을 초기화하는 데 비용이 많이 들 수 있으므로 서버 성능에 상당한 영향을 미칩니다.
// app.js (shared between server and client)
import { createSSRApp } from 'vue'
import { createStore } from './store.js'
// called on each request
export function createApp() {
const app = createSSRApp(/* ... */)
// create new instance of store per request
const store = createStore(/* ... */)
// provide store at the app level
app.provide('store', store)
// also expose store for hydration purposes
return { app, store }
}
수화 불일치
사전 렌더링된 HTML의 DOM 구조가 클라이언트측 앱의 예상 출력과 일치하지 않으면 수화 불일치 오류가 발생합니다.
수화 불일치는 다음과 같은 원인에 의해 가장 일반적으로 발생합니다.
1. 템플릿에 잘못된 HTML 중첩 구조가 포함되어 있고 렌더링된 HTML이 브라우저의 기본 HTML 구문 분석 동작에 의해 "수정"되었습니다.
- 예를 들어 일반적인 문제는 <div>가 <p> 안에 배치될 수 없다는 것입니다.
<p><div>hi</div></p>
<p></p>
<div>hi</div>
<p></p>
2. 렌더링 중에 사용되는 데이터에 임의로 생성된 값이 포함됩니다.
2. 시드를 사용한 생성을 지원하는 난수 생성기 라이브러리를 사용
서버 실행과 클라이언트 실행이 동일한 시드를 사용하도록 보장합니다
(예: 직렬화된 상태의 시드를 포함하고 클라이언트에서 해당 시드 이용)
3. 서버와 클라이언트가 다른 시간대에 있습니다.
사용자 정의 디렉티브
const myDirective = {
mounted(el, binding) {
// client-side implementation:
// directly update the DOM
el.id = binding.value
},
getSSRProps(binding) {
// server-side implementation:
// return the props to be rendered.
// getSSRProps only receives the directive binding.
return {
id: binding.value
}
}
}
텔레포트
텔레포트는 SSR 중에 특별한 처리가 필요합니다.
렌더링된 앱에 텔레포트가 포함된 경우 텔레포트된 콘텐츠는 렌더링된 문자열의 일부가 아닙니다.
더 쉬운 해결책은 Teleport를 마운트 시 조건부로 렌더링하는 것입니다.
텔레포트된 콘텐츠를 수화해야 하는 경우 ssr 컨텍스트 개체의 teleports 속성 아래에 노출됩니다.
const ctx = {}
const html = await renderToString(app, ctx)
console.log(ctx.teleports) // { '#teleported': 'teleported content' }
주의 : Teleport와 SSR을 함께 사용할 시, body를 target으로 하지 마세요.
일반적으로 <body>에는
Teleports가 수화를 위한 올바른 시작 위치를 결정하는 것을 불가능하게 만드는 다른 서버 렌더링 콘텐츠가 포함됩니다.
대신 텔레포트 전용 컨테이너를 사용하세요.
- ex) 텔레포트된 콘텐츠만 포함하는 <div id="teleported"></div>.
'FrontEnd' 카테고리의 다른 글
Rollup.js 알아보기 1편 : Rollup.js 튜토리얼 (0) | 2023.01.21 |
---|---|
Content security policy[콘텐츠 보안 정책]에 대해 알아보기 (0) | 2023.01.21 |
ECMAScript(ESM)의 module resolution(모듈 해석)에 대해 알아보자 (0) | 2023.01.16 |
Node.JS 앱은 어떻게 종료되는가? (0) | 2023.01.15 |
Vue3로 debounce, throttle 구현하기 (0) | 2023.01.13 |