Skip to content

Commit 78775ae

Browse files
Refactor : 코드 리팩토링
1 parent 2b47aff commit 78775ae

File tree

10 files changed

+86
-125
lines changed

10 files changed

+86
-125
lines changed

packages/vanilla/server.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
import compression from "compression";
22
import express from "express";
3-
import fs from "fs";
3+
import fs from "fs/promises";
44
import sirv from "sirv";
55
import { mswServer } from "./src/mocks/serverBrowser.js";
66

77
// 환경 변수 및 설정
88
const isProd = process.env.NODE_ENV === "production";
99
const port = process.env.PORT || 5173;
1010
const baseUrl = process.env.BASE || (isProd ? "/front_6th_chapter4-1/vanilla/" : "/");
11-
const templateHtml = isProd ? fs.readFileSync("dist/vanilla/index.html", "utf-8") : "";
11+
const templateHtml = isProd ? await fs.readFile("dist/vanilla/index.html", "utf-8") : "";
1212
const app = express();
1313

14-
// 런타임에 결정되는 변수들 (개발/프로덕션 환경에 따라 달라짐)
15-
let template;
16-
let render;
1714
let vite;
1815

1916
// MSW 서버 시작 (API 모킹을 위해)
@@ -39,8 +36,11 @@ if (isProd) {
3936
}
4037

4138
// 모든 라우트를 처리하는 SSR 핸들러
42-
app.get("*all", async (req, res) => {
39+
app.get(/^(?!.*\/api).*/, async (req, res) => {
4340
try {
41+
let template;
42+
let render;
43+
4444
if (!isProd) {
4545
// 개발 환경: 매 요청마다 템플릿을 다시 읽고 변환
4646
template = await fs.readFile("./index.html", "utf-8");
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
/**
2+
* 서버 환경용 메모리 스토리지 - localStorage API와 동일한 인터페이스 제공
3+
* @returns {Object} localStorage와 호환되는 메서드를 가진 객체
4+
*/
15
export const createServerStorage = () => {
26
const storage = new Map();
37

48
return {
59
getItem: (key) => storage.get(key),
610
setItem: (key, value) => storage.set(key, value),
711
removeItem: (key) => storage.delete(key),
12+
clear: () => storage.clear(),
813
};
914
};

packages/vanilla/src/lib/createStorage.js

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import { createServerStorage } from "./createServerStorage.js";
12
/**
23
* 환경별 스토리지 추상화 함수 - 브라우저/서버 환경에서 동일한 인터페이스 제공
34
* @param {string} key - 스토리지 키 (예: "cart", "user-preferences")
45
* @param {Storage} storage - 스토리지 구현체 (기본값: 환경별 자동 선택)
56
* @returns {Object} { get, set, reset } 스토리지 조작 메서드들
67
*/
7-
export const createStorage = (key, storage = typeof window === "undefined" ? memoryStorage() : window.localStorage) => {
8+
export const createStorage = (
9+
key,
10+
storage = typeof window === "undefined" ? createServerStorage() : window.localStorage,
11+
) => {
812
// 데이터 조회
913
const get = () => {
1014
try {
@@ -36,18 +40,3 @@ export const createStorage = (key, storage = typeof window === "undefined" ? mem
3640

3741
return { get, set, reset };
3842
};
39-
40-
/**
41-
* 서버 환경용 메모리 스토리지 - localStorage API와 동일한 인터페이스 제공
42-
* @returns {Object} localStorage와 호환되는 메서드를 가진 객체
43-
*/
44-
const memoryStorage = () => {
45-
const storage = new Map(); // 메모리 내 데이터 저장소
46-
47-
return {
48-
getItem: (key) => storage.get(key),
49-
setItem: (key, value) => storage.set(key, value),
50-
removeItem: (key) => storage.delete(key),
51-
clear: () => storage.clear(),
52-
};
53-
};

packages/vanilla/src/main-server.js

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,8 @@ router.addRoute(".*", NotFoundPage);
1717
*/
1818
export const render = async (url = "", query) => {
1919
try {
20-
// URL 정규화: 베이스 URL이 포함된 경우 제거
21-
const baseUrl = "/front_6th_chapter4-1/vanilla/";
22-
let normalizedUrl = url;
23-
24-
if (url.includes(baseUrl)) {
25-
normalizedUrl = url.replace(baseUrl, "/");
26-
if (normalizedUrl === "/") normalizedUrl = "/";
27-
} else if (url === baseUrl.slice(0, -1)) {
28-
// 마지막 슬래시 없는 경우
29-
normalizedUrl = "/";
30-
}
31-
3220
// 서버사이드 라우터 시작
33-
router.start(normalizedUrl, query);
21+
router.start(url, query);
3422

3523
const route = router.route;
3624
if (!route) {

packages/vanilla/src/main.js

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,62 @@ const enableMocking = () =>
1717
}),
1818
);
1919

20-
function main() {
21-
// 서버 데이터 복원
22-
if (window.__INITIAL_DATA__) {
23-
const data = window.__INITIAL_DATA__;
24-
if (data.products) productStore.dispatch(PRODUCT_ACTIONS.SETUP, data);
25-
if (data.currentProduct) productStore.dispatch(PRODUCT_ACTIONS.SET_CURRENT_PRODUCT, data);
26-
delete window.__INITIAL_DATA__;
20+
// SSR 데이터를 클라이언트 스토어에 hydrate
21+
function hydrateFromSSRData() {
22+
if (typeof window === "undefined" || !window.__INITIAL_DATA__) {
23+
return;
24+
}
25+
26+
try {
27+
const initialData = window.__INITIAL_DATA__;
28+
29+
const currentPath = window.location.pathname;
30+
31+
// 홈페이지 hydration
32+
if (currentPath === "/" && initialData.products) {
33+
productStore.dispatch({
34+
type: PRODUCT_ACTIONS.SETUP,
35+
payload: {
36+
products: initialData.products || [],
37+
totalCount: initialData.totalCount || 0,
38+
categories: initialData.categories || {},
39+
currentProduct: null,
40+
relatedProducts: [],
41+
loading: false,
42+
error: null,
43+
status: "done",
44+
},
45+
});
46+
}
47+
// 상품 상세 페이지 hydration
48+
else if (currentPath.includes("/product/") && initialData.product) {
49+
productStore.dispatch({
50+
type: PRODUCT_ACTIONS.SETUP,
51+
payload: {
52+
products: [],
53+
totalCount: 0,
54+
categories: {},
55+
currentProduct: initialData.product,
56+
relatedProducts: initialData.relatedProducts || [],
57+
loading: false,
58+
error: null,
59+
status: "done",
60+
},
61+
});
62+
}
63+
64+
// hydration 완료 플래그
65+
window.__HYDRATED__ = true;
66+
} catch (error) {
67+
console.error("❌ SSR hydration 실패", error);
2768
}
69+
}
2870

71+
function main() {
2972
registerAllEvents();
3073
registerGlobalEvents();
3174
loadCartFromStorage();
75+
hydrateFromSSRData();
3276
initRender();
3377
router.start();
3478
}

packages/vanilla/src/mocks/handlers.js

Lines changed: 4 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ function filterProducts(products, query) {
6363
}
6464

6565
export const handlers = [
66-
// 상품 목록 API (상대 경로)
67-
http.get("/api/products", async ({ request }) => {
66+
// 상품 목록 API
67+
http.get("*/api/products", async ({ request }) => {
6868
const url = new URL(request.url);
6969
const page = parseInt(url.searchParams.get("page") ?? url.searchParams.get("current")) || 1;
7070
const limit = parseInt(url.searchParams.get("limit")) || 20;
@@ -111,7 +111,7 @@ export const handlers = [
111111
}),
112112

113113
// 상품 상세 API
114-
http.get("/api/products/:id", ({ params }) => {
114+
http.get("*/api/products/:id", ({ params }) => {
115115
const { id } = params;
116116
const product = items.find((item) => item.productId === id);
117117

@@ -133,76 +133,7 @@ export const handlers = [
133133
}),
134134

135135
// 카테고리 목록 API
136-
http.get("/api/categories", async () => {
137-
const categories = getUniqueCategories();
138-
await delay();
139-
return HttpResponse.json(categories);
140-
}),
141-
142-
// 절대 URL 패턴도 처리 (SSG용)
143-
http.get("http://localhost:5174/api/products", async ({ request }) => {
144-
const url = new URL(request.url);
145-
const page = parseInt(url.searchParams.get("page") ?? url.searchParams.get("current")) || 1;
146-
const limit = parseInt(url.searchParams.get("limit")) || 20;
147-
const search = url.searchParams.get("search") || "";
148-
const category1 = url.searchParams.get("category1") || "";
149-
const category2 = url.searchParams.get("category2") || "";
150-
const sort = url.searchParams.get("sort") || "price_asc";
151-
152-
const filteredProducts = filterProducts(items, {
153-
search,
154-
category1,
155-
category2,
156-
sort,
157-
});
158-
159-
const startIndex = (page - 1) * limit;
160-
const endIndex = startIndex + limit;
161-
const paginatedProducts = filteredProducts.slice(startIndex, endIndex);
162-
163-
const response = {
164-
products: paginatedProducts,
165-
pagination: {
166-
page,
167-
limit,
168-
total: filteredProducts.length,
169-
totalPages: Math.ceil(filteredProducts.length / limit),
170-
hasNext: endIndex < filteredProducts.length,
171-
hasPrev: page > 1,
172-
},
173-
filters: {
174-
search,
175-
category1,
176-
category2,
177-
sort,
178-
},
179-
};
180-
181-
await delay();
182-
return HttpResponse.json(response);
183-
}),
184-
185-
http.get("http://localhost:5174/api/products/:id", ({ params }) => {
186-
const { id } = params;
187-
const product = items.find((item) => item.productId === id);
188-
189-
if (!product) {
190-
return HttpResponse.json({ error: "Product not found" }, { status: 404 });
191-
}
192-
193-
const detailProduct = {
194-
...product,
195-
description: `${product.title}에 대한 상세 설명입니다. ${product.brand} 브랜드의 우수한 품질을 자랑하는 상품으로, 고객 만족도가 높은 제품입니다.`,
196-
rating: Math.floor(Math.random() * 2) + 4,
197-
reviewCount: Math.floor(Math.random() * 1000) + 50,
198-
stock: Math.floor(Math.random() * 100) + 10,
199-
images: [product.image, product.image.replace(".jpg", "_2.jpg"), product.image.replace(".jpg", "_3.jpg")],
200-
};
201-
202-
return HttpResponse.json(detailProduct);
203-
}),
204-
205-
http.get("http://localhost:5174/api/categories", async () => {
136+
http.get("*/api/categories", async () => {
206137
const categories = getUniqueCategories();
207138
await delay();
208139
return HttpResponse.json(categories);
File renamed without changes.

packages/vanilla/src/pages/ProductDetailPage.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -239,12 +239,14 @@ export const ProductDetailPage = withLifecycle(
239239
onMount: () => {
240240
// SSG 서버 실행
241241
if (typeof window !== "undefined") {
242-
const { currentProduct, status } = productStore.getState();
243-
const productId = router.params.id;
244-
// Hydration된 데이터가 있으면 API 호출 스킵
245-
if (window.__HYDRATED__ && currentProduct && currentProduct.productId === productId && status === "done") {
246-
console.log("✅ SSR 데이터 이미 있어서 API 요청 스킵");
247-
return;
242+
if (window.__HYDRATED__) {
243+
const { currentProduct, status } = productStore.getState();
244+
const productId = router.params.id;
245+
// Hydration된 데이터가 있으면 API 호출 스킵
246+
if (currentProduct && currentProduct.productId === productId && status === "done") {
247+
console.log("✅ SSR 데이터 이미 있어서 API 요청 스킵");
248+
return;
249+
}
248250
}
249251

250252
loadProductDetailForPage(router.params.id);

packages/vanilla/src/router/router.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
import { Router, ServerRouter } from "../lib";
33
import { BASE_URL } from "../constants.js";
44

5-
export const router = typeof window === "undefined" ? new ServerRouter("") : new Router(BASE_URL);
5+
export const router = typeof window !== "undefined" ? new Router(BASE_URL) : new ServerRouter(BASE_URL);

packages/vanilla/static-site-generate.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
// SSG(Static Site Generation) 스크립트
22
// 홈페이지와 상품 상세 페이지를 미리 생성하여 정적 파일로 저장
33
import fs from "fs/promises";
4-
import { mswServer } from "./src/mocks/serverBrowser.js";
4+
import { mswServer } from "./src/mocks/mswServer.js";
55
import items from "./src/mocks/items.json" with { type: "json" };
66

77
// 서버 사이드 렌더링 함수 가져오기
88
const { render } = await import("./dist/vanilla-ssr/main-server.js");
99

10+
const BASE = "/front_6th_chapter4-1/vanilla/";
11+
1012
/**
1113
* 주어진 URL을 렌더링하여 HTML 파일로 생성
1214
* @param {string} url - 렌더링할 URL
@@ -39,13 +41,13 @@ async function generateStaticSite() {
3941

4042
try {
4143
// 홈페이지 생성 (루트 경로로 전달)
42-
await writeRoute("/", template, templatePath);
44+
await writeRoute(BASE, template, templatePath);
4345

4446
const productIds = items.slice(1, 10).map((p) => p.productId);
4547

4648
// 각 상품별로 상세 페이지 생성
4749
for (const id of productIds) {
48-
const url = `/product/${id}/`; // 베이스 URL 제거, 상대 경로만 사용
50+
const url = `${BASE}/product/${id}/`;
4951
const outDir = `../../dist/vanilla/product/${id}`;
5052
await fs.mkdir(outDir, { recursive: true });
5153
await writeRoute(url, template, `${outDir}/index.html`);

0 commit comments

Comments
 (0)