개요
이 글에서는 어떻게 동작을 하는지 살펴보도록 하겠습니다.
동작 설명
기본 예제
공식문서 코드
공식 문서에서 설명하는 코드는 아래와 같습니다. (주석은 공식문서에 작성된 내용입니다)
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
<!--ssr-outlet-->
<script src="/index.tsx" type="module"></script>
</body>
</html>
JavaScript
복사
index.html
import express from 'express';
import {createServer} from 'vite';
import axios from 'axios';
import fs from 'fs';
async function createDevServer() {
const port = process.env.PORT || 5174;
// Create http server
const app = express();
const vite = await createServer({
server: { middlewareMode: true },
appType: 'custom',
});
app.use('*', (req, res, next) => {
vite.middlewares.handle(req, res, next);
});
app.get('*', async (req, res) => {
try {
// [1] index.html 파일을 읽어들입니다.
let template = fs.readFileSync('./index.html', 'utf-8');
// [2] Vite의 HTML 변환 작업을 통해 Vite HMR 클라이언트를 주입하고,
// Vite 플러그인의 HTML 변환도 적용합니다.
// (예시: @vitejs/plugin-react의 Global Preambles)
template = await vite.transformIndexHtml(url, template)
// [3] 서버의 진입점(Entry)을 로드합니다.
// ssrLoadModule은 Node.js에서 사용할 수 있도록 ESM 소스 코드를 자동으로 변환합니다.
// 추가적인 번들링이 필요하지 않으며, HMR과 유사한 동작을 수행합니다.
const { render } = await vite.ssrLoadModule('/src/entry-server.js')
// [4] 앱의 HTML을 렌더링합니다.
// 이는 entry-server.js에서 내보낸(Export) `render` 함수가
// ReactDOMServer.renderToString()과 같은 적절한 프레임워크의 SSR API를 호출한다고 가정합니다.
const appHtml = await render(url)
// [5] 렌더링된 HTML을 템플릿에 주입합니다.
const html = template.replace(`<!--ssr-outlet-->`, appHtml)
res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
} catch (e) {
vite?.ssrFixStacktrace(e);
next(e) }
});
// Start http server
app.listen(port, () => {
console.log(`Server started at http://localhost:${port}`);
});
}
createDevServer();
JavaScript
복사
server.js
[1] index.html 불러오기
index.html 파일을 읽어들입니다.
말그대로 readFileSync를 통해서 index.html을 불러옵니다.
[2] transformIndexHtml
Vite의 HTML 변환 작업을 통해 Vite HMR 클라이언트를 주입하고,
Vite 플러그인의 HTML 변환도 적용합니다.
(예시: @vitejs/plugin-react의 Global Preambles)
크로스 브라우징에서 렌더링하는데 필요한 각종 폴리필들과 동적 모듈들을 위한 import들을 처리해줍니다.
그리고 HMR을 위한 코드와 index.tsx로 로컬 경로로 되어있는 코드를 vite dev server와 매핑된 script로 변경해줍니다.
<!DOCTYPE html>
<html>
<head>
<script type="module">
import RefreshRuntime from 'http://localhost:5173/@react-refresh'
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () => {
}
window.$RefreshSig$ = () => (type) => type
window.__vite_plugin_react_preamble_installed__ = true
</script>
</head>
<body>
<!--ssr-outlet-->
<script src="http://localhost:5173/@vite/client" type="module"></script>
<script src="http://localhost:5173/index.tsx" type="module"></script>
<!--<script src="/index.tsx" type="module"></script>-->
</body>
</html>
JavaScript
복사
[2] template
[3] ssrLoadModule
서버의 진입점(Entry)을 로드합니다.
ssrLoadModule은 Node.js에서 사용할 수 있도록 ESM 소스 코드를 자동으로 변환합니다.
추가적인 번들링이 필요하지 않으며, HMR과 유사한 동작을 수행합니다.
entry-server.js 코드를 불러온 뒤 번들링하고, 각종 옵션들을 추가해줍니다. 결과적으로 번들링 된 entry-server.js가 나오기 때문에 리턴된 { render }는 entry-server.js 에서 리턴된 값입니다.
[4] render(url)
앱의 HTML을 렌더링합니다.
이는 entry-server.js에서 내보낸(Export) `render` 함수가
ReactDOMServer.renderToString()과 같은 적절한 프레임워크의 SSR API를 호출한다고 가정합니다.
render 를 통해 html에 추가할 값을 불러옵니다. React든 Vanilla든 script들의 string이 넘어온다고 생각하면 쉽습니다.
•
entry-server.js React 예시
react의 경우 아래와 같이 renderToString()을 통해서 값을 넘겨줄 수 있습니다.
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import App from './App'
export function render() {
const html = ReactDOMServer.renderToString(
<React.StrictMode>
<App />
</React.StrictMode>
)
return { html }
}
JavaScript
복사
•
entry-server.js Vanilla 예시
vanilla js의 경우 tag를 string으로 return 해주면 됩니다.
export async function render() {
return '<div>Hello</div>'
}
JavaScript
복사
[5] text 대치
렌더링된 HTML을 템플릿에 주입합니다.
예제 코드에선 <!--ssr-outlet--> 라는 코드를 사용해주었지만, 단순 텍스트 대치이기 때문에 어떠한 문자열을 사용하도 상관없습니다.
적용 예시
외부에서 불러온 HTML에 HMR 적용하기
JSP에서 호출한 페이지 HMR 적용하기
SSR을 위한 페이지는 아니고, JSP로 작성한 프로젝트를 구동하는데 HMR을 적용하기 위한 목적인 node 서버입니다.
const express = require('express');
const { createServer } = require('vite');
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const port = process.env.PORT || 5173;
async function createDevServer() {
const app = express();
const vite = await createServer({
server: { middlewareMode: 'ssr' },
appType: 'custom',
});
app.use('*', (req, res, next) => {
vite.middlewares.handle(req, res, next);
});
app.use('*', async (req, res) => {
try {
const jspUrl = `http://${req.hostname}:8080`;
// 외부 JSP 서버에서 HTML을 가져옴
const response = await axios.get(`${jspUrl}${req.originalUrl}`, {
...req,
});
// 호출을 통해 가져온 값을 처리
const appHtml = await vite.transformIndexHtml(req.url, response.data);
const serverHtml = ...
// 추가하고 싶은 특정 코드 추가
const html = appHtml.replace(`<!--ssr-outlet-->`, serverHtml);
res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
} catch (e) {
vite?.ssrFixStacktrace(e);
console.log(e.stack);
res.status(500).end(e.stack);
}
});
return { app, vite };
}
createDevServer().then(({ app }) =>
app.listen(port, (name) => {
console.log(`Server Start : http://localhost:${port}`);
}),
);
JavaScript
복사
server.js