Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions packages/vanilla/deploy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import fs from "fs";
import { fileURLToPath } from "url";
import { dirname, join } from "path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

/**
* 파일 시스템 기반 배포 스크립트
* 빌드된 정적 파일들을 배포 디렉토리로 복사
*/
async function deploy() {
console.log("🚀 파일 시스템 기반 배포 시작...");

const sourceDir = join(__dirname, "../../dist/vanilla");
const deployDir = process.env.DEPLOY_DIR || join(__dirname, "../../dist/deploy/vanilla");

// 소스 디렉토리 확인
if (!fs.existsSync(sourceDir)) {
console.error(`❌ 소스 디렉토리를 찾을 수 없습니다: ${sourceDir}`);
console.error(" 먼저 'pnpm run build:ssg'를 실행해주세요.");
process.exit(1);
}

// 배포 디렉토리 생성
if (fs.existsSync(deployDir)) {
console.log(`🗑️ 기존 배포 디렉토리 삭제: ${deployDir}`);
fs.rmSync(deployDir, { recursive: true, force: true });
}
fs.mkdirSync(deployDir, { recursive: true });

console.log(`📦 소스: ${sourceDir}`);
console.log(`📤 배포: ${deployDir}`);

// 디렉토리 복사 함수
function copyDirectory(src, dest) {
const entries = fs.readdirSync(src, { withFileTypes: true });

for (const entry of entries) {
const srcPath = join(src, entry.name);
const destPath = join(dest, entry.name);

if (entry.isDirectory()) {
fs.mkdirSync(destPath, { recursive: true });
copyDirectory(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}

// 파일 복사
console.log("📋 파일 복사 중...");
copyDirectory(sourceDir, deployDir);

// 배포 정보 파일 생성
const deployInfo = {
timestamp: new Date().toISOString(),
source: sourceDir,
destination: deployDir,
files: countFiles(deployDir),
};

fs.writeFileSync(join(deployDir, ".deploy-info.json"), JSON.stringify(deployInfo, null, 2));

console.log(`✅ 배포 완료!`);
console.log(` 배포 디렉토리: ${deployDir}`);
console.log(` 생성된 파일 수: ${deployInfo.files}`);
console.log(` 배포 정보: .deploy-info.json`);
}

/**
* 디렉토리 내 파일 개수 계산
*/
function countFiles(dir) {
let count = 0;
const entries = fs.readdirSync(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = join(dir, entry.name);
if (entry.isDirectory()) {
count += countFiles(fullPath);
} else {
count++;
}
}

return count;
}

// 실행
deploy().catch((error) => {
console.error("배포 중 오류 발생:", error);
process.exit(1);
});
6 changes: 4 additions & 2 deletions packages/vanilla/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
"type": "module",
"scripts": {
"dev": "vite --port 5173",
"dev:ssr": "PORT=5174 node server.js",
"dev:ssr": "PORT=5173 node server.js",
"build:client": "rm -rf ./dist/vanilla && vite build --outDir ./dist/vanilla && cp ./dist/vanilla/index.html ./dist/vanilla/404.html",
"build:client-for-ssg": "rm -rf ../../dist/vanilla && vite build --outDir ../../dist/vanilla",
"build:server": "vite build --outDir ./dist/vanilla-ssr --ssr src/main-server.js",
"build:ssg": "pnpm run build:client-for-ssg && node static-site-generate.js",
"build:ssg": "pnpm run build:client-for-ssg && pnpm run build:server && node static-site-generate.js",
"build:without-ssg": "pnpm run build:client && pnpm run build:server",
"build": "pnpm run build:client && pnpm run build:server && pnpm run build:ssg",
"lint:fix": "eslint --fix ./src",
Expand All @@ -20,6 +20,8 @@
"preview:ssr-with-build": "pnpm run build:without-ssg && pnpm run preview:ssr",
"preview:ssg": "vite preview --outDir ../../dist/vanilla --port 4178",
"preview:ssg-with-build": "pnpm run build && pnpm run preview:ssg",
"deploy": "node deploy.js",
"deploy:build": "pnpm run build:ssg && pnpm run deploy",
"serve:test:dev": "concurrently -n \"DevCSR,DevSSR,ProdCSR,ProdSSR,SSG\" -c \"#FF6B6B,#006D77,#FFD166,#6A5ACD,#00C2A8\" \"pnpm run dev\" \"pnpm run dev:ssr\" \"pnpm run preview:csr\" \"pnpm run preview:ssr\" \"pnpm run preview:ssg\"",
"serve:test": "pnpm run build:without-ssg && pnpm run build:ssg && pnpm run serve:test:dev",
"prepare": "husky"
Expand Down
226 changes: 206 additions & 20 deletions packages/vanilla/server.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,220 @@
import express from "express";
import compression from "compression";
import sirv from "sirv";
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import { readdirSync, readFileSync } from "fs";
import { render } from "./dist/vanilla-ssr/main-server.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const prod = process.env.NODE_ENV === "production";
const port = process.env.PORT || 5173;
const base = process.env.BASE || (prod ? "/front_7th_chapter4-1/vanilla/" : "/");

const app = express();

const render = () => {
return `<div>안녕하세요</div>`;
};
/**
* 프로덕션 환경에서 빌드된 파일명 찾기
*/
function findAssetFiles() {
if (!prod) {
return { js: "index.js", css: "index.css" };
}

try {
const assetsDir = join(__dirname, "dist/vanilla/assets");
const files = readdirSync(assetsDir);

const jsFile = files.find((file) => file.startsWith("index-") && file.endsWith(".js"));
const cssFile = files.find((file) => file.startsWith("index-") && file.endsWith(".css"));

return {
js: jsFile ? `assets/${jsFile}` : "assets/index.js",
css: cssFile ? `assets/${cssFile}` : "assets/index.css",
};
} catch (error) {
console.warn("Failed to find asset files, using default names:", error.message);
return { js: "assets/index.js", css: "assets/index.css" };
}
}

const assetFiles = findAssetFiles();

/**
* HTML 템플릿 생성 함수
*/
function createHtmlTemplate(html, headContent = "", baseUrl, isProd, assets, initialData = null) {
const cssPath = `${baseUrl}${assets.css}`;
const jsPath = `${baseUrl}${assets.js}`;

// 개발 환경에서는 index.html을 읽어서 사용
// 프로덕션 환경에서는 빌드된 index.html을 사용
let template;
try {
const templatePath = prod ? join(__dirname, "dist/vanilla/index.html") : join(__dirname, "index.html");
template = readFileSync(templatePath, "utf-8");
} catch {
// 템플릿 파일이 없으면 기본 템플릿 사용
template = `
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<!--app-head-->
<link rel="stylesheet" href="${cssPath}">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#6b7280'
}
}
}
}
</script>
</head>
<body class="bg-gray-50">
<div id="root"><!--app-html--></div>
<script type="module" src="${jsPath}"></script>
</body>
</html>`.trim();
}

// 플레이스홀더 치환
let result = template.replace("<!--app-html-->", html);

// app-head 치환 (headContent가 있으면 추가, 없으면 제거)
if (headContent) {
result = result.replace("<!--app-head-->", headContent);
} else {
result = result.replace("<!--app-head-->", "");
}

// 프로덕션 환경에서 에셋 경로 업데이트
if (prod) {
result = result.replace(/href="\/src\/styles\.css"/g, `href="${cssPath}"`);
result = result.replace(/src="\/src\/main\.js"/g, `src="${jsPath}"`);
}

// 초기 데이터 스크립트 주입 (Hydration을 위해)
if (initialData) {
const initialDataScript = `<script>window.__INITIAL_DATA__ = ${JSON.stringify(initialData)};</script>`;
// </body> 태그 앞에 스크립트 삽입
result = result.replace("</body>", `${initialDataScript}\n </body>`);
}

app.get("*all", (req, res) => {
res.send(
`
return result;
}

/**
* SSR 렌더링 미들웨어
*/
async function ssrMiddleware(req, res, next) {
try {
const url = req.originalUrl.replace(base, "/") || "/";
const query = req.query;

// SSR 렌더링 (html과 initialData 반환)
const { html, initialData } = await render(url, query);

// HTML 템플릿 생성 및 응답 (initialData 포함)
const template = createHtmlTemplate(html, "", base, prod, assetFiles, initialData);
res.setHeader("Content-Type", "text/html");
res.send(template);
} catch (error) {
if (prod) {
console.error("SSR Error:", error.message);
} else {
console.error("SSR Error:", error);
}
next(error);
}
}

/**
* 에러 핸들링 미들웨어
*/
// eslint-disable-next-line no-unused-vars
function errorMiddleware(err, req, res, next) {
if (prod) {
console.error("Server Error:", err.message);
} else {
console.error("Server Error:", err);
}

const errorMessage = prod ? "서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요." : err.message;
const errorDetails = prod
? ""
: `<pre style="text-align: left; margin-top: 20px; padding: 10px; background: #f5f5f5; border-radius: 4px; overflow-x: auto;">${err.stack}</pre>`;

res.status(500).send(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vanilla Javascript SSR</title>
</head>
<body>
<div id="app">${render()}</div>
</body>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Server Error</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-50">
<div class="min-h-screen flex items-center justify-center px-4">
<div class="max-w-md w-full bg-white rounded-lg shadow-lg p-6 text-center">
<div class="text-red-500 mb-4">
<svg class="mx-auto h-16 w-16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
</div>
<h1 class="text-2xl font-bold text-gray-900 mb-2">서버 오류가 발생했습니다</h1>
<p class="text-gray-600 mb-4">${errorMessage}</p>
${errorDetails}
<a href="${base}" class="inline-block mt-6 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors">
홈으로 돌아가기
</a>
</div>
</div>
</body>
</html>
`.trim(),
);
});
`);
}

// 압축 미들웨어
app.use(compression());

// 정적 파일 서빙 (빌드된 클라이언트 파일들)
if (prod) {
app.use(base, sirv(join(__dirname, "dist/vanilla"), { gzip: true, maxAge: 31536000 }));
} else {
// 개발 환경에서는 Vite가 정적 파일을 서빙하므로 여기서는 SSR만 처리
// 개발 환경에서는 소스맵 등 디버깅 정보 제공
app.use((req, res, next) => {
if (req.path.startsWith("/assets/")) {
console.log(`[DEV] Asset request: ${req.path}`);
}
next();
});
}

// SSR 렌더링 미들웨어
app.use("*", ssrMiddleware);

// 에러 핸들링 미들웨어
app.use(errorMiddleware);

// Start http server
// 서버 시작
app.listen(port, () => {
console.log(`React Server started at http://localhost:${port}`);
console.log("=".repeat(50));
console.log(`🚀 Vanilla SSR Server started`);
console.log(`📍 URL: http://localhost:${port}`);
console.log(`📂 Base path: ${base}`);
console.log(`🌍 Environment: ${prod ? "production" : "development"}`);
if (prod) {
console.log(`📦 Assets: ${assetFiles.js}, ${assetFiles.css}`);
}
console.log("=".repeat(50));
});
Loading
Loading