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/server.js b/packages/vanilla/server.js
index b9a56d98..b91851b7 100644
--- a/packages/vanilla/server.js
+++ b/packages/vanilla/server.js
@@ -1,31 +1,71 @@
+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;
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(),
- );
+mswServer.listen({ onUnhandledRequest: "bypass" });
+
+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 {
+ let url = req.originalUrl;
+ if (base !== "/" && url.startsWith(base)) {
+ url = url.replace(base, "");
+ }
+ if (!url.startsWith("/")) {
+ url = "/" + url;
+ }
+
+ /** @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, req.query);
+
+ const html = template
+ .replace(``, rendered.head ?? "")
+ .replace(``, ``)
+ .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/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/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/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/src/lib/ServerRouter.js b/packages/vanilla/src/lib/ServerRouter.js
new file mode 100644
index 00000000..73b95abb
--- /dev/null
+++ b/packages/vanilla/src/lib/ServerRouter.js
@@ -0,0 +1,169 @@
+/**
+ * Server 라우터
+ */
+import { createObserver } from "./createObserver.js";
+
+export class ServerRouter {
+ #routes;
+ #route;
+ #observer = createObserver();
+ #baseUrl;
+ #serverQuery = {};
+
+ constructor(baseUrl = "") {
+ this.#routes = new Map();
+ this.#route = null;
+ this.#baseUrl = baseUrl.replace(/\/$/, "");
+ }
+
+ get baseUrl() {
+ return this.#baseUrl;
+ }
+
+ get query() {
+ // 서버 사이드에서는 #serverQuery를 사용, 클라이언트 사이드에서는 URL 파라미터 사용
+ if (typeof window === "undefined") {
+ return this.#serverQuery;
+ }
+ return ServerRouter.parseQuery(window.location.search);
+ }
+
+ set query(newQuery) {
+ // 서버 사이드에서는 #serverQuery를 설정, 클라이언트 사이드에서는 URL 업데이트
+ if (typeof window === "undefined") {
+ this.#serverQuery = { ...newQuery };
+ } else {
+ 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 = "/", 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 {
+ // baseUrl이 없으면 자동으로 붙여줌
+ let fullUrl = url.startsWith(this.#baseUrl) ? url : this.#baseUrl + (url.startsWith("/") ? url : "/" + url);
+
+ 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) => {
+ 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}` : ""}`;
+ };
+}
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/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 a598ef30..1ae59e67 100644
--- a/packages/vanilla/src/lib/index.js
+++ b/packages/vanilla/src/lib/index.js
@@ -1,4 +1,5 @@
-export * from "./createObserver";
-export * from "./createStore";
-export * from "./createStorage";
-export * from "./Router";
+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 40b58858..09c99e42 100644
--- a/packages/vanilla/src/main-server.js
+++ b/packages/vanilla/src/main-server.js
@@ -1,4 +1,90 @@
+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 {
+ 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;
+
+ return {
+ 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 });
- return "";
+ 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/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);
diff --git a/packages/vanilla/src/mocks/server.js b/packages/vanilla/src/mocks/server.js
new file mode 100644
index 00000000..e7c21166
--- /dev/null
+++ b/packages/vanilla/src/mocks/server.js
@@ -0,0 +1,135 @@
+// import { http, HttpResponse } from "msw";
+import items from "./items.json" with { type: "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/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);
diff --git a/packages/vanilla/src/pages/HomePage.js b/packages/vanilla/src/pages/HomePage.js
index ca08c26c..5bcf5ee9 100644
--- a/packages/vanilla/src/pages/HomePage.js
+++ b/packages/vanilla/src/pages/HomePage.js
@@ -1,12 +1,32 @@
-import { ProductList, SearchBar } from "../components";
-import { productStore } 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(
{
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/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 73d0ec30..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 = `
@@ -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: `
-