본문 바로가기
📌 Front End/└ React

[React] 리액트 SSR 렌더링 및 빌드 배포 가이드 - React Vite 프로젝트 SSR

by 쫄리_ 2024. 10. 4.
728x90
반응형

📌 SSR(Server Side Rendering)

SSR은 서버에 페이지에 대한 요청을 하고,

서버로부터 HTML, JS, CSS 파일 및 데이터를 전달받아 렌더링하는 방식입니다.

즉, 서버에서 렌더링을 수행하는 방법입니다.

 

🔎 SSR 과정 요약

  1. 사용자가 접속합니다.
  2. 서버에서 React를 실행합니다.
  3. 서버에서 HTML 코드를 생성하여 완성된 <html>과 <script>를 클라이언트로 전송합니다.
  4. 클라이언트는 완성된 <html>에 <script>를 연결하여 렌더링을 완료합니다.

 

 


📌 React로 서버 측 렌더링(SSR)

서버에서 React 코드를 한 번 실행하고, 결과물을 클라이언트에 전달하는 기법

SSR(Server Side Rendering)이라고 합니다.

이 방법은 서버에서 페이지를 미리 렌더링하여 클라이언트에 제공합니다.

SSR의 빌드와 배포 과정을 다음과 같이 설명할 수 있습니다.

 

🔎 서버에서 HTML 제공

서버는 렌더링된 HTML을 생성하여 클라이언트로 전송합니다.

🔎 웹 페이지 구성

클라이언트는 이 HTML을 사용하여 웹 페이지를 구성합니다.

이 과정에서 프론트엔드 개발자가 렌더링과 연결을 설정해야 합니다.


✅ Express 라이브러리 설치

CSR 방식으로 리액트 앱을 배포할 때는 서버에서 정적 파일을 제공해야 합니다.

이를 위해 다음 라이브러리들을 설치합니다.

  • express : 서버에서 리액트 앱의 정적 파일(HTML, JS, CSS)을 제공하는 역할
  • fs : 파일 시스템을 다루기 위한 Node.js의 기본 모듈 (파일을 읽고 쓰는 작업에 사용)
npm i express fs

 


💻 클라이언트 - React 앱의 진입점 (main.tsx 설정)

'📁components/📜main.tsx' 
클라이언트 측에서 React 애플리케이션을 실행하는 코드
hydrateRoot 함수를 사용하여 서버에서 전달된 HTML과 React 애플리케이션을 연결

 

"📁components/📜main.tsx" React 애플리케이션의 시작점입니다.

클라이언트에서 사용할 최상위 컴포넌트를 설정합니다.

  •  서버에서의 렌더링 
    서버는 <App /> 컴포넌트를 미리 렌더링하여 HTML을 생성하고, 이를 클라이언트에 전달
  •  하이드레이션(hydrate) 
    클라이언트는 서버에서 받은 HTML을 사용해 React 애플리케이션의 기능을 추가합니다. 
    이 과정을 "하이드레이션"이라고 하며, 
    이미 있는 HTML에 React를 연결하여 동작하게 만드는 것

 

🔎 hydrateRoot 사용

기존 HTML을 새로 렌더링하지 않고서버에서 이미 렌더링된 HTML과 React를 연결합니다.
이렇게 하면 불필요한 렌더링 없이 클라이언트에서 빠르게 연결할 수 있습니다.

  • ReactDOM의 createRoot 대신 hydrateRoot를 사용하여 
    서버에서 렌더링된 HTML을 클라이언트와 연결합니다.
  • 첫 번째 인자는 연결할 DOM 요소, 두 번째 인자는 React 컴포넌트
// ▶ 📜main.tsx
import React from 'react';
import ReactDOM, { hydrateRoot } from 'react-dom/client';
import App from './app';

hydrateRoot(
     document.querySelector("#reactRoot") as HTMLElement,
     <App/>
)

 


🗄️ 서버 사이드 렌더링(SSR) 설정 (ssr.tsx)

'📁components/📜ssr.tsx' 새 파일 생성
서버에서 React 컴포넌트를 HTML로 변환하는 기능을 수행하여
클라이언트에 전달할 HTML을 생성합니다.
서버에서 HTML을 미리 렌더링

 

서버에서 React 애플리케이션의 초기 HTML을 생성하여 클라이언트로 전달합니다.

renderToString 함수를 사용해 <App /> 컴포넌트를 HTML 문자열로 변환하고, 이를 반환

import 불러오기  renderToString 
React 컴포넌트를 HTML로 변환하는 함수

 App 
앱의 메인 컴포넌트
render 함수 이 함수 안에서  컴포넌트를 HTML로 변환</app >합니다.
 renderToString(<App />)  사용하여
앱의 메인 컴포넌트인 App의 내용을 HTML로 만듭니다.
HTML 반환  return { html }; 
만들어진 HTML을 클라이언트에 전달합니다.

 

// ▶ 📁components/📜ssr.tsx
import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './app';

export default function render(){
     const html = renderToString(
          <App/>
     );

     return { html }; // 변환된 HTML을 반환
}

 


📄 HTML 기본 템플릿 파일 (index.html)

'📄index.html' ➡️ <!-- root-container --> 주석 추가

 

"📄index.html" HTML 템플릿으로, React 애플리케이션이 로드되는 기본 페이지입니다.

이 파일은 React 애플리케이션이 렌더링될 공간을 포함하고 있습니다.

<!-- root-container --> 주석 부분은 서버에서 생성된 React HTML이 들어갈 자리입니다.

<!-- root-container -->

서버가 HTML을 만들 때, <!-- root-container -->는 React 앱의 내용을 넣을 자리를 표시하는 주석입니다.
변환된 HTML 내용은 <div id="reactRoot"></div> 사이에 들어가며,
이 내용은 실제 HTML이 아닌 텍스트 형태로 전달됩니다.

변환된 HTML 내용은 📁components/📜ssr.tsx에서 작성한
React의 상위 <App /> 컴포넌트가 HTML 형식으로 변환된 것입니다.
따라서 <!-- root-container --> 안으로 문자열 형태로 들어갑니다.

 

 

📄 index.html

// ▶ 📄index.html
<!DOCTYPE html>
<html lang="ko">
    <head>
        <meta charset="UTF-8">
        <title>Vite & React SSR</title>
    </head>
    <body>
        <div id="reactRoot"><!-- root-container --></div>
        <script type="module" src="components/main.tsx"></script>
    </body>
</html>

 


🗄️ Express 서버 설정 (server.js)

'📜server.js' 새 파일 생성
Express 서버를 설정하고,
클라이언트 요청에 대해 서버에서 생성된 HTML을 제공

 

root(최상위) 경로에 'server.js' 새 파일을 만든 후, 아래와 같이 코드를 작성합니다.
이 코드는 Express라는 도구를 사용하여 웹 서버를 설정하는 내용입니다.

 

서버는 📁dist/server/ssr.js에서 가져온 SSR(서버 사이드 렌더링) HTML을
./dist/client/index.html 파일에 연결합니다.

이 과정에서 Node.js의 fs 모듈을 사용하여 파일을 입출력합니다.
그 후 📄index.html 파일의 <!-- root-container --> 부분에 HTML을 삽입하고, 

클라이언트에게 전달합니다.

import 불러오기  import express from 'express'; 
Express를 사용하기 위해 가져옵니다.

 import fs from 'fs'; 
파일을 읽고 쓰기 위해 파일 시스템 모듈을 가져옵니다.

 import ssr from './dist/server/ssr.js'; 
서버 사이드 렌더링(SSR) 기능을 가져옵니다.
서버 앱 만들기  const app = express(); 
웹 서버를 만들기 위해 Express 앱을 생성합니다.
정적 파일 제공  app.use('/', express.static('dist/client')); 
사용자가 웹사이트에 접속할 때,
dist/client 폴더에 있는 파일들을 제공합니다.
특정 경로 처리  app.use('/app', (req, res) => {...}); 
/app 경로로 들어오는 요청을 처리하는 기능을 정의합니다.

req (request)
클라이언트가 서버에 보낸 요청에 대한 정보 (예: URL, 헤더, 데이터)
res (response)
서버가 클라이언트에게 보내는 응답을 제어 (예: HTML, JSON, 상태 코드)

  • http://localhost:9999/
    CSR(클라이언트 사이드 렌더링) 방식으로 처리
    서버는 정적 파일을 제공하고, 브라우저가 React로 페이지를 렌더링
  • http://localhost:9999/app
    SSR(서버 사이드 렌더링) 방식으로 처리
    서버가 React 컴포넌트를 HTML로 렌더링해 클라이언트에 전달
HTML 생성하기  const { html } = ssr(); 
ssr() 함수를 호출하여
React의 <App /> 컴포넌트를 HTML 문자열로 변환하고,
결과를 html 변수에 저장합니다.
index.html 파일 읽기  const index = fs.readFileSync('./dist/client/index.html'); 
index.html 파일을 텍스트 형태로 읽어옵니다.
HTML 내용 삽입  const result = ${index}.replace('<!-- root-container -->', html); 
읽어온 📄index.html 파일에서 <!-- root-container --> 주석
위에서 생성한 html 값으로 대체합니다.
(<App /> 컴포넌트를 HTML 문자열로 변환한 결과)
응답 보내기  res.setHeader('Content-Type', 'text/html').send(result); 
클라이언트에게 HTML 콘텐츠를 응답으로 보냅니다.

setHeader('Content-Type', 'text/html')
클라이언트가 응답이 HTML 문서임을 인식하도록 돕습니다.
서버 실행  app.listen(9999); 
서버를 시작합니다.
이제 http://localhost:9999에서 웹사이트에 접속할 수 있습니다.

 

// ▶ 📜server.js
import express from 'express';
import fs from 'fs';
import ssr from './dist/server/ssr.js'; // ssr.tsx에서 export한 render 함수를 가져옴

const app = express();

app.use('/', express.static('dist/client')); // 클라이언트 정적 파일 제공
app.use('/app', (req, res) => {
    const { html } = ssr(); // ssr.tsx의 render 함수 호출

    const index = fs.readFileSync('./dist/client/index.html'); // 클라이언트 HTML 파일 읽기
    const result = `${index}`.replace('<!-- root-container -->', html); // HTML 삽입    

    res.setHeader('Content-Type', 'text/html').send(result); // 클라이언트에 응답
});

app.listen(9999); // 서버 시작

 


✅ Vite 빌드 명령어

리액트 앱을 배포하기 전에 Vite를 사용해 코드를 빌드해야 합니다.

빌드를 통해 최적화된 파일을 생성할 수 있습니다.

 

💻 클라이언트 빌드

  • 주요 파일 : main.tsx
  • 빌드 결과물 : dist/client 폴더 저장

클라이언트 애플리케이션을 브라우저에서 실행할 준비를 합니다.

hydrateRoot 함수를 사용해 서버에서 이미 렌더링된 HTML과 클라이언트의 React 애플리케이션을 연결합니다.

이 과정에서 최종적으로 생성되는 HTML, CSS, JavaScript 파일들이 브라우저에서 실행됩니다.

 

  • --ssrManifest
    서버에서 어떤 React 컴포넌트가 HTML로 변환되었는지와 
    그 컴포넌트를 표시하기 위해 필요한 CSS와 JavaScript 파일의 경로를 포함하는 메타데이터 파일
  • --outDir : 빌드된 파일이 저장될 폴더 경로를 지정 (예: 📁dist/client 폴더에 결과물이 저장됨)
npx vite build --ssrManifest --outDir dist/client

 


🗄️ 서버 빌드

  • 주요 파일 : ssr.tsx, server.js
  • 빌드 결과물 : dist/server 폴더 저장

🗄️ ssr.tsx
React 컴포넌트를 HTML로 렌더링하는 기능을 제공하며,

renderToString을 사용하여 <App />을 HTML로 변환합니다.

 

🗄️ server.js
Express.js 서버를 설정하고 클라이언트에 HTML을 전달하는 역할을 합니다.
클라이언트가 요청을 보낼 때 ssr.tsx에서 생성된 HTML을 포함한 응답을 반환합니다.

 

  • --ssr
    React 컴포넌트를 렌더링하여 HTML로 변환하는 로직이 있는 파일에서만 사용할 수 있는 옵션
    주로 renderToString 함수를 사용하는 ssr.tsx 파일이 이에 해당
  • --outDir : 빌드된 파일이 저장될 폴더 경로를 지정 (예: 📁dist/server 폴더에 결과물이 저장됨)
npx vite build --ssr components/ssr.tsx --outDir dist/server

 


📜 클라이언트와 서버의 빌드 동시 단축어 설정 (package.json)

'📜package.json' ➡️ scripts 카테고리에 단축어 추가

 

"📜package.json" 파일은 프로젝트의 설정과 의존성을 관리하는 중요한 파일입니다.

두 개의 빌드 프로세스를 동시에 실행하기 위해, 

scripts 카테고리에 다음과 같은 단축어를 추가합니다.

// 클라이언트와 서버 빌드를 동시에 실행
"build": "npm run build:client & npm run build:server",
// 클라이언트 애플리케이션을 빌드하여 dist/client 폴더에 저장
"build:client": "npx vite build --ssrManifest --outDir dist/client",
// 서버 애플리케이션을 빌드하여 dist/server 폴더에 저장
"build:server": "npx vite build --ssr components/ssr.tsx --outDir dist/server",

 

Express 서버를 실행하기 위해, scripts 카테고리에 다음과 같은 단축어를 추가합니다.

// server.js 파일을 실행하여 서버를 시작
"server": "node server.js",

 

✅ Vite 빌드 명령어

"📜package.json" 파일의 scripts 카테고리에 클라이언트와 서버의 빌드 단축어를 설정했으므로,

아래의 명령어로 클라이언트와 서버를 동시에 빌드할 수 있습니다.

npm run build

 

 

📜 package.json

// ▶ 📜package.json
{
  "name": "project",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "npx vite dev",
    "build": "npm run build:client & npm run build:server",
    "build:client": "npx vite build --ssrManifest --outDir dist/client",
    "build:server": "npx vite build --ssr components/ssr.tsx --outDir dist/server",
    "server": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react": "^4.3.1",
    "express": "^4.21.0",
    "fs": "^0.0.1-security",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "typescript": "^5.5.4",
    "vite": "^5.4.0"
  }
}

 


🎯 서버 실행 명령어

"📜package.json" 파일의 scripts 카테고리에 서버 실행 단축어를 설정했으므로,

아래의 명령어로 서버를 실행합니다.

기존에 실행 중인 리액트 서버가 있다면 종료한 후 실행합니다.

npm run server

 


🌐 브라우저 실행

해당 URL에 접속하면, 서버에서 미리 렌더링된 HTML을 브라우저가 받아와 페이지가 빠르게 로딩됩니다.

파일을 수정할 때는 개발 서버처럼 즉시 반영되지 않으며, 서버 빌드와 재실행이 필요합니다.
브라우저에 변경 사항이 반영되지 않으면 캐시를 지워야 합니다.

 

브라우저의 개발자 도구에서 'Network' 탭을 열고 'app'의 미리보기를 확인하면,

초기 내용이 보입니다.

http://localhost:9999/app

 

 

 


✏️ 서버 사이드 렌더링(SSR) 예시 코드

🗂️ 폴더 구조

.code/day27 
├── components
│   ├── app.tsx
│   ├── main.tsx
│   └── ssr.tsx
├── dist
│   └── client
│       ├── .vite
│       │   └── ssr-manifest.json
│       ├── assets
│       │   └── index-BSlshfwg.js
│       └── index.html
│   └── server
│       └── ssr.js
├── server.js
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts

 

파일명 설명
📁 components/app.tsx 애플리케이션 메인 컴포넌트
📁 components/main.tsx 클라이언트에서 애플리케이션을 시작하는 파일로,
hydrateRoot를 사용해
서버에서 렌더링된 HTML과 App 컴포넌트 연결
📁 components/ssr.tsx 서버 사이드 렌더링(SSR)을 위한 컴포넌트 및 로직을 정의
서버에서 초기 HTML을 생성하는 데 사용
💻 dist/client/.vite/ssr-manifest.json SSR을 위한 메타데이터 파일
클라이언트와 서버 간의 리소스 매핑을 돕습니다.
클라이언트 측 코드와 연결되어 리소스 로딩 최적화
💻 dist/client/assets/index-BSlshfwg.js 클라이언트 측에서 실행되는 JavaScript 번들 파일
애플리케이션의 로직 포함
💻 dist/client/assets/index.html 클라이언트 측 초기 렌더링을 위한 템플릿 HTML 파일
서버 사이드 렌더링된 HTML과 함께 사용되며,
클라이언트에게 제공되는 기본 구조를 정의
🗄️ dist/server/ssr.js 서버 사이드 렌더링을 담당하는 스크립트
App 컴포넌트를 서버에서 HTML 문자열로 렌더링한 후,
그 결과를 반환
🗄️ server.js SSR을 위한 Express 서버 설정 파일
클라이언트에서 정적 파일을 제공하고,
/app 경로로 들어오는 요청에 대해
ssr.js 파일을 통해 서버 사이드 렌더링된 HTML을 생성하여 반환
📄 index.html 기본 HTML 템플릿 파일
클라이언트와 서버 양쪽에서 초기 렌더링에 사용
클라이언트와 서버 간에 공통으로 사용되는 HTML 구조 정의
⚙️ package.json 프로젝트 설정 및 의존성 관리
빌드 및 서버 실행을 위한 단축어를 scripts 카테고리에 추가
⚙️ tsconfig.json TypeScript 설정
⚙️ vite.config.ts Vite 빌드 도구 설정

 


📁 components/app.tsx

import React, { Fragment, useState } from 'react';

export default function (){

    const [ length, lengthC ] = useState(10);

    return <Fragment>
        <button onClick={()=>lengthC(length+1)}>증가</button>
        <table>
                <tbody>
                    {
                        Array.from({ length }).map((v,idx) => (
                            <tr key={idx}>
                                <td>데이터</td>
                                <td>데이터</td>
                                <td>데이터</td>
                                <td>데이터</td>
                                <td>데이터</td>
                                <td>데이터</td>
                                <td>데이터</td>
                                <td>데이터</td>
                                <td>데이터</td>
                                <td>데이터</td>
                            </tr>
                        ))
                    }
                </tbody>
        </table>
    </Fragment>
}

📁 components/main.tsx

import React from 'react';
import ReactDOM, { hydrateRoot } from 'react-dom/client';
import App from './app';

hydrateRoot(
     document.querySelector("#reactRoot") as HTMLElement,
     <App/>
)

📁 components/ssr.tsx

import React from 'react';
import { renderToString } from 'react-dom/server';
import App from './app';

export default function render(){
     const html = renderToString(
          <App/>
     );

     return { html };
}

💻 dist/client/.vite/ssr-manifest.json

{
  "\u0000F:/ksh/FED_WEB_2024/code/day27/node_modules/react-dom/cjs/react-dom.production.min.js?commonjs-exports": [],
  "\u0000F:/ksh/FED_WEB_2024/code/day27/node_modules/react-dom/index.js?commonjs-module": [],
  "\u0000F:/ksh/FED_WEB_2024/code/day27/node_modules/react/cjs/react-jsx-runtime.production.min.js?commonjs-exports": [],
  "\u0000F:/ksh/FED_WEB_2024/code/day27/node_modules/react/cjs/react.production.min.js?commonjs-exports": [],
  "\u0000F:/ksh/FED_WEB_2024/code/day27/node_modules/react/index.js?commonjs-module": [],
  "\u0000F:/ksh/FED_WEB_2024/code/day27/node_modules/react/jsx-runtime.js?commonjs-module": [],
  "\u0000F:/ksh/FED_WEB_2024/code/day27/node_modules/scheduler/cjs/scheduler.production.min.js?commonjs-exports": [],
  "\u0000F:/ksh/FED_WEB_2024/code/day27/node_modules/scheduler/index.js?commonjs-module": [],
  "\u0000vite/modulepreload-polyfill.js": [],
  "components/app.tsx": [],
  "components/main.tsx": [],
  "index.html": [],
  "node_modules/react-dom/cjs/react-dom.production.min.js": [],
  "node_modules/react-dom/client.js": [],
  "node_modules/react-dom/index.js": [],
  "node_modules/react/cjs/react-jsx-runtime.production.min.js": [],
  "node_modules/react/cjs/react.production.min.js": [],
  "node_modules/react/index.js": [],
  "node_modules/react/jsx-runtime.js": [],
  "node_modules/scheduler/cjs/scheduler.production.min.js": [],
  "node_modules/scheduler/index.js": []
}

💻 dist/client/assets/index-BSlshfwg.js

(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const u of l)if(u.type==="childList")for(const o of u.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function t(l){const u={};return l.integrity&&(u.integrity=l.integrity),l.referrerPolicy&&(u.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?u.credentials="include":l.crossOrigin==="anonymous"?u.credentials="omit":u.credentials="same-origin",u}function r(l){if(l.ep)return;l.ep=!0;const u=t(l);fetch(l.href,u)}})();var Bi={exports:{}},el={},Hi={exports:{}},L={};/**
 * @license React
 * react.production.min.js
 *
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 /*
    app.tsx와 같은 파일이 포함되어 있으며, 
    이는 실제 React 컴포넌트 코드가 포함된 파일을 나타냅니다.
 */
 error(e)}}ba(),bi.exports=ge;var zd=bi.exports,ec,Vi=zd;Vi.createRoot,ec=Vi.hydrateRoot;function Ld(){const[e,n]=Lr.useState(10);return te.jsxs(Lr.Fragment,{children:[te.jsx("button",{onClick:()=>n(e+1),children:"증가"}),te.jsx("table",{children:te.jsx("tbody",{children:Array.from({length:e}).map((t,r)=>te.jsxs("tr",{children:[te.jsx("td",{children:"데이터"}),te.jsx("td",{children:"데이터"}),te.jsx("td",{children:"데이터"}),te.jsx("td",{children:"데이터"}),te.jsx("td",{children:"데이터"}),te.jsx("td",{children:"데이터"}),te.jsx("td",{children:"데이터"}),te.jsx("td",{children:"데이터"}),te.jsx("td",{children:"데이터"}),te.jsx("td",{children:"데이터"})]},r))})})]})}ec(document.querySelector("#reactRoot"),te.jsx(Ld,{}));

💻 dist/client/assets/index.html

<!DOCTYPE html>
<html lang="ko">
    <head>
        <meta charset="UTF-8">
        <title>Vite & React SSR</title>
        <script type="module" crossorigin src="/assets/index-BSlshfwg.js"></script>
    </head>
    <body>
        <div id="reactRoot"><!-- root-container --></div>

    </body>
</html>

🗄️ dist/server/ssr.js

import { jsxs, jsx } from "react/jsx-runtime";
import { renderToString } from "react-dom/server";
import { useState, Fragment } from "react";
function App() {
  const [length, lengthC] = useState(10);
  return /* @__PURE__ */ jsxs(Fragment, { children: [
    /* @__PURE__ */ jsx("button", { onClick: () => lengthC(length + 1), children: "증가" }),
    /* @__PURE__ */ jsx("table", { children: /* @__PURE__ */ jsx("tbody", { children: Array.from({ length }).map((v, idx) => /* @__PURE__ */ jsxs("tr", { children: [
      /* @__PURE__ */ jsx("td", { children: "데이터" }),
      /* @__PURE__ */ jsx("td", { children: "데이터" }),
      /* @__PURE__ */ jsx("td", { children: "데이터" }),
      /* @__PURE__ */ jsx("td", { children: "데이터" }),
      /* @__PURE__ */ jsx("td", { children: "데이터" }),
      /* @__PURE__ */ jsx("td", { children: "데이터" }),
      /* @__PURE__ */ jsx("td", { children: "데이터" }),
      /* @__PURE__ */ jsx("td", { children: "데이터" }),
      /* @__PURE__ */ jsx("td", { children: "데이터" }),
      /* @__PURE__ */ jsx("td", { children: "데이터" })
    ] }, idx)) }) })
  ] });
}
function render() {
  const html = renderToString(
    /* @__PURE__ */ jsx(App, {})
  );
  return { html };
}
export {
  render as default
};

🗄️ server.js

import express from 'express';
import fs from 'fs';
import ssr from './dist/server/ssr.js';

const app = express();

app.use('/', express.static('dist/client'));
app.use('/app', (req, res) => {
    const { html } = ssr();

    const index = fs.readFileSync('./dist/client/index.html');
    const result = `${index}`.replace('<!-- root-container -->', html);    

    res.setHeader('Content-Type', 'text/html').send(result);
});

app.listen(9999);

📄 index.html

<!DOCTYPE html>
<html lang="ko">
    <head>
        <meta charset="UTF-8">
        <title>Vite & React SSR</title>
    </head>
    <body>
        <div id="reactRoot"><!-- root-container --></div>
        <script type="module" src="components/main.tsx"></script>
    </body>
</html>

⚙️ package.json

{
  "name": "project",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "npx vite dev",
    "build": "npm run build:client & npm run build:server",
    "build:client": "npx vite build --ssrManifest --outDir dist/client",
    "build:server": "npx vite build --ssr components/ssr.tsx --outDir dist/server",
    "server": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react": "^4.3.1",
    "express": "^4.21.0",
    "fs": "^0.0.1-security",
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "typescript": "^5.5.4",
    "vite": "^5.4.0"
  }
}

⚙️ tsconfig.json

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "react",
    "module": "ESNext",
    "target": "ESNext",
    "moduleResolution": "Node",
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true
  }
}

⚙️ vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
    plugins: [react()],
    root: "./",
    base: "/",
    build: {
        outDir: "./dist"
    },
    server: {
        port: 9999,
        open: true
    }
});

 


728x90
반응형