또 에러?
저번 글에서 ts-node로 ts 파일 실행시키는 것을 해 보았는데요..
2022.10.19 - [컴퓨터/기타] - ts-node 로 TypeScript (*.ts)파일 실행하기 Unknown file extension ".ts"
ts-node 로 TypeScript (*.ts)파일 실행하기 Unknown file extension ".ts"
사건의 발단 Vue로 SSR 튜토리얼을 해보던 중이었습니다. 저는 타입스크립트로 해보고 싶었기에 타입스크립트로 작성하였습니다. https://vuejs.org/guide/scaling-up/ssr.html#basic-tutorial Server-Side Rende..
supern0va.tistory.com
저때는 잘 된 줄 알았지만...어림도 없지
서버 사이드 렌더링을 진행하다 보니까 또 에러가 계속 발생하였습니다.
server.ts를 보면 아래와 같습니다.
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import express from "express";
import { createServer as createViteServer } from "vite";
const __dirname: string = path.dirname(fileURLToPath(import.meta.url));
// --snip--
ts-node를 실행하면 7 번째 라인에서 오류가 나더군요.. 튜토리얼에서는 ES6의 import가 없어서 tsconfig.json의 compileOptions.module이 CommonJS임에도 잘 동작을 했었던거 같습니다.
아무튼 실행을 해봅니다.
좀 읽어보면 import가 안된다는 내용이네요.
이게 CommonJS에서는 ESM이 지원이 안되기 때문에 자꾸 이런 오류가 나는거 같습니다. Nodejs는 CommonJS이기 때문이죠,,.
어떻게 해결할까
Vite가 ESM을 Nodejs가 읽을 수 있게 자동으로 변환을 해 줍니다. 마침 제 프로젝트에 Vite가 이미 있기 때문에 Vite를 사용하기로 했습니다. 그리고 친절한 공식문서가 있습니다.
여담으로 Vite는 어떻게 읽는지 아시나요? 공식 문서를 보니까 이렇게 쓰여있네요
Vite (French word for "quick", pronounced /vit/, like "veet")
한국어로 하면 "비트" 정도일 거 같습니다. 지금까지 바이트인줄 알았네요.
아무튼 각설하고 돌아와서, 공식 문서를 보니까 방법은 간단했습니다. Vite를 미들웨어로 사용하면 된다는 것인데요
server.ts에 다음과 같이 작성해줍니다.
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import express from "express";
import { createServer as createViteServer } from "vite";
const __dirname: string = path.dirname(fileURLToPath(import.meta.url));
async function createServer() {
const app: express.Application = express();
// 미들웨어 모드로 Vite 서버를 생성하고 애플리케이션의 타입을 'custom'으로 설정합니다.
// 이는 Vite의 자체 HTML 제공 로직을 비활성화하고, 상위 서버에서 이를 제어할 수 있도록 합니다.
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "custom",
});
// Vite를 미들웨어로 사용합니다.
// 만약 Express 라우터(express.Router())를 사용하는 경우, router.use를 사용해야 합니다.
app.use(vite.middlewares as express.RequestHandler);
app.use(
"*",
async (
req: express.Request,
res: express.Response,
next: express.NextFunction,
) => {
const url: string = req.originalUrl;
try {
// 1. index.html 파일을 읽어들입니다.
let template = fs.readFileSync(
path.resolve(__dirname, "index.html"),
"utf-8",
);
// 2. Vite의 HTML 변환 작업을 통해 Vite HMR 클라이언트를 주입하고,
// Vite 플러그인의 HTML 변환도 적용합니다.
// (예시: @vitejs/plugin-react의 Global Preambles)
template = await vite.transformIndexHtml(url, template);
// 3. 서버의 진입점(Entry)을 로드합니다.
// vite.ssrLoadModule은 Node.js에서 사용할 수 있도록 ESM 소스 코드를 자동으로 변환합니다.
// 추가적인 번들링이 필요하지 않으며, HMR과 유사한 동작을 수행합니다.
const { render } = await vite.ssrLoadModule("/src/entry-server.ts");
// 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);
// 6. 렌더링된 HTML을 응답으로 전송합니다.
res.status(200).set({ "Content-Type": "text/html" }).end(html);
} catch (e: any) {
// 만약 오류가 발생된다면, Vite는 스택트레이스(Stacktrace)를 수정하여
// 오류가 실제 코드에 매핑되도록 재구성합니다.
vite.ssrFixStacktrace(e);
next(e);
}
},
);
app.listen(5173, () => {
console.log("http://localhost:5173");
});
}
createServer();
그리고 main.ts에는 App.vue를 불러와서 createSSRApp에 등록해줍니다.
import { createPinia } from "pinia";
import { createSSRApp } from "vue";
import App from "./App.vue";
import { createRouter } from "./router";
// SSR requires a fresh app instance per request, therefore we export a function
// that creates a fresh app instance. If using Vuex, we'd also be creating a
// fresh store here.
export function createApp() {
const app = createSSRApp(App);
const pinia = createPinia();
app.use(pinia);
const router = createRouter();
app.use(router);
return { app, router };
}
그리고 src/entry-client.ts는 다음과 같이 작성을 합니다. id가 app인 곳에 마운트 해줍니다.
import { createApp } from "./main";
const { app, router } = createApp();
// wait until router is ready before mounting to ensure hydration match
app.mount("#app");
그리고 src/entry-server.ts도 작성 해줍니다.
import { basename } from "node:path";
import { renderToString } from "vue/server-renderer";
import { createApp } from "./main";
export async function render(url: any, manifest?: any) {
const { app, router } = createApp();
await router.push(url);
await router.isReady();
const html = await renderToString(app);
return html;
}
function renderPreloadLink(file: any) {
if (file.endsWith(".js")) {
return `<link rel="modulepreload" crossorigin href="${file}">`;
} else if (file.endsWith(".ts")) {
return `<link rel="modulepreload" crossorigin href="${file}">`;
} else if (file.endsWith(".css")) {
return `<link rel="stylesheet" href="${file}">`;
} else if (file.endsWith(".woff")) {
return ` <link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`;
} else if (file.endsWith(".woff2")) {
return ` <link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`;
} else if (file.endsWith(".gif")) {
return ` <link rel="preload" href="${file}" as="image" type="image/gif">`;
} else if (file.endsWith(".jpg") || file.endsWith(".jpeg")) {
return ` <link rel="preload" href="${file}" as="image" type="image/jpeg">`;
} else if (file.endsWith(".png")) {
return ` <link rel="preload" href="${file}" as="image" type="image/png">`;
} else {
// TODO
return "";
}
}
그리고 index.html에 렌더링 된 HTML을 넣을 자리를 지정해줍니다. ssr-outlet 자리에 렌더링 된 HTML이 들어갑니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--oreload-link-->
<title>Vite App</title>
</head>
<body>
<div id="app"><!--ssr-outlet--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
그 다음 ts-node server.ts를 실행해보면
제대로 동작하는거 같아 보이네요. localhost:5173에 접속을 해봅시다.
SSR구현에 성공!!
인 줄 알았으나 체크박스를 눌러도 반응이 없네요....
찾아보니까 ssr은 기본적으로 정적인 HTML만 돌려주는거라 Client Hydration이란 것을 해야 한다고 하네요.
그 내용은 다음에 이어서 해 보도록 하겠습니다.
참조
https://vuejs.org/guide/scaling-up/ssr.html#rendering-an-app
Server-Side Rendering (SSR) | Vue.js
Join in-person 1-3 November 2022, Toronto, Canada Join the Vue community in-person for VueConf Toronto from 1-3 November 2022! Use the code VUEJS to get 25% off on tickets!
vuejs.org
https://vitejs.dev/guide/ssr.html
Vite
Next Generation Frontend Tooling
vitejs.dev
https://github.com/vitejs/vite/tree/main/playground/ssr-vue
GitHub - vitejs/vite: Next generation frontend tooling. It's fast!
Next generation frontend tooling. It's fast! Contribute to vitejs/vite development by creating an account on GitHub.
github.com
https://devblog.kakaostyle.com/ko/2022-04-09-1-esm-problem/