Vue

Vite + Vue + Typescript로 SSR 삽질 해보기 [1]

초코민트냠냠 2022. 10. 20. 16:38
반응형

또 에러?

저번 글에서 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/

 

반응형