Skip to content

Commit 198947b

Browse files
committed
feat: ssg 파일 생성
1 parent 526b2f2 commit 198947b

File tree

1 file changed

+195
-12
lines changed

1 file changed

+195
-12
lines changed
Lines changed: 195 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,203 @@
1-
import fs from "fs";
1+
import fs from "fs/promises";
2+
import path from "path";
3+
import { fileURLToPath } from "url";
4+
import { mockGetProducts } from "./src/api/mockApi.js";
25

3-
const render = () => {
4-
return `<div>안녕하세요</div>`;
5-
};
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = path.dirname(__filename);
68

9+
// 디렉토리 경로 설정
10+
const PROJECT_ROOT = path.join(__dirname, "../..");
11+
const DIST_DIR = path.join(PROJECT_ROOT, "dist", "vanilla-ssg");
12+
const CLIENT_TEMPLATE = path.join(PROJECT_ROOT, "dist", "vanilla", "index.html");
13+
const SERVER_MODULE = path.join(__dirname, "dist", "vanilla-ssr", "main-server.js");
14+
15+
/**
16+
* SSG에서 생성할 페이지 목록을 반환
17+
* @returns {Promise<Array>} 페이지 목록
18+
*/
19+
async function getPages() {
20+
const pages = [];
21+
22+
// 1. 홈페이지
23+
pages.push({
24+
url: "/",
25+
filePath: path.join(DIST_DIR, "index.html"),
26+
description: "홈페이지",
27+
});
28+
29+
// 2. 404 페이지
30+
pages.push({
31+
url: "/404",
32+
filePath: path.join(DIST_DIR, "404.html"),
33+
description: "404 페이지",
34+
});
35+
36+
try {
37+
// 3. 상품 상세 페이지들 (처음 20개 상품)
38+
console.log("📦 상품 목록 로딩 중...");
39+
const productsData = await mockGetProducts({ limit: 20, page: 1 });
40+
41+
for (const product of productsData.products) {
42+
pages.push({
43+
url: `/product/${product.productId}/`,
44+
filePath: path.join(DIST_DIR, "product", product.productId, "index.html"),
45+
description: `상품: ${product.title}`,
46+
});
47+
}
48+
49+
console.log(`✅ ${productsData.products.length}개 상품 페이지 추가됨`);
50+
} catch (error) {
51+
console.warn("⚠️ 상품 데이터 로딩 실패, 기본 페이지만 생성:", error.message);
52+
}
53+
54+
return pages;
55+
}
56+
57+
/**
58+
* 디렉토리가 없으면 생성
59+
* @param {string} dirPath
60+
*/
61+
async function ensureDir(dirPath) {
62+
try {
63+
await fs.mkdir(dirPath, { recursive: true });
64+
} catch (error) {
65+
// 이미 존재하면 무시
66+
if (error.code !== "EEXIST") {
67+
throw error;
68+
}
69+
}
70+
}
71+
72+
/**
73+
* HTML 파일을 지정된 경로에 저장
74+
* @param {string} filePath
75+
* @param {string} html
76+
*/
77+
async function saveHtmlFile(filePath, html) {
78+
const dir = path.dirname(filePath);
79+
await ensureDir(dir);
80+
await fs.writeFile(filePath, html, "utf-8");
81+
}
82+
83+
/**
84+
* 정적 사이트 생성 메인 함수
85+
*/
786
async function generateStaticSite() {
8-
// HTML 템플릿 읽기
9-
const template = fs.readFileSync("../../dist/vanilla/index.html", "utf-8");
87+
try {
88+
console.log("🚀 Static Site Generation 시작...");
89+
90+
// 1. 페이지 목록 생성
91+
console.log("📋 페이지 목록 생성 중...");
92+
const pages = await getPages();
93+
console.log(`📄 총 ${pages.length}개 페이지 생성 예정`);
94+
95+
// 2. 템플릿 및 렌더링 함수 로드
96+
console.log("📄 템플릿 및 렌더링 함수 로드 중...");
97+
const templatePath = CLIENT_TEMPLATE;
1098

11-
// 어플리케이션 렌더링하기
12-
const appHtml = render();
99+
let template;
100+
try {
101+
template = await fs.readFile(templatePath, "utf-8");
102+
} catch (error) {
103+
console.error(`❌ 템플릿 파일을 찾을 수 없음: ${templatePath}`, error);
104+
console.log("💡 먼저 'pnpm run build:ssg' 명령을 실행하세요");
105+
process.exit(1);
106+
}
13107

14-
// 결과 HTML 생성하기
15-
const result = template.replace("<!--app-html-->", appHtml);
16-
fs.writeFileSync("../../dist/vanilla/index.html", result);
108+
let render;
109+
try {
110+
const serverModule = await import(SERVER_MODULE);
111+
render = serverModule.render;
112+
} catch (error) {
113+
console.error(`❌ 서버 렌더링 모듈을 불러올 수 없음: ${SERVER_MODULE}`, error);
114+
console.log("💡 먼저 'pnpm run build:server' 명령을 실행하세요");
115+
process.exit(1);
116+
}
117+
118+
// 3. 각 페이지 렌더링 및 저장
119+
console.log("🎨 페이지 렌더링 시작...");
120+
let successCount = 0;
121+
let errorCount = 0;
122+
123+
for (const page of pages) {
124+
try {
125+
console.log(` 🔄 ${page.description} (${page.url})`);
126+
127+
// 서버 렌더링
128+
const rendered = await render(page.url);
129+
130+
// HTML 템플릿에 삽입
131+
const html = template
132+
.replace("<!--app-head-->", rendered.head ?? "")
133+
.replace("<!--app-html-->", rendered.html ?? "")
134+
.replace(
135+
"</head>",
136+
`<script>window.__INITIAL_DATA__ = ${JSON.stringify(rendered.initialData ?? {})}</script></head>`,
137+
);
138+
139+
// 파일 저장
140+
await saveHtmlFile(page.filePath, html);
141+
142+
console.log(` ✅ ${page.filePath}`);
143+
successCount++;
144+
} catch (error) {
145+
console.error(` ❌ ${page.description} 생성 실패:`, error.message);
146+
errorCount++;
147+
}
148+
}
149+
150+
// 4. 결과 요약
151+
console.log("\n📊 SSG 완료!");
152+
console.log(`✅ 성공: ${successCount}개 페이지`);
153+
if (errorCount > 0) {
154+
console.log(`❌ 실패: ${errorCount}개 페이지`);
155+
}
156+
console.log(`📁 출력 디렉토리: ${DIST_DIR}`);
157+
158+
// 5. 생성된 파일 목록 표시
159+
console.log("\n📋 생성된 파일들:");
160+
const allFiles = await getAllFiles(DIST_DIR);
161+
allFiles.forEach((file) => {
162+
const relPath = path.relative(DIST_DIR, file);
163+
console.log(` • ${relPath}`);
164+
});
165+
166+
console.log("\n🎉 Static Site Generation 완료!");
167+
} catch (error) {
168+
console.error("💥 SSG 실행 중 오류 발생:", error.message);
169+
process.exit(1);
170+
}
171+
}
172+
173+
/**
174+
* 디렉토리 내 모든 HTML 파일 목록 반환
175+
* @param {string} dir
176+
* @returns {Promise<string[]>}
177+
*/
178+
async function getAllFiles(dir) {
179+
const files = [];
180+
181+
try {
182+
const entries = await fs.readdir(dir, { withFileTypes: true });
183+
184+
for (const entry of entries) {
185+
const fullPath = path.join(dir, entry.name);
186+
if (entry.isDirectory()) {
187+
const subFiles = await getAllFiles(fullPath);
188+
files.push(...subFiles);
189+
} else if (entry.name.endsWith(".html")) {
190+
files.push(fullPath);
191+
}
192+
}
193+
} catch (error) {
194+
console.error(error.message);
195+
}
196+
197+
return files;
17198
}
18199

19200
// 실행
20-
generateStaticSite();
201+
if (import.meta.url === `file://${process.argv[1]}`) {
202+
generateStaticSite();
203+
}

0 commit comments

Comments
 (0)