From 9690ecece871415d5f3b05f3e3c053523fb5e358 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Wed, 3 Sep 2025 01:42:58 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20server.js=20=EC=84=B8=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vanilla/server.js | 70 ++++++++++++++++++++--------- packages/vanilla/src/main-server.js | 5 ++- 2 files changed, 54 insertions(+), 21 deletions(-) diff --git a/packages/vanilla/server.js b/packages/vanilla/server.js index b9a56d98..07d477cc 100644 --- a/packages/vanilla/server.js +++ b/packages/vanilla/server.js @@ -1,31 +1,61 @@ +import fs from "fs/promises"; import express from "express"; const prod = process.env.NODE_ENV === "production"; const port = process.env.PORT || 5173; const base = process.env.BASE || (prod ? "/front_6th_chapter4-1/vanilla/" : "/"); +const templateHtml = prod ? await fs.readFile("./dist/vanilla/index.html", "utf-8") : ""; + const app = express(); -const render = () => { - return `
안녕하세요
`; -}; - -app.get("*all", (req, res) => { - res.send( - ` - - - - - - Vanilla Javascript SSR - - -
${render()}
- - - `.trim(), - ); +let vite; +if (!prod) { + const { createServer } = await import("vite"); + vite = await createServer({ + server: { middlewareMode: true }, + appType: "custom", + base, + }); + app.use(vite.middlewares); +} else { + const compression = (await import("compression")).default; + const sirv = (await import("sirv")).default; + app.use(compression()); + app.use(base, sirv("./dist/vanilla", { extensions: [] })); +} + +// Serve HTML +app.use("*all", async (req, res) => { + try { + const url = req.originalUrl.replace(base, ""); + + /** @type {string} */ + let template; + /** @type {import('./src/main-server.js').render} */ + let render; + if (!prod) { + // Always read fresh template in development + template = await fs.readFile("./index.html", "utf-8"); + template = await vite.transformIndexHtml(url, template); + render = (await vite.ssrLoadModule("/src/main-server.js")).render; + } else { + template = templateHtml; + render = (await import("./dist/vanilla-ssr/main-server.js")).render; + } + + const rendered = await render(url); + + const html = template + .replace(``, rendered.head ?? "") + .replace(``, rendered.html ?? ""); + + res.status(200).set({ "Content-Type": "text/html" }).send(html); + } catch (e) { + vite?.ssrFixStacktrace(e); + console.log(e.stack); + res.status(500).end(e.stack); + } }); // Start http server diff --git a/packages/vanilla/src/main-server.js b/packages/vanilla/src/main-server.js index 40b58858..ebc66997 100644 --- a/packages/vanilla/src/main-server.js +++ b/packages/vanilla/src/main-server.js @@ -1,4 +1,7 @@ export const render = async (url, query) => { console.log({ url, query }); - return ""; + return { + head: "test", + html: "
test
", + }; }; From 0929d45abd47ca37b7bff55cf6efef2b45dd8f2c Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Thu, 4 Sep 2025 04:15:17 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20msw=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vanilla/server.js | 6 +++++- packages/vanilla/src/mocks/serverBrowser.js | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 packages/vanilla/src/mocks/serverBrowser.js diff --git a/packages/vanilla/server.js b/packages/vanilla/server.js index 07d477cc..c5f812d7 100644 --- a/packages/vanilla/server.js +++ b/packages/vanilla/server.js @@ -1,5 +1,6 @@ import fs from "fs/promises"; import express from "express"; +import { mswServer } from "./src/mocks/serverBrowser.js"; const prod = process.env.NODE_ENV === "production"; const port = process.env.PORT || 5173; @@ -9,6 +10,8 @@ const templateHtml = prod ? await fs.readFile("./dist/vanilla/index.html", "utf- const app = express(); +mswServer.listen({ onUnhandledRequest: "bypass" }); + let vite; if (!prod) { const { createServer } = await import("vite"); @@ -44,10 +47,11 @@ app.use("*all", async (req, res) => { render = (await import("./dist/vanilla-ssr/main-server.js")).render; } - const rendered = await render(url); + const rendered = await render(url, req.query); const html = template .replace(``, rendered.head ?? "") + .replace(``, ``) .replace(``, rendered.html ?? ""); res.status(200).set({ "Content-Type": "text/html" }).send(html); diff --git a/packages/vanilla/src/mocks/serverBrowser.js b/packages/vanilla/src/mocks/serverBrowser.js new file mode 100644 index 00000000..28b28250 --- /dev/null +++ b/packages/vanilla/src/mocks/serverBrowser.js @@ -0,0 +1,4 @@ +import { setupServer } from "msw/node"; +import { handlers } from "./handlers.js"; + +export const mswServer = setupServer(...handlers); From c0c9b6b674799bb8a4ea6f9b20b25763e8eaf1e5 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Thu, 4 Sep 2025 04:16:14 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=EC=A0=88=EB=8C=80=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C,=20=EC=99=80=EC=9D=BC=EB=93=9C=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vanilla/src/api/productApi.js | 8 +++++--- packages/vanilla/src/mocks/handlers.js | 8 ++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/vanilla/src/api/productApi.js b/packages/vanilla/src/api/productApi.js index c2330fbe..519cc69d 100644 --- a/packages/vanilla/src/api/productApi.js +++ b/packages/vanilla/src/api/productApi.js @@ -1,3 +1,5 @@ +const BASE_URL = () => (typeof window === "undefined" ? "http://localhost:5174" : ""); + export async function getProducts(params = {}) { const { limit = 20, search = "", category1 = "", category2 = "", sort = "price_asc" } = params; const page = params.current ?? params.page ?? 1; @@ -11,17 +13,17 @@ export async function getProducts(params = {}) { sort, }); - const response = await fetch(`/api/products?${searchParams}`); + const response = await fetch(`${BASE_URL()}/api/products?${searchParams}`); return await response.json(); } export async function getProduct(productId) { - const response = await fetch(`/api/products/${productId}`); + const response = await fetch(`${BASE_URL()}/api/products/${productId}`); return await response.json(); } export async function getCategories() { - const response = await fetch("/api/categories"); + const response = await fetch(`${BASE_URL()}/api/categories`); return await response.json(); } diff --git a/packages/vanilla/src/mocks/handlers.js b/packages/vanilla/src/mocks/handlers.js index 6e3035e6..9cd19b09 100644 --- a/packages/vanilla/src/mocks/handlers.js +++ b/packages/vanilla/src/mocks/handlers.js @@ -1,5 +1,5 @@ import { http, HttpResponse } from "msw"; -import items from "./items.json"; +import items from "./items.json" with { type: "json" }; const delay = async () => await new Promise((resolve) => setTimeout(resolve, 200)); @@ -64,7 +64,7 @@ function filterProducts(products, query) { export const handlers = [ // 상품 목록 API - http.get("/api/products", async ({ request }) => { + http.get("*/api/products", async ({ request }) => { const url = new URL(request.url); const page = parseInt(url.searchParams.get("page") ?? url.searchParams.get("current")) || 1; const limit = parseInt(url.searchParams.get("limit")) || 20; @@ -111,7 +111,7 @@ export const handlers = [ }), // 상품 상세 API - http.get("/api/products/:id", ({ params }) => { + http.get("*/api/products/:id", ({ params }) => { const { id } = params; const product = items.find((item) => item.productId === id); @@ -133,7 +133,7 @@ export const handlers = [ }), // 카테고리 목록 API - http.get("/api/categories", async () => { + http.get("*/api/categories", async () => { const categories = getUniqueCategories(); await delay(); return HttpResponse.json(categories); From 517fc025f559ffe635f997555373ec64334ff827 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Thu, 4 Sep 2025 04:17:17 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20memoryStorage=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vanilla/src/lib/createStorage.js | 12 +++++++++++- packages/vanilla/src/storage/cartStorage.js | 7 +++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/vanilla/src/lib/createStorage.js b/packages/vanilla/src/lib/createStorage.js index 08b504f2..0a91a0c9 100644 --- a/packages/vanilla/src/lib/createStorage.js +++ b/packages/vanilla/src/lib/createStorage.js @@ -1,10 +1,20 @@ +export const memoryStorage = () => { + const storage = new Map(); + + return { + getItem: (key) => storage.get(key), + setItem: (key, value) => storage.set(key, value), + removeItem: (key) => storage.delete(key), + }; +}; + /** * 로컬스토리지 추상화 함수 * @param {string} key - 스토리지 키 * @param {Storage} storage - 기본값은 localStorage * @returns {Object} { get, set, reset } */ -export const createStorage = (key, storage = window.localStorage) => { +export const createStorage = (key, storage = typeof window === "undefined" ? memoryStorage() : window.localStorage) => { const get = () => { try { const item = storage.getItem(key); diff --git a/packages/vanilla/src/storage/cartStorage.js b/packages/vanilla/src/storage/cartStorage.js index 7aa68383..85488c55 100644 --- a/packages/vanilla/src/storage/cartStorage.js +++ b/packages/vanilla/src/storage/cartStorage.js @@ -1,3 +1,6 @@ -import { createStorage } from "../lib"; +import { memoryStorage, createStorage } from "../lib"; -export const cartStorage = createStorage("shopping_cart"); +export const cartStorage = createStorage( + "shopping_cart", + typeof window === "undefined" ? memoryStorage() : window.localStorage, +); From ffed02ee48f8219067e2aab78c4c3fa6def40ed1 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Thu, 4 Sep 2025 04:18:14 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20serverRouter=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EB=B0=8F=20=EB=9D=BC=EC=9A=B0=ED=84=B0=20=EB=B6=84=EA=B8=B0?= =?UTF-8?q?=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vanilla/index.html | 15 +- packages/vanilla/src/lib/ServerRouter.js | 177 +++++++++++++++++++++++ packages/vanilla/src/lib/index.js | 1 + packages/vanilla/src/main-server.js | 4 +- packages/vanilla/src/router/router.js | 4 +- 5 files changed, 191 insertions(+), 10 deletions(-) create mode 100644 packages/vanilla/src/lib/ServerRouter.js diff --git a/packages/vanilla/index.html b/packages/vanilla/index.html index 483a6d5e..56f70659 100644 --- a/packages/vanilla/index.html +++ b/packages/vanilla/index.html @@ -5,18 +5,19 @@ - + + diff --git a/packages/vanilla/src/lib/ServerRouter.js b/packages/vanilla/src/lib/ServerRouter.js new file mode 100644 index 00000000..b8bb91c1 --- /dev/null +++ b/packages/vanilla/src/lib/ServerRouter.js @@ -0,0 +1,177 @@ +/** + * Server 라우터 + */ +import { createObserver } from "./createObserver.js"; + +export class ServerRouter { + #routes; + #route; + #observer = createObserver(); + #baseUrl; + + constructor(baseUrl = "") { + this.#routes = new Map(); + this.#route = null; + this.#baseUrl = baseUrl.replace(/\/$/, ""); + + // window.addEventListener("popstate", () => { + // this.#route = this.#findRoute(); + // this.#observer.notify(); + // }); + } + + get baseUrl() { + return this.#baseUrl; + } + + get query() { + // return ServerRouter.parseQuery(window.location.search); + return this.#route?.params ?? {}; + } + + set query(newQuery) { + const newUrl = ServerRouter.getUrl(newQuery, this.#baseUrl); + this.push(newUrl); + } + + get params() { + return this.#route?.params ?? {}; + } + + get route() { + return this.#route; + } + + get target() { + return this.#route?.handler; + } + + subscribe(fn) { + this.#observer.subscribe(fn); + } + + /** + * 라우트 등록 + * @param {string} path - 경로 패턴 (예: "/product/:id") + * @param {Function} handler - 라우트 핸들러 + */ + addRoute(path, handler) { + // 경로 패턴을 정규식으로 변환 + const paramNames = []; + const regexPath = path + .replace(/:\w+/g, (match) => { + paramNames.push(match.slice(1)); // ':id' -> 'id' + return "([^/]+)"; + }) + .replace(/\//g, "\\/"); + + const regex = new RegExp(`^${this.#baseUrl}${regexPath}$`); + + this.#routes.set(path, { + regex, + paramNames, + handler, + }); + } + + // #findRoute(url = window.location.pathname) { + #findRoute(url = "/", origin = "http://localhost") { + const { pathname } = new URL(url, origin); + for (const [routePath, route] of this.#routes) { + const match = pathname.match(route.regex); + if (match) { + // 매치된 파라미터들을 객체로 변환 + const params = {}; + route.paramNames.forEach((name, index) => { + params[name] = match[index + 1]; + }); + + return { + ...route, + params, + path: routePath, + }; + } + } + return null; + } + + /** + * 네비게이션 실행 + * @param {string} url - 이동할 경로 + */ + push(url = "/") { + try { + this.#route = this.#findRoute(url); + + // baseUrl이 없으면 자동으로 붙여줌 + // let fullUrl = url.startsWith(this.#baseUrl) ? url : this.#baseUrl + (url.startsWith("/") ? url : "/" + url); + + // const prevFullUrl = `${window.location.pathname}${window.location.search}`; + + // // 히스토리 업데이트 + // if (prevFullUrl !== fullUrl) { + // window.history.pushState(null, "", fullUrl); + // } + + // this.#route = this.#findRoute(fullUrl); + // this.#observer.notify(); + } catch (error) { + console.error("라우터 네비게이션 오류:", error); + } + } + + /** + * 라우터 시작 + */ + start() { + this.#route = this.#findRoute(); + this.#observer.notify(); + } + + /** + * 쿼리 파라미터를 객체로 파싱 + * @param {string} search - location.search 또는 쿼리 문자열 + * @returns {Object} 파싱된 쿼리 객체 + */ + // static parseQuery = (search = window.location.search) => { + static parseQuery = (search) => { + const params = new URLSearchParams(search); + const query = {}; + for (const [key, value] of params) { + query[key] = value; + } + return query; + }; + + /** + * 객체를 쿼리 문자열로 변환 + * @param {Object} query - 쿼리 객체 + * @returns {string} 쿼리 문자열 + */ + static stringifyQuery = (query) => { + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(query)) { + if (value !== null && value !== undefined && value !== "") { + params.set(key, String(value)); + } + } + return params.toString(); + }; + + static getUrl = (newQuery, pathname = "/", baseUrl = "") => { + const currentQuery = ServerRouter.parseQuery(); + const updatedQuery = { ...currentQuery, ...newQuery }; + + // 빈 값들 제거 + Object.keys(updatedQuery).forEach((key) => { + if (updatedQuery[key] === null || updatedQuery[key] === undefined || updatedQuery[key] === "") { + delete updatedQuery[key]; + } + }); + + const queryString = ServerRouter.stringifyQuery(updatedQuery); + return `${baseUrl}${pathname.replace(baseUrl, "")}${queryString ? `?${queryString}` : ""}`; + // return `${baseUrl}${window.location.pathname.replace(baseUrl, "")}${queryString ? `?${queryString}` : ""}`; + }; +} diff --git a/packages/vanilla/src/lib/index.js b/packages/vanilla/src/lib/index.js index a598ef30..da63f1df 100644 --- a/packages/vanilla/src/lib/index.js +++ b/packages/vanilla/src/lib/index.js @@ -2,3 +2,4 @@ export * from "./createObserver"; export * from "./createStore"; export * from "./createStorage"; export * from "./Router"; +export * from "./ServerRouter"; diff --git a/packages/vanilla/src/main-server.js b/packages/vanilla/src/main-server.js index ebc66997..f8a732c6 100644 --- a/packages/vanilla/src/main-server.js +++ b/packages/vanilla/src/main-server.js @@ -1,7 +1,9 @@ +import { HomePage } from "./pages"; + export const render = async (url, query) => { console.log({ url, query }); return { head: "test", - html: "
test
", + html: HomePage(), }; }; diff --git a/packages/vanilla/src/router/router.js b/packages/vanilla/src/router/router.js index d897ee76..75e7ba3c 100644 --- a/packages/vanilla/src/router/router.js +++ b/packages/vanilla/src/router/router.js @@ -1,5 +1,5 @@ // 글로벌 라우터 인스턴스 -import { Router } from "../lib"; +import { Router, ServerRouter } from "../lib"; import { BASE_URL } from "../constants.js"; -export const router = new Router(BASE_URL); +export const router = typeof window !== "undefined" ? new Router(BASE_URL) : new ServerRouter(BASE_URL); From c3b81df7d73aceb28e2c7bfdb994bad6460b5650 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Fri, 5 Sep 2025 02:32:28 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20ssr=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=20=ED=86=B5=EA=B3=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vanilla/server.js | 8 +- packages/vanilla/src/lib/ServerRouter.js | 42 +++--- packages/vanilla/src/main-server.js | 91 +++++++++++- packages/vanilla/src/main.js | 21 +++ packages/vanilla/src/mocks/server.js | 135 ++++++++++++++++++ packages/vanilla/src/pages/HomePage.js | 35 ++++- .../vanilla/src/pages/ProductDetailPage.js | 47 +++++- 7 files changed, 343 insertions(+), 36 deletions(-) create mode 100644 packages/vanilla/src/mocks/server.js diff --git a/packages/vanilla/server.js b/packages/vanilla/server.js index c5f812d7..b91851b7 100644 --- a/packages/vanilla/server.js +++ b/packages/vanilla/server.js @@ -31,7 +31,13 @@ if (!prod) { // Serve HTML app.use("*all", async (req, res) => { try { - const url = req.originalUrl.replace(base, ""); + let url = req.originalUrl; + if (base !== "/" && url.startsWith(base)) { + url = url.replace(base, ""); + } + if (!url.startsWith("/")) { + url = "/" + url; + } /** @type {string} */ let template; diff --git a/packages/vanilla/src/lib/ServerRouter.js b/packages/vanilla/src/lib/ServerRouter.js index b8bb91c1..73b95abb 100644 --- a/packages/vanilla/src/lib/ServerRouter.js +++ b/packages/vanilla/src/lib/ServerRouter.js @@ -8,16 +8,12 @@ export class ServerRouter { #route; #observer = createObserver(); #baseUrl; + #serverQuery = {}; constructor(baseUrl = "") { this.#routes = new Map(); this.#route = null; this.#baseUrl = baseUrl.replace(/\/$/, ""); - - // window.addEventListener("popstate", () => { - // this.#route = this.#findRoute(); - // this.#observer.notify(); - // }); } get baseUrl() { @@ -25,13 +21,21 @@ export class ServerRouter { } get query() { - // return ServerRouter.parseQuery(window.location.search); - return this.#route?.params ?? {}; + // 서버 사이드에서는 #serverQuery를 사용, 클라이언트 사이드에서는 URL 파라미터 사용 + if (typeof window === "undefined") { + return this.#serverQuery; + } + return ServerRouter.parseQuery(window.location.search); } set query(newQuery) { - const newUrl = ServerRouter.getUrl(newQuery, this.#baseUrl); - this.push(newUrl); + // 서버 사이드에서는 #serverQuery를 설정, 클라이언트 사이드에서는 URL 업데이트 + if (typeof window === "undefined") { + this.#serverQuery = { ...newQuery }; + } else { + const newUrl = ServerRouter.getUrl(newQuery, this.#baseUrl); + this.push(newUrl); + } } get params() { @@ -65,7 +69,7 @@ export class ServerRouter { }) .replace(/\//g, "\\/"); - const regex = new RegExp(`^${this.#baseUrl}${regexPath}$`); + const regex = new RegExp(`^${this.#baseUrl}${regexPath}/?$`); this.#routes.set(path, { regex, @@ -74,7 +78,6 @@ export class ServerRouter { }); } - // #findRoute(url = window.location.pathname) { #findRoute(url = "/", origin = "http://localhost") { const { pathname } = new URL(url, origin); for (const [routePath, route] of this.#routes) { @@ -102,20 +105,11 @@ export class ServerRouter { */ push(url = "/") { try { - this.#route = this.#findRoute(url); - // baseUrl이 없으면 자동으로 붙여줌 - // let fullUrl = url.startsWith(this.#baseUrl) ? url : this.#baseUrl + (url.startsWith("/") ? url : "/" + url); - - // const prevFullUrl = `${window.location.pathname}${window.location.search}`; - - // // 히스토리 업데이트 - // if (prevFullUrl !== fullUrl) { - // window.history.pushState(null, "", fullUrl); - // } + let fullUrl = url.startsWith(this.#baseUrl) ? url : this.#baseUrl + (url.startsWith("/") ? url : "/" + url); - // this.#route = this.#findRoute(fullUrl); - // this.#observer.notify(); + this.#route = this.#findRoute(fullUrl); + this.#observer.notify(); } catch (error) { console.error("라우터 네비게이션 오류:", error); } @@ -134,7 +128,6 @@ export class ServerRouter { * @param {string} search - location.search 또는 쿼리 문자열 * @returns {Object} 파싱된 쿼리 객체 */ - // static parseQuery = (search = window.location.search) => { static parseQuery = (search) => { const params = new URLSearchParams(search); const query = {}; @@ -172,6 +165,5 @@ export class ServerRouter { const queryString = ServerRouter.stringifyQuery(updatedQuery); return `${baseUrl}${pathname.replace(baseUrl, "")}${queryString ? `?${queryString}` : ""}`; - // return `${baseUrl}${window.location.pathname.replace(baseUrl, "")}${queryString ? `?${queryString}` : ""}`; }; } diff --git a/packages/vanilla/src/main-server.js b/packages/vanilla/src/main-server.js index f8a732c6..9f373fb4 100644 --- a/packages/vanilla/src/main-server.js +++ b/packages/vanilla/src/main-server.js @@ -1,9 +1,90 @@ -import { HomePage } from "./pages"; +import { HomePage, NotFoundPage, ProductDetailPageSSR } from "./pages"; +import { router } from "./router"; +import { getProductsOnServer, getUniqueCategories, getProductDetailOnServer } from "/src/mocks/server.js"; + +router.addRoute("/", () => { + const { + products, + pagination: { total: totalCount }, + } = getProductsOnServer(router.query); + const categories = getUniqueCategories(); + + const results = { + products, + categories, + totalCount, + }; + + return { + initialData: results, + html: HomePage(results), + head: "쇼핑몰 - 홈", + }; +}); + +router.addRoute("/product/:id", () => { + const productId = router.params.id; + const productData = getProductDetailOnServer(productId); + + if (!productData) { + return { + initialData: { error: "상품을 찾을 수 없습니다." }, + html: NotFoundPage(), + head: "상품을 찾을 수 없습니다", + }; + } + + const { product, relatedProducts } = productData; -export const render = async (url, query) => { - console.log({ url, query }); return { - head: "test", - html: HomePage(), + initialData: { + currentProduct: product, + relatedProducts, + }, + html: ProductDetailPageSSR({ + currentProduct: product, + relatedProducts, + }), + head: `${product.title} - 쇼핑몰`, }; +}); + +router.addRoute(".*", () => { + return { + initialData: {}, + html: NotFoundPage(), + head: "404", + }; +}); + +export const render = async (url, query) => { + console.log({ url, query }); + try { + // 1. 서버 라우터에 쿼리 설정 (서버 사이드용) + router.query = query; + + // 2. URL로 네비게이션하여 해당 라우트 찾기 + router.push(url); + + // 3. 현재 라우트의 핸들러가 있는지 확인 + if (!router.target) { + throw new Error(`No route found for URL: ${url}`); + } + + // 4. 핸들러 실행하여 렌더링 결과 얻기 + const result = router.target(); + + return { + ...result, + data: JSON.stringify(result.initialData), + }; + } catch (error) { + console.error(error); + return { + initialData: { error: error.message }, + data: JSON.stringify({ error: error.message }), + html: "
에러발생
", + head: "에러", + }; + } }; diff --git a/packages/vanilla/src/main.js b/packages/vanilla/src/main.js index 4c3f2765..b02ee958 100644 --- a/packages/vanilla/src/main.js +++ b/packages/vanilla/src/main.js @@ -4,6 +4,7 @@ import { registerAllEvents } from "./events"; import { loadCartFromStorage } from "./services"; import { router } from "./router"; import { BASE_URL } from "./constants.js"; +import { productStore } from "./stores"; const enableMocking = () => import("./mocks/browser.js").then(({ worker }) => @@ -16,6 +17,26 @@ const enableMocking = () => ); function main() { + // SSR 데이터가 있으면 store에 설정 + if (window.__INITIAL_DATA__) { + const { currentProduct, relatedProducts, error } = window.__INITIAL_DATA__; + if (currentProduct) { + productStore.setState({ + currentProduct, + relatedProducts: relatedProducts || [], + loading: false, + error: null, + }); + } else if (error) { + productStore.setState({ + currentProduct: null, + relatedProducts: [], + loading: false, + error, + }); + } + } + registerAllEvents(); registerGlobalEvents(); loadCartFromStorage(); diff --git a/packages/vanilla/src/mocks/server.js b/packages/vanilla/src/mocks/server.js new file mode 100644 index 00000000..2086a345 --- /dev/null +++ b/packages/vanilla/src/mocks/server.js @@ -0,0 +1,135 @@ +// import { http, HttpResponse } from "msw"; +import items from "./items.json"; + +// const delay = async () => await new Promise((resolve) => setTimeout(resolve, 200)); + +// 카테고리 추출 함수 +export function getUniqueCategories() { + const categories = {}; + + items.forEach((item) => { + const cat1 = item.category1; + const cat2 = item.category2; + + if (!categories[cat1]) categories[cat1] = {}; + if (cat2 && !categories[cat1][cat2]) categories[cat1][cat2] = {}; + }); + + return categories; +} + +// 상품 검색 및 필터링 함수 +function filterProducts(products, query) { + let filtered = [...products]; + + // 검색어 필터링 + if (query.search) { + const searchTerm = query.search.toLowerCase(); + filtered = filtered.filter( + (item) => item.title.toLowerCase().includes(searchTerm) || item.brand.toLowerCase().includes(searchTerm), + ); + } + + // 카테고리 필터링 + if (query.category1) { + filtered = filtered.filter((item) => item.category1 === query.category1); + } + if (query.category2) { + filtered = filtered.filter((item) => item.category2 === query.category2); + } + + // 정렬 + if (query.sort) { + switch (query.sort) { + case "price_asc": + filtered.sort((a, b) => parseInt(a.lprice) - parseInt(b.lprice)); + break; + case "price_desc": + filtered.sort((a, b) => parseInt(b.lprice) - parseInt(a.lprice)); + break; + case "name_asc": + filtered.sort((a, b) => a.title.localeCompare(b.title, "ko")); + break; + case "name_desc": + filtered.sort((a, b) => b.title.localeCompare(a.title, "ko")); + break; + default: + // 기본은 가격 낮은 순 + filtered.sort((a, b) => parseInt(a.lprice) - parseInt(b.lprice)); + } + } + + return filtered; +} + +export function getProductsOnServer(query = {}) { + const page = parseInt(query.page ?? query.current) || 1; + const limit = parseInt(query.limit) || 20; + const search = query.search || ""; + const category1 = query.category1 || ""; + const category2 = query.category2 || ""; + const sort = query.sort || "price_asc"; + + // 필터링된 상품들 + const filteredProducts = filterProducts(items, { + search, + category1, + category2, + sort, + }); + + // 페이지네이션 + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + const paginatedProducts = filteredProducts.slice(startIndex, endIndex); + + // 응답 데이터 + return { + products: paginatedProducts, + pagination: { + page, + limit, + total: filteredProducts.length, + totalPages: Math.ceil(filteredProducts.length / limit), + hasNext: endIndex < filteredProducts.length, + hasPrev: page > 1, + }, + filters: { + search, + category1, + category2, + sort, + }, + }; +} + +// 상품 상세 데이터를 가져오는 함수 +export function getProductDetailOnServer(productId) { + const product = items.find((item) => item.productId === productId); + + if (!product) { + return null; + } + + // 상세 정보에 추가 데이터 포함 + const detailProduct = { + ...product, + description: `${product.title}에 대한 상세 설명입니다. ${product.brand} 브랜드의 우수한 품질을 자랑하는 상품으로, 고객 만족도가 높은 제품입니다.`, + rating: Math.floor(Math.random() * 2) + 4, // 4~5점 랜덤 + reviewCount: Math.floor(Math.random() * 1000) + 50, // 50~1050개 랜덤 + stock: Math.floor(Math.random() * 100) + 10, // 10~110개 랜덤 + }; + + // 관련 상품 찾기 (같은 카테고리) + const relatedProducts = items + .filter( + (item) => + item.productId !== productId && item.category1 === product.category1 && item.category2 === product.category2, + ) + .slice(0, 20); + + return { + product: detailProduct, + relatedProducts, + }; +} diff --git a/packages/vanilla/src/pages/HomePage.js b/packages/vanilla/src/pages/HomePage.js index ca08c26c..e7995f18 100644 --- a/packages/vanilla/src/pages/HomePage.js +++ b/packages/vanilla/src/pages/HomePage.js @@ -1,5 +1,5 @@ import { ProductList, SearchBar } from "../components"; -import { productStore } from "../stores"; +import { productStore, PRODUCT_ACTIONS } from "../stores"; import { router, withLifecycle } from "../router"; import { loadProducts, loadProductsAndCategories } from "../services"; import { PageWrapper } from "./PageWrapper.js"; @@ -7,6 +7,26 @@ import { PageWrapper } from "./PageWrapper.js"; export const HomePage = withLifecycle( { onMount: () => { + if (typeof window === "undefined") { + console.log("이 코드는 서버에서 실행이 되고 "); + return; + } + if (window.__INITIAL_DATA__?.products?.length > 0) { + console.log("이 코드는 클라이언트에서 실행이 되는데, __INITIAL_DATA__ 가 있을 때에만!"); + const { products, categories, totalCount } = window.__INITIAL_DATA__; + productStore.dispatch({ + type: PRODUCT_ACTIONS.SETUP, + payload: { + products, + categories, + totalCount, + loading: false, + status: "done", + }, + }); + return; + } + console.log("이 코드는 아무것도 없을 때!"); loadProductsAndCategories(); }, watches: [ @@ -17,8 +37,17 @@ export const HomePage = withLifecycle( () => loadProducts(true), ], }, - () => { - const productState = productStore.getState(); + (props = {}) => { + const productState = + props.products?.length > 0 + ? { + products: props.products, + loading: false, + error: null, + totalCount: props.totalCount, + categories: props.categories, + } + : productStore.getState(); const { search: searchQuery, limit, sort, category1, category2 } = router.query; const { products, loading, error, totalCount, categories } = productState; const category = { category1, category2 }; diff --git a/packages/vanilla/src/pages/ProductDetailPage.js b/packages/vanilla/src/pages/ProductDetailPage.js index 73d0ec30..b7bdac51 100644 --- a/packages/vanilla/src/pages/ProductDetailPage.js +++ b/packages/vanilla/src/pages/ProductDetailPage.js @@ -237,7 +237,11 @@ function ProductDetail({ product, relatedProducts = [] }) { export const ProductDetailPage = withLifecycle( { onMount: () => { - loadProductDetailForPage(router.params.id); + // SSR에서 이미 데이터가 로드되었으면 추가 로딩하지 않음 + const state = productStore.getState(); + if (!state.currentProduct || state.currentProduct.productId !== router.params.id) { + loadProductDetailForPage(router.params.id); + } }, watches: [() => [router.params.id], () => loadProductDetailForPage(router.params.id)], }, @@ -247,7 +251,7 @@ export const ProductDetailPage = withLifecycle( return PageWrapper({ headerLeft: `
- +

상품 상세

+
+ `.trim(), + children: error ? ErrorContent({ error }) : ErrorContent({ error: "상품을 찾을 수 없습니다." }), + }); + } + + return PageWrapper({ + headerLeft: ` +
+ +

상품 상세

+
+ `.trim(), + children: ProductDetail({ product, relatedProducts }), + }); +}; From d7f0b19495a4fb192b6958f020a4ee2efb52cd24 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Fri, 5 Sep 2025 03:26:53 +0900 Subject: [PATCH 07/11] =?UTF-8?q?feat:=20ssg=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vanilla/static-site-generate.js | 216 +++++++++++++++++++++-- 1 file changed, 205 insertions(+), 11 deletions(-) diff --git a/packages/vanilla/static-site-generate.js b/packages/vanilla/static-site-generate.js index c479f112..3f5f1d17 100644 --- a/packages/vanilla/static-site-generate.js +++ b/packages/vanilla/static-site-generate.js @@ -1,20 +1,214 @@ import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; -const render = () => { - return `
안녕하세요
`; -}; +// ES 모듈에서 __dirname 대체 +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +// 경로 설정 +const projectRoot = path.resolve(__dirname, "../.."); +const distDir = path.join(projectRoot, "dist/vanilla"); + +/** + * 정적 사이트 생성 메인 함수 + */ async function generateStaticSite() { - // HTML 템플릿 읽기 - const template = fs.readFileSync("../../dist/vanilla/index.html", "utf-8"); + console.log("🚀 정적 사이트 생성을 시작합니다..."); + + try { + // 1. 기존 파일 검증 + await validateExistingFiles(); + + // 2. 상품 데이터 로드 + const items = await loadProductData(); + console.log(`📦 ${items.length}개의 상품을 발견했습니다.`); + + // 3. 추가 정적 페이지 생성 + await generateCategoryPages(items); + await generateSitemap(items); + + // 4. SEO 최적화 + await optimizeSEO(); + + console.log("✅ 정적 사이트 생성이 완료되었습니다!"); + } catch (error) { + console.error("❌ 정적 사이트 생성 중 오류 발생:", error); + process.exit(1); + } +} + +/** + * 기존 생성된 파일들 검증 + */ +async function validateExistingFiles() { + console.log("🔍 기존 파일들을 검증합니다..."); + + const requiredFiles = [path.join(distDir, "index.html"), path.join(distDir, "product")]; + + for (const file of requiredFiles) { + if (!fs.existsSync(file)) { + throw new Error(`필수 파일이 없습니다: ${file}`); + } + } + + console.log("✅ 기존 파일 검증 완료"); +} + +/** + * 상품 데이터 로드 + */ +async function loadProductData() { + const itemsPath = path.join(__dirname, "src/mocks/items.json"); + const itemsData = fs.readFileSync(itemsPath, "utf-8"); + return JSON.parse(itemsData); +} + +/** + * 카테고리별 페이지 생성 + */ +async function generateCategoryPages(items) { + console.log("📂 카테고리별 페이지 생성 중..."); + + const categories = {}; + + // 카테고리별 상품 그룹화 + items.forEach((item) => { + const cat1 = item.category1; + if (!categories[cat1]) { + categories[cat1] = []; + } + categories[cat1].push(item); + }); + + // 각 카테고리별 HTML 파일 생성 + const categoriesDir = path.join(distDir, "categories"); + if (!fs.existsSync(categoriesDir)) { + fs.mkdirSync(categoriesDir, { recursive: true }); + } + + for (const [categoryName, products] of Object.entries(categories)) { + const categorySlug = categoryName.replace(/\//g, "-").toLowerCase(); + const fileName = `${categorySlug}.html`; + const filePath = path.join(categoriesDir, fileName); + + // 간단한 카테고리 페이지 HTML 생성 + const html = generateCategoryHtml(categoryName, products); + fs.writeFileSync(filePath, html); + } + + console.log(`✅ ${Object.keys(categories).length}개의 카테고리 페이지 생성 완료`); +} + +/** + * 사이트맵 생성 + */ +async function generateSitemap(items) { + console.log("🗺️ 사이트맵 생성 중..."); + + const baseUrl = "https://your-domain.com"; // 실제 도메인으로 변경 필요 + const urls = [{ url: "/", priority: "1.0", changefreq: "daily" }]; + + // 상품 페이지들 추가 + items.forEach((item) => { + urls.push({ + url: `/product/${item.productId}`, + priority: "0.8", + changefreq: "weekly", + }); + }); + + // 카테고리 페이지들 추가 + const uniqueCategories = [...new Set(items.map((item) => item.category1))]; + uniqueCategories.forEach((category) => { + const categorySlug = category.replace(/\//g, "-").toLowerCase(); + urls.push({ + url: `/categories/${categorySlug}`, + priority: "0.7", + changefreq: "weekly", + }); + }); + + // 사이트맵 XML 생성 + const sitemapXml = ` + +${urls + .map( + (url) => ` + ${baseUrl}${url.url} + ${url.priority} + ${url.changefreq} + `, + ) + .join("\n")} +`; + + const sitemapPath = path.join(distDir, "sitemap.xml"); + fs.writeFileSync(sitemapPath, sitemapXml); + + console.log("✅ 사이트맵 생성 완료"); +} + +/** + * SEO 최적화 + */ +async function optimizeSEO() { + console.log("🔍 SEO 최적화 중..."); + + // robots.txt 생성 + const robotsTxt = `User-agent: * +Allow: / + +Sitemap: https://your-domain.com/sitemap.xml`; + + fs.writeFileSync(path.join(distDir, "robots.txt"), robotsTxt); + + console.log("✅ SEO 최적화 완료"); +} + +/** + * 카테고리 페이지 HTML 생성 + */ +function generateCategoryHtml(categoryName, products) { + const productList = products + .slice(0, 20) + .map( + (product) => ` +
+ ${product.title} +

${product.title}

+

${product.lprice}원

+ 자세히 보기 +
+ `, + ) + .join(""); + + return ` + + + + + ${categoryName} - 쇼핑몰 + + + + +
+
+

${categoryName}

+ ← 홈으로 돌아가기 +
- // 어플리케이션 렌더링하기 - const appHtml = render(); +
+ ${productList} +
- // 결과 HTML 생성하기 - const result = template.replace("", appHtml); - fs.writeFileSync("../../dist/vanilla/index.html", result); + ${products.length > 20 ? `

그 외 ${products.length - 20}개의 상품이 더 있습니다.

` : ""} +
+ +`; } // 실행 -generateStaticSite(); +generateStaticSite().catch(console.error); From 266695b8d006084df09d642cc03ffcb8b646ece2 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Fri, 5 Sep 2025 04:41:48 +0900 Subject: [PATCH 08/11] =?UTF-8?q?fix:=20ssg=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vanilla/src/constants.js | 3 +- packages/vanilla/static-site-generate.js | 268 +++++++---------------- 2 files changed, 77 insertions(+), 194 deletions(-) diff --git a/packages/vanilla/src/constants.js b/packages/vanilla/src/constants.js index 504b29b9..e036e376 100644 --- a/packages/vanilla/src/constants.js +++ b/packages/vanilla/src/constants.js @@ -1 +1,2 @@ -export const BASE_URL = import.meta.env.PROD ? "/front_6th_chapter4-1/vanilla/" : "/"; +export const BASE_URL = + typeof window === "undefined" ? "/" : import.meta.env?.PROD ? "/front_6th_chapter4-1/vanilla/" : "/"; diff --git a/packages/vanilla/static-site-generate.js b/packages/vanilla/static-site-generate.js index 3f5f1d17..0389e650 100644 --- a/packages/vanilla/static-site-generate.js +++ b/packages/vanilla/static-site-generate.js @@ -6,209 +6,91 @@ import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -// 경로 설정 -const projectRoot = path.resolve(__dirname, "../.."); -const distDir = path.join(projectRoot, "dist/vanilla"); - -/** - * 정적 사이트 생성 메인 함수 - */ -async function generateStaticSite() { - console.log("🚀 정적 사이트 생성을 시작합니다..."); - - try { - // 1. 기존 파일 검증 - await validateExistingFiles(); - - // 2. 상품 데이터 로드 - const items = await loadProductData(); - console.log(`📦 ${items.length}개의 상품을 발견했습니다.`); - - // 3. 추가 정적 페이지 생성 - await generateCategoryPages(items); - await generateSitemap(items); - - // 4. SEO 최적화 - await optimizeSEO(); - - console.log("✅ 정적 사이트 생성이 완료되었습니다!"); - } catch (error) { - console.error("❌ 정적 사이트 생성 중 오류 발생:", error); - process.exit(1); - } +// 경로 상수 +const DIST_DIR = path.resolve(__dirname, "../../dist/vanilla"); +const SSR_DIR = path.resolve(__dirname, "src"); + +// 상품 데이터 가져오기 +async function mockGetProducts({ limit = 20 }) { + // 서버 모듈에서 상품 데이터 가져오기 + const serverModule = await import(path.join(SSR_DIR, "mocks/server.js")); + const { getProductsOnServer } = serverModule; + + const result = getProductsOnServer({ limit }); + return result.products; } -/** - * 기존 생성된 파일들 검증 - */ -async function validateExistingFiles() { - console.log("🔍 기존 파일들을 검증합니다..."); - - const requiredFiles = [path.join(distDir, "index.html"), path.join(distDir, "product")]; +// 페이지 목록 생성 +async function getPages() { + const products = await mockGetProducts({ limit: 20 }); + + return [ + { url: "/", filePath: path.join(DIST_DIR, "index.html") }, + { url: "/404", filePath: path.join(DIST_DIR, "404.html") }, + ...products.map((p) => ({ + url: `/product/${p.productId}/`, + filePath: path.join(DIST_DIR, `product/${p.productId}/index.html`), + })), + ]; +} - for (const file of requiredFiles) { - if (!fs.existsSync(file)) { - throw new Error(`필수 파일이 없습니다: ${file}`); - } +// HTML 파일 저장 +async function saveHtmlFile(filePath, html) { + // 디렉토리 생성 + const dir = path.dirname(filePath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); } - console.log("✅ 기존 파일 검증 완료"); + // 파일 저장 + fs.writeFileSync(filePath, html, "utf-8"); } -/** - * 상품 데이터 로드 - */ -async function loadProductData() { - const itemsPath = path.join(__dirname, "src/mocks/items.json"); - const itemsData = fs.readFileSync(itemsPath, "utf-8"); - return JSON.parse(itemsData); -} - -/** - * 카테고리별 페이지 생성 - */ -async function generateCategoryPages(items) { - console.log("📂 카테고리별 페이지 생성 중..."); - - const categories = {}; - - // 카테고리별 상품 그룹화 - items.forEach((item) => { - const cat1 = item.category1; - if (!categories[cat1]) { - categories[cat1] = []; +async function generateStaticSite() { + try { + console.log("SSG 시작..."); + + // 1. 템플릿 + SSR 모듈 로드 + const templatePath = path.join(__dirname, "index.html"); + const template = fs.readFileSync(templatePath, "utf-8"); + + // SSR 모듈 동적 import + const ssrModule = await import(path.join(SSR_DIR, "main-server.js")); + const { render } = ssrModule; + + // 2. 페이지 목록 생성 + const pages = await getPages(); + console.log(`${pages.length}개 페이지 생성 예정`); + + // 3. 각 페이지 렌더링 + 저장 + for (let i = 0; i < pages.length; i++) { + const page = pages[i]; + console.log(`[${i + 1}/${pages.length}] ${page.url} 렌더링 중...`); + + try { + // SSR 렌더링 + const result = await render(page.url); + + // HTML 템플릿에 데이터 삽입 + let html = template + .replace("", result.html) + .replace("", result.head || "") + .replace("", ``); + + // 파일 저장 + await saveHtmlFile(page.filePath, html); + console.log(`✓ ${page.url} 완료`); + } catch (error) { + console.error(`✗ ${page.url} 렌더링 실패:`, error.message); + } } - categories[cat1].push(item); - }); - - // 각 카테고리별 HTML 파일 생성 - const categoriesDir = path.join(distDir, "categories"); - if (!fs.existsSync(categoriesDir)) { - fs.mkdirSync(categoriesDir, { recursive: true }); - } - - for (const [categoryName, products] of Object.entries(categories)) { - const categorySlug = categoryName.replace(/\//g, "-").toLowerCase(); - const fileName = `${categorySlug}.html`; - const filePath = path.join(categoriesDir, fileName); - // 간단한 카테고리 페이지 HTML 생성 - const html = generateCategoryHtml(categoryName, products); - fs.writeFileSync(filePath, html); + console.log("SSG 완료!"); + } catch (error) { + console.error("SSG 실패:", error); + process.exit(1); } - - console.log(`✅ ${Object.keys(categories).length}개의 카테고리 페이지 생성 완료`); -} - -/** - * 사이트맵 생성 - */ -async function generateSitemap(items) { - console.log("🗺️ 사이트맵 생성 중..."); - - const baseUrl = "https://your-domain.com"; // 실제 도메인으로 변경 필요 - const urls = [{ url: "/", priority: "1.0", changefreq: "daily" }]; - - // 상품 페이지들 추가 - items.forEach((item) => { - urls.push({ - url: `/product/${item.productId}`, - priority: "0.8", - changefreq: "weekly", - }); - }); - - // 카테고리 페이지들 추가 - const uniqueCategories = [...new Set(items.map((item) => item.category1))]; - uniqueCategories.forEach((category) => { - const categorySlug = category.replace(/\//g, "-").toLowerCase(); - urls.push({ - url: `/categories/${categorySlug}`, - priority: "0.7", - changefreq: "weekly", - }); - }); - - // 사이트맵 XML 생성 - const sitemapXml = ` - -${urls - .map( - (url) => ` - ${baseUrl}${url.url} - ${url.priority} - ${url.changefreq} - `, - ) - .join("\n")} -`; - - const sitemapPath = path.join(distDir, "sitemap.xml"); - fs.writeFileSync(sitemapPath, sitemapXml); - - console.log("✅ 사이트맵 생성 완료"); -} - -/** - * SEO 최적화 - */ -async function optimizeSEO() { - console.log("🔍 SEO 최적화 중..."); - - // robots.txt 생성 - const robotsTxt = `User-agent: * -Allow: / - -Sitemap: https://your-domain.com/sitemap.xml`; - - fs.writeFileSync(path.join(distDir, "robots.txt"), robotsTxt); - - console.log("✅ SEO 최적화 완료"); -} - -/** - * 카테고리 페이지 HTML 생성 - */ -function generateCategoryHtml(categoryName, products) { - const productList = products - .slice(0, 20) - .map( - (product) => ` -
- ${product.title} -

${product.title}

-

${product.lprice}원

- 자세히 보기 -
- `, - ) - .join(""); - - return ` - - - - - ${categoryName} - 쇼핑몰 - - - - -
-
-

${categoryName}

- ← 홈으로 돌아가기 -
- -
- ${productList} -
- - ${products.length > 20 ? `

그 외 ${products.length - 20}개의 상품이 더 있습니다.

` : ""} -
- -`; } // 실행 -generateStaticSite().catch(console.error); +generateStaticSite(); From eeb8c5276fca43718203e1bd3a4ef80ecabdbfba Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Fri, 5 Sep 2025 04:47:27 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20import=20=EA=B2=BD=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vanilla/src/components/CartModal.js | 2 +- packages/vanilla/src/components/ProductList.js | 2 +- packages/vanilla/src/components/index.js | 16 ++++++++-------- packages/vanilla/src/lib/createStore.js | 2 +- packages/vanilla/src/lib/index.js | 10 +++++----- packages/vanilla/src/main-server.js | 6 +++--- packages/vanilla/src/mocks/server.js | 2 +- packages/vanilla/src/pages/HomePage.js | 8 ++++---- packages/vanilla/src/pages/NotFoundPage.js | 4 ++-- packages/vanilla/src/pages/PageWrapper.js | 4 ++-- packages/vanilla/src/pages/ProductDetailPage.js | 6 +++--- packages/vanilla/src/pages/index.js | 6 +++--- packages/vanilla/src/router/index.js | 2 +- packages/vanilla/src/router/router.js | 2 +- packages/vanilla/src/services/cartService.js | 4 ++-- packages/vanilla/src/services/index.js | 4 ++-- packages/vanilla/src/services/productService.js | 6 +++--- packages/vanilla/src/storage/cartStorage.js | 2 +- packages/vanilla/src/storage/index.js | 2 +- packages/vanilla/src/stores/cartStore.js | 4 ++-- packages/vanilla/src/stores/index.js | 8 ++++---- packages/vanilla/src/stores/productStore.js | 4 ++-- packages/vanilla/src/stores/uiStore.js | 4 ++-- 23 files changed, 55 insertions(+), 55 deletions(-) diff --git a/packages/vanilla/src/components/CartModal.js b/packages/vanilla/src/components/CartModal.js index f9695180..bbb947bd 100644 --- a/packages/vanilla/src/components/CartModal.js +++ b/packages/vanilla/src/components/CartModal.js @@ -1,4 +1,4 @@ -import { CartItem } from "./CartItem"; +import { CartItem } from "./CartItem.js"; export function CartModal({ items = [], selectedAll = false, isOpen = false }) { if (!isOpen) { diff --git a/packages/vanilla/src/components/ProductList.js b/packages/vanilla/src/components/ProductList.js index e32e49b1..62b9558e 100644 --- a/packages/vanilla/src/components/ProductList.js +++ b/packages/vanilla/src/components/ProductList.js @@ -1,4 +1,4 @@ -import { ProductCard, ProductCardSkeleton } from "./ProductCard"; +import { ProductCard, ProductCardSkeleton } from "./ProductCard.js"; const loadingSkeleton = Array(6).fill(0).map(ProductCardSkeleton).join(""); diff --git a/packages/vanilla/src/components/index.js b/packages/vanilla/src/components/index.js index ef27b3d5..3e5ad533 100644 --- a/packages/vanilla/src/components/index.js +++ b/packages/vanilla/src/components/index.js @@ -1,8 +1,8 @@ -export * from "./ProductCard"; -export * from "./SearchBar"; -export * from "./ProductList"; -export * from "./CartItem"; -export * from "./CartModal"; -export * from "./Toast"; -export * from "./Logo"; -export * from "./Footer"; +export * from "./ProductCard.js"; +export * from "./SearchBar.js"; +export * from "./ProductList.js"; +export * from "./CartItem.js"; +export * from "./CartModal.js"; +export * from "./Toast.js"; +export * from "./Logo.js"; +export * from "./Footer.js"; diff --git a/packages/vanilla/src/lib/createStore.js b/packages/vanilla/src/lib/createStore.js index 19c74f82..9337ba3f 100644 --- a/packages/vanilla/src/lib/createStore.js +++ b/packages/vanilla/src/lib/createStore.js @@ -1,4 +1,4 @@ -import { createObserver } from "./createObserver"; +import { createObserver } from "./createObserver.js"; /** * Redux-style Store 생성 함수 diff --git a/packages/vanilla/src/lib/index.js b/packages/vanilla/src/lib/index.js index da63f1df..1ae59e67 100644 --- a/packages/vanilla/src/lib/index.js +++ b/packages/vanilla/src/lib/index.js @@ -1,5 +1,5 @@ -export * from "./createObserver"; -export * from "./createStore"; -export * from "./createStorage"; -export * from "./Router"; -export * from "./ServerRouter"; +export * from "./createObserver.js"; +export * from "./createStore.js"; +export * from "./createStorage.js"; +export * from "./Router.js"; +export * from "./ServerRouter.js"; diff --git a/packages/vanilla/src/main-server.js b/packages/vanilla/src/main-server.js index 9f373fb4..09c99e42 100644 --- a/packages/vanilla/src/main-server.js +++ b/packages/vanilla/src/main-server.js @@ -1,6 +1,6 @@ -import { HomePage, NotFoundPage, ProductDetailPageSSR } from "./pages"; -import { router } from "./router"; -import { getProductsOnServer, getUniqueCategories, getProductDetailOnServer } from "/src/mocks/server.js"; +import { HomePage, NotFoundPage, ProductDetailPageSSR } from "./pages/index.js"; +import { router } from "./router/index.js"; +import { getProductsOnServer, getUniqueCategories, getProductDetailOnServer } from "./mocks/server.js"; router.addRoute("/", () => { const { diff --git a/packages/vanilla/src/mocks/server.js b/packages/vanilla/src/mocks/server.js index 2086a345..e7c21166 100644 --- a/packages/vanilla/src/mocks/server.js +++ b/packages/vanilla/src/mocks/server.js @@ -1,5 +1,5 @@ // import { http, HttpResponse } from "msw"; -import items from "./items.json"; +import items from "./items.json" with { type: "json" }; // const delay = async () => await new Promise((resolve) => setTimeout(resolve, 200)); diff --git a/packages/vanilla/src/pages/HomePage.js b/packages/vanilla/src/pages/HomePage.js index e7995f18..5bcf5ee9 100644 --- a/packages/vanilla/src/pages/HomePage.js +++ b/packages/vanilla/src/pages/HomePage.js @@ -1,7 +1,7 @@ -import { ProductList, SearchBar } from "../components"; -import { productStore, PRODUCT_ACTIONS } from "../stores"; -import { router, withLifecycle } from "../router"; -import { loadProducts, loadProductsAndCategories } from "../services"; +import { ProductList, SearchBar } from "../components/index.js"; +import { productStore, PRODUCT_ACTIONS } from "../stores/index.js"; +import { router, withLifecycle } from "../router/index.js"; +import { loadProducts, loadProductsAndCategories } from "../services/index.js"; import { PageWrapper } from "./PageWrapper.js"; export const HomePage = withLifecycle( diff --git a/packages/vanilla/src/pages/NotFoundPage.js b/packages/vanilla/src/pages/NotFoundPage.js index be69ed5f..f4a82a0d 100644 --- a/packages/vanilla/src/pages/NotFoundPage.js +++ b/packages/vanilla/src/pages/NotFoundPage.js @@ -1,5 +1,5 @@ -import { PageWrapper } from "./PageWrapper"; -import { Logo } from "../components"; +import { PageWrapper } from "./PageWrapper.js"; +import { Logo } from "../components/index.js"; export const NotFoundPage = () => PageWrapper({ diff --git a/packages/vanilla/src/pages/PageWrapper.js b/packages/vanilla/src/pages/PageWrapper.js index fc13328e..f1527b63 100644 --- a/packages/vanilla/src/pages/PageWrapper.js +++ b/packages/vanilla/src/pages/PageWrapper.js @@ -1,5 +1,5 @@ -import { cartStore, uiStore } from "../stores"; -import { CartModal, Footer, Toast } from "../components"; +import { cartStore, uiStore } from "../stores/index.js"; +import { CartModal, Footer, Toast } from "../components/index.js"; export const PageWrapper = ({ headerLeft, children }) => { const cart = cartStore.getState(); diff --git a/packages/vanilla/src/pages/ProductDetailPage.js b/packages/vanilla/src/pages/ProductDetailPage.js index b7bdac51..effc6728 100644 --- a/packages/vanilla/src/pages/ProductDetailPage.js +++ b/packages/vanilla/src/pages/ProductDetailPage.js @@ -1,6 +1,6 @@ -import { productStore } from "../stores"; -import { loadProductDetailForPage } from "../services"; -import { router, withLifecycle } from "../router"; +import { productStore } from "../stores/index.js"; +import { loadProductDetailForPage } from "../services/index.js"; +import { router, withLifecycle } from "../router/index.js"; import { PageWrapper } from "./PageWrapper.js"; const loadingContent = ` diff --git a/packages/vanilla/src/pages/index.js b/packages/vanilla/src/pages/index.js index 1bf01f33..bb674eec 100644 --- a/packages/vanilla/src/pages/index.js +++ b/packages/vanilla/src/pages/index.js @@ -1,3 +1,3 @@ -export * from "./HomePage"; -export * from "./ProductDetailPage"; -export * from "./NotFoundPage"; +export * from "./HomePage.js"; +export * from "./ProductDetailPage.js"; +export * from "./NotFoundPage.js"; diff --git a/packages/vanilla/src/router/index.js b/packages/vanilla/src/router/index.js index f4964f8d..4d84d2cb 100644 --- a/packages/vanilla/src/router/index.js +++ b/packages/vanilla/src/router/index.js @@ -1,2 +1,2 @@ -export * from "./router"; +export * from "./router.js"; export * from "./withLifecycle.js"; diff --git a/packages/vanilla/src/router/router.js b/packages/vanilla/src/router/router.js index 75e7ba3c..044edd36 100644 --- a/packages/vanilla/src/router/router.js +++ b/packages/vanilla/src/router/router.js @@ -1,5 +1,5 @@ // 글로벌 라우터 인스턴스 -import { Router, ServerRouter } from "../lib"; +import { Router, ServerRouter } from "../lib/index.js"; import { BASE_URL } from "../constants.js"; export const router = typeof window !== "undefined" ? new Router(BASE_URL) : new ServerRouter(BASE_URL); diff --git a/packages/vanilla/src/services/cartService.js b/packages/vanilla/src/services/cartService.js index 85f7c5e9..d7adb1bc 100644 --- a/packages/vanilla/src/services/cartService.js +++ b/packages/vanilla/src/services/cartService.js @@ -1,5 +1,5 @@ -import { CART_ACTIONS, cartStore, UI_ACTIONS, uiStore } from "../stores"; -import { cartStorage } from "../storage"; +import { CART_ACTIONS, cartStore, UI_ACTIONS, uiStore } from "../stores/index.js"; +import { cartStorage } from "../storage/index.js"; /** * 로컬스토리지에서 장바구니 데이터 로드 diff --git a/packages/vanilla/src/services/index.js b/packages/vanilla/src/services/index.js index 845d25b4..782661e1 100644 --- a/packages/vanilla/src/services/index.js +++ b/packages/vanilla/src/services/index.js @@ -1,2 +1,2 @@ -export * from "./productService"; -export * from "./cartService"; +export * from "./productService.js"; +export * from "./cartService.js"; diff --git a/packages/vanilla/src/services/productService.js b/packages/vanilla/src/services/productService.js index 8a12e8bd..fbb1ea9c 100644 --- a/packages/vanilla/src/services/productService.js +++ b/packages/vanilla/src/services/productService.js @@ -1,6 +1,6 @@ -import { getCategories, getProduct, getProducts } from "../api/productApi"; -import { initialProductState, productStore, PRODUCT_ACTIONS } from "../stores"; -import { router } from "../router"; +import { getCategories, getProduct, getProducts } from "../api/productApi.js"; +import { initialProductState, productStore, PRODUCT_ACTIONS } from "../stores/index.js"; +import { router } from "../router/index.js"; export const loadProductsAndCategories = async () => { router.query = { current: undefined }; // 항상 첫 페이지로 초기화 diff --git a/packages/vanilla/src/storage/cartStorage.js b/packages/vanilla/src/storage/cartStorage.js index 85488c55..b3bb3754 100644 --- a/packages/vanilla/src/storage/cartStorage.js +++ b/packages/vanilla/src/storage/cartStorage.js @@ -1,4 +1,4 @@ -import { memoryStorage, createStorage } from "../lib"; +import { memoryStorage, createStorage } from "../lib/index.js"; export const cartStorage = createStorage( "shopping_cart", diff --git a/packages/vanilla/src/storage/index.js b/packages/vanilla/src/storage/index.js index 122983be..27d82b35 100644 --- a/packages/vanilla/src/storage/index.js +++ b/packages/vanilla/src/storage/index.js @@ -1 +1 @@ -export * from "./cartStorage"; +export * from "./cartStorage.js"; diff --git a/packages/vanilla/src/stores/cartStore.js b/packages/vanilla/src/stores/cartStore.js index fe61f167..cc95a3de 100644 --- a/packages/vanilla/src/stores/cartStore.js +++ b/packages/vanilla/src/stores/cartStore.js @@ -1,5 +1,5 @@ -import { createStore } from "../lib"; -import { CART_ACTIONS } from "./actionTypes"; +import { createStore } from "../lib/index.js"; +import { CART_ACTIONS } from "./actionTypes.js"; import { cartStorage } from "../storage/index.js"; /** diff --git a/packages/vanilla/src/stores/index.js b/packages/vanilla/src/stores/index.js index 36fefd54..2e2c7dda 100644 --- a/packages/vanilla/src/stores/index.js +++ b/packages/vanilla/src/stores/index.js @@ -1,4 +1,4 @@ -export * from "./actionTypes"; -export * from "./productStore"; -export * from "./cartStore"; -export * from "./uiStore"; +export * from "./actionTypes.js"; +export * from "./productStore.js"; +export * from "./cartStore.js"; +export * from "./uiStore.js"; diff --git a/packages/vanilla/src/stores/productStore.js b/packages/vanilla/src/stores/productStore.js index 0f39343d..00c9f5c7 100644 --- a/packages/vanilla/src/stores/productStore.js +++ b/packages/vanilla/src/stores/productStore.js @@ -1,5 +1,5 @@ -import { createStore } from "../lib"; -import { PRODUCT_ACTIONS } from "./actionTypes"; +import { createStore } from "../lib/index.js"; +import { PRODUCT_ACTIONS } from "./actionTypes.js"; /** * 상품 스토어 초기 상태 diff --git a/packages/vanilla/src/stores/uiStore.js b/packages/vanilla/src/stores/uiStore.js index 606603d7..0a05f796 100644 --- a/packages/vanilla/src/stores/uiStore.js +++ b/packages/vanilla/src/stores/uiStore.js @@ -1,5 +1,5 @@ -import { createStore } from "../lib"; -import { UI_ACTIONS } from "./actionTypes"; +import { createStore } from "../lib/index.js"; +import { UI_ACTIONS } from "./actionTypes.js"; /** * UI 스토어 초기 상태 From fed016caafb67d702461a02ae1dd3bc0cfef8423 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Fri, 5 Sep 2025 06:39:44 +0900 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20base=20path=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vanilla/vite.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vanilla/vite.config.js b/packages/vanilla/vite.config.js index 020b5c4a..48639183 100644 --- a/packages/vanilla/vite.config.js +++ b/packages/vanilla/vite.config.js @@ -1,5 +1,5 @@ import { defineConfig } from "vite"; -const base = process.env.NODE_ENV === "production" ? "/front_6th_chapter4-1/vanilla/" : ""; +const base = process.env.NODE_ENV === "production" ? "/vanilla/" : ""; export default defineConfig({ base }); From 828913c0c735fb07d3b19fcbc34ab9b4f6618765 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Fri, 5 Sep 2025 07:12:21 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20vite.config=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/vanilla/vite.config.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/vanilla/vite.config.js b/packages/vanilla/vite.config.js index 48639183..121267dd 100644 --- a/packages/vanilla/vite.config.js +++ b/packages/vanilla/vite.config.js @@ -1,5 +1,14 @@ import { defineConfig } from "vite"; -const base = process.env.NODE_ENV === "production" ? "/vanilla/" : ""; +const base = process.env.NODE_ENV === "production" ? "/front_6th_chapter4-1/vanilla/" : ""; -export default defineConfig({ base }); +export default defineConfig({ + base, + build: { + rollupOptions: { + output: { + manualChunks: undefined, + }, + }, + }, +});