diff --git a/.cursor/rules/ssr-ssg-rules.mdc b/.cursor/rules/ssr-ssg-rules.mdc
new file mode 100644
index 00000000..d37372cf
--- /dev/null
+++ b/.cursor/rules/ssr-ssg-rules.mdc
@@ -0,0 +1,45 @@
+---
+description: SSR, SSG 행동 강령
+globs:
+alwaysApply: true
+---
+version: 1
+rules:
+ # Express Server (server.js)
+ - require: Implement an Express middleware-based server
+ hint: "Use middleware pattern, template rendering, hydration entry support"
+ - require: Handle environment splitting (development vs production)
+ hint: "Add dev/prod mode checks and configuration"
+ - require: Inject HTML templates (``, ``)
+ hint: "Replace placeholders in base HTML template"
+
+ # Server Rendering (main-server.js)
+ - require: Implement Router that works on the server
+ hint: "Server-side routing logic should match client routes"
+ - require: Prefetch server data (product list, product detail) using route params
+ hint: "Handle async data fetching before rendering"
+ - require: Initialize and manage server-side store
+ hint: "Populate store/state with prefetched data for rendering"
+
+ # Static Site Generation (static-site-generate.js)
+ - require: Generate static pages at build-time
+ hint: "Run generation script during build process"
+ - require: Support dynamic routes (e.g., product detail pages)
+ hint: "Iterate over data to pre-generate dynamic routes"
+ - require: Write generated pages into the file system for deployment
+ hint: "Save HTML files under output directory"
+
+ # Client Hydration (main.js)
+ - require: Inject `window.__INITIAL_DATA__` script
+ hint: "Serialize server state into HTML for client hydration"
+ - require: Restore client state using initial server data
+ hint: "Sync client store with server-injected initial data"
+ - require: Sync server and client state for consistency
+ hint: "Ensure no mismatch between rendered HTML and hydrated app"
+ - require: Load client-side app entry
+ hint: "Bootstrap SPA after SSR render"
+
+ # Testing (global)
+ - require: Ensure all implementations pass e2e test with `pnpm run test:e2e:basic`
+ hint: "Run automated tests to validate SSR, hydration, and SSG behavior"
+
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 00000000..c1626484
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,532 @@
+# Vanilla JavaScript SSR & SSG 구현 문서
+
+## 📋 목차
+
+- [프로젝트 개요](#프로젝트-개요)
+- [SSR 구현](#ssr-구현)
+- [SSG 구현](#ssg-구현)
+- [아키텍처](#아키텍처)
+- [핵심 라이브러리](#핵심-라이브러리)
+- [컴포넌트 시스템](#컴포넌트-시스템)
+- [상태 관리](#상태-관리)
+- [라우팅 시스템](#라우팅-시스템)
+- [이벤트 시스템](#이벤트-시스템)
+- [빌드 및 배포](#빌드-및-배포)
+
+## 🎯 프로젝트 개요
+
+이 프로젝트는 **순수 Vanilla JavaScript**로 구현된 쇼핑몰 애플리케이션으로, **SSR(Server-Side Rendering)**과 **SSG(Static Site Generation)**을 모두 지원합니다.
+
+### ✨ 주요 특징
+
+- ✅ **프레임워크 없는** 순수 JavaScript 구현
+- ✅ **SSR & SSG** 동시 지원
+- ✅ **SPA 라우팅** 시스템
+- ✅ **Redux 패턴** 상태 관리
+- ✅ **컴포넌트 기반** 아키텍처
+- ✅ **이벤트 위임** 시스템
+- ✅ **무한 스크롤** 구현
+- ✅ **로컬 스토리지** 동기화
+
+## 🖥️ SSR 구현
+
+### 1. 서버 구성 (`server.js`)
+
+```javascript
+import express from "express";
+
+const app = express();
+const port = process.env.PORT || 5173;
+
+const render = () => {
+ return `
안녕하세요
`;
+};
+
+app.get("*all", (req, res) => {
+ res.send(`
+
+
+
+
+
+ Vanilla Javascript SSR
+
+
+${render()}
+
+
+ `.trim());
+});
+
+app.listen(port, () => {
+ console.log(`Server started at http://localhost:${port}`);
+});
+```
+
+### 2. 서버 렌더링 엔트리 (`main-server.js`)
+
+```javascript
+export const render = async (url, query) => {
+ console.log({ url, query });
+ return "";
+};
+```
+
+### 3. SSR 특징
+
+- **Express 서버**를 사용한 서버 사이드 렌더링
+- **모든 경로**(`*all`)에 대한 요청 처리
+- **HTML 템플릿**에 렌더링된 컨텐츠 삽입
+- **서버에서 완성된 HTML** 전송
+
+## 📄 SSG 구현
+
+### 1. 정적 사이트 생성기 (`static-site-generate.js`)
+
+```javascript
+import fs from "fs";
+
+const render = () => {
+ return `안녕하세요
`;
+};
+
+async function generateStaticSite() {
+ // HTML 템플릿 읽기
+ const template = fs.readFileSync("../../dist/vanilla/index.html", "utf-8");
+
+ // 어플리케이션 렌더링하기
+ const appHtml = render();
+
+ // 결과 HTML 생성하기
+ const result = template.replace("", appHtml);
+ fs.writeFileSync("../../dist/vanilla/index.html", result);
+}
+
+// 실행
+generateStaticSite();
+```
+
+### 2. SSG 특징
+
+- **빌드 시점**에 정적 HTML 파일 생성
+- **템플릿 플레이스홀더** (``) 교체
+- **파일 시스템**에 직접 HTML 파일 저장
+- **CDN 배포** 최적화
+
+## 🏗️ 아키텍처
+
+### 전체 구조
+
+```
+src/
+├── main.js # 클라이언트 엔트리 포인트
+├── main-server.js # 서버 엔트리 포인트
+├── render.js # 렌더링 시스템
+├── events.js # 이벤트 등록
+├── constants.js # 상수 정의
+│
+├── api/ # API 통신
+│ └── productApi.js
+│
+├── components/ # UI 컴포넌트
+│ ├── ProductCard.js
+│ ├── ProductList.js
+│ ├── SearchBar.js
+│ ├── CartModal.js
+│ ├── Toast.js
+│ └── ...
+│
+├── pages/ # 페이지 컴포넌트
+│ ├── HomePage.js
+│ ├── ProductDetailPage.js
+│ ├── NotFoundPage.js
+│ └── PageWrapper.js
+│
+├── stores/ # 상태 관리
+│ ├── productStore.js
+│ ├── cartStore.js
+│ ├── uiStore.js
+│ └── actionTypes.js
+│
+├── services/ # 비즈니스 로직
+│ ├── productService.js
+│ └── cartService.js
+│
+├── router/ # 라우팅 시스템
+│ ├── router.js
+│ └── withLifecycle.js
+│
+├── lib/ # 핵심 라이브러리
+│ ├── Router.js
+│ ├── createStore.js
+│ ├── createObserver.js
+│ └── createStorage.js
+│
+├── utils/ # 유틸리티
+│ ├── eventUtils.js
+│ ├── domUtils.js
+│ └── withBatch.js
+│
+├── storage/ # 로컬 스토리지
+│ └── cartStorage.js
+│
+└── mocks/ # 목업 데이터
+ ├── browser.js
+ ├── handlers.js
+ └── items.json
+```
+
+## 🔧 핵심 라이브러리
+
+### 1. 옵저버 패턴 (`createObserver.js`)
+
+```javascript
+export const createObserver = () => {
+ const listeners = new Set();
+ const subscribe = (fn) => listeners.add(fn);
+ const notify = () => listeners.forEach((listener) => listener());
+
+ return { subscribe, notify };
+};
+```
+
+### 2. 스토어 시스템 (`createStore.js`)
+
+```javascript
+export const createStore = (reducer, initialState) => {
+ const { subscribe, notify } = createObserver();
+ let state = initialState;
+
+ const getState = () => state;
+
+ const dispatch = (action) => {
+ const newState = reducer(state, action);
+ if (newState !== state) {
+ state = newState;
+ notify();
+ }
+ };
+
+ return { getState, dispatch, subscribe };
+};
+```
+
+### 3. 라우터 시스템 (`Router.js`)
+
+```javascript
+export class Router {
+ #routes = new Map();
+ #route = null;
+ #observer = createObserver();
+
+ addRoute(path, handler) {
+ // 경로 패턴을 정규식으로 변환
+ const paramNames = [];
+ const regexPath = path
+ .replace(/:\w+/g, (match) => {
+ paramNames.push(match.slice(1));
+ return "([^/]+)";
+ })
+ .replace(/\//g, "\\/");
+
+ const regex = new RegExp(`^${this.#baseUrl}${regexPath}$`);
+ this.#routes.set(path, { regex, paramNames, handler });
+ }
+
+ push(url) {
+ window.history.pushState(null, "", url);
+ this.#route = this.#findRoute(url);
+ this.#observer.notify();
+ }
+}
+```
+
+### 4. 스토리지 시스템 (`createStorage.js`)
+
+```javascript
+export const createStorage = (key, storage = window.localStorage) => {
+ const get = () => {
+ try {
+ const item = storage.getItem(key);
+ return item ? JSON.parse(item) : null;
+ } catch (error) {
+ console.error(`Error parsing storage item for key "${key}":`, error);
+ return null;
+ }
+ };
+
+ const set = (value) => {
+ try {
+ storage.setItem(key, JSON.stringify(value));
+ } catch (error) {
+ console.error(`Error setting storage item for key "${key}":`, error);
+ }
+ };
+
+ return { get, set, reset: () => storage.removeItem(key) };
+};
+```
+
+## 🧩 컴포넌트 시스템
+
+### 1. 함수형 컴포넌트
+
+```javascript
+// ProductCard 컴포넌트 예시
+export function ProductCard(product) {
+ const { productId, title, image, lprice, brand } = product;
+ const price = Number(lprice);
+
+ return `
+
+

+
${title}
+
${brand}
+
${price.toLocaleString()}원
+
+
+ `;
+}
+```
+
+### 2. 페이지 래퍼 패턴
+
+```javascript
+export const PageWrapper = ({ headerLeft, children }) => {
+ const cart = cartStore.getState();
+ const { cartModal, toast } = uiStore.getState();
+
+ return `
+
+
+ ${children}
+ ${CartModal({ ...cart, isOpen: cartModal.isOpen })}
+ ${Toast(toast)}
+
+ `;
+};
+```
+
+### 3. 라이프사이클 관리
+
+```javascript
+export const HomePage = withLifecycle(
+ {
+ onMount: () => {
+ loadProductsAndCategories();
+ },
+ watches: [
+ () => {
+ const { search, limit, sort, category1, category2 } = router.query;
+ return [search, limit, sort, category1, category2];
+ },
+ () => loadProducts(true),
+ ],
+ },
+ () => {
+ // 컴포넌트 렌더링 로직
+ return PageWrapper({ headerLeft, children });
+ }
+);
+```
+
+## 📊 상태 관리
+
+### 1. Redux 패턴 구현
+
+```javascript
+// 액션 타입 정의
+export const PRODUCT_ACTIONS = {
+ SET_PRODUCTS: "products/setProducts",
+ ADD_PRODUCTS: "products/addProducts",
+ SET_LOADING: "products/setLoading",
+ SET_ERROR: "products/setError",
+};
+
+// 리듀서 구현
+const productReducer = (state, action) => {
+ switch (action.type) {
+ case PRODUCT_ACTIONS.SET_PRODUCTS:
+ return {
+ ...state,
+ products: action.payload.products,
+ totalCount: action.payload.totalCount,
+ loading: false,
+ };
+ default:
+ return state;
+ }
+};
+
+// 스토어 생성
+export const productStore = createStore(productReducer, initialState);
+```
+
+### 2. 스토어 구조
+
+- **productStore**: 상품 목록, 상세, 카테고리 관리
+- **cartStore**: 장바구니 아이템, 선택 상태 관리
+- **uiStore**: 모달, 토스트, 로딩 상태 관리
+
+### 3. 로컬 스토리지 동기화
+
+```javascript
+export const saveCartToStorage = () => {
+ try {
+ const state = cartStore.getState();
+ cartStorage.set(state);
+ } catch (error) {
+ console.error("장바구니 저장 실패:", error);
+ }
+};
+```
+
+## 🛣️ 라우팅 시스템
+
+### 1. SPA 라우터 구현
+
+```javascript
+// 라우트 등록
+router.addRoute("/", HomePage);
+router.addRoute("/product/:id/", ProductDetailPage);
+router.addRoute(".*", NotFoundPage);
+
+// 네비게이션
+router.push("/product/123/");
+
+// 쿼리 파라미터 관리
+router.query = { search: "키보드", limit: 20 };
+```
+
+### 2. 동적 라우팅
+
+- **파라미터 추출**: `/product/:id/` → `{ id: "123" }`
+- **쿼리 스트링**: `?search=키보드&limit=20`
+- **히스토리 API**: `pushState`를 사용한 SPA 네비게이션
+
+## ⚡ 이벤트 시스템
+
+### 1. 이벤트 위임 패턴
+
+```javascript
+// 전역 이벤트 핸들러 저장소
+const eventHandlers = {};
+
+// 이벤트 위임을 통한 핸들러 추가
+export const addEvent = (eventType, selector, handler) => {
+ if (!eventHandlers[eventType]) {
+ eventHandlers[eventType] = {};
+ }
+ eventHandlers[eventType][selector] = handler;
+};
+
+// 사용 예시
+addEvent("click", ".add-to-cart-btn", (e) => {
+ const productId = e.target.getAttribute("data-product-id");
+ addToCart(productId);
+});
+```
+
+### 2. 배치 렌더링
+
+```javascript
+export const withBatch = (fn) => {
+ let scheduled = false;
+
+ return (...args) => {
+ if (scheduled) return;
+ scheduled = true;
+
+ queueMicrotask(() => {
+ scheduled = false;
+ fn(...args);
+ });
+ };
+};
+```
+
+## 🚀 빌드 및 배포
+
+### 1. 빌드 스크립트
+
+```json
+{
+ "scripts": {
+ "dev": "vite --port 5173",
+ "dev:ssr": "PORT=5174 node server.js",
+ "build:client": "vite build --outDir ./dist/vanilla",
+ "build:server": "vite build --outDir ./dist/vanilla-ssr --ssr src/main-server.js",
+ "build:ssg": "pnpm run build:client-for-ssg && node static-site-generate.js",
+ "build": "pnpm run build:client && pnpm run build:server && pnpm run build:ssg"
+ }
+}
+```
+
+### 2. 배포 방식
+
+- **CSR**: 클라이언트 사이드 렌더링 (`preview:csr`)
+- **SSR**: 서버 사이드 렌더링 (`preview:ssr`)
+- **SSG**: 정적 사이트 생성 (`preview:ssg`)
+
+### 3. 환경 분리
+
+```javascript
+const prod = process.env.NODE_ENV === "production";
+const base = process.env.BASE || (prod ? "/front_6th_chapter4-1/vanilla/" : "/");
+```
+
+## 🎯 성능 최적화
+
+### 1. 무한 스크롤
+
+```javascript
+// 스크롤 위치 감지
+export const isNearBottom = (threshold = 200) => {
+ const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
+ const windowHeight = window.innerHeight;
+ const documentHeight = document.documentElement.scrollHeight;
+
+ return scrollTop + windowHeight >= documentHeight - threshold;
+};
+
+// 무한 스크롤 이벤트
+addEvent("scroll", window, () => {
+ if (isNearBottom() && hasMore && !loading) {
+ loadMoreProducts();
+ }
+});
+```
+
+### 2. 이미지 지연 로딩
+
+```html
+
+```
+
+### 3. 코드 분할
+
+- **동적 임포트**: MSW 모킹 시스템
+- **조건부 로딩**: 테스트 환경 분리
+
+## 📚 추가 문서
+
+- [아키텍처 상세 가이드](./architecture.md)
+- [컴포넌트 시스템 가이드](./components.md)
+- [상태 관리 가이드](./state-management.md)
+- [라우팅 시스템 가이드](./routing.md)
+- [빌드 및 배포 가이드](./build-deploy.md)
+
+## 🎉 결론
+
+이 Vanilla JavaScript 프로젝트는 **프레임워크 없이도** 현대적인 웹 애플리케이션의 모든 기능을 구현할 수 있음을 보여줍니다:
+
+- ✅ **SSR/SSG** 동시 지원
+- ✅ **컴포넌트 기반** 아키텍처
+- ✅ **상태 관리** 시스템
+- ✅ **SPA 라우팅**
+- ✅ **이벤트 위임**
+- ✅ **성능 최적화**
+
+순수 JavaScript의 **가벼움**과 **유연성**을 활용하면서도, React나 Vue.js와 유사한 **개발 경험**을 제공하는 것이 이 프로젝트의 핵심 가치입니다.
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 00000000..db2a2418
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,550 @@
+# 아키텍처 상세 가이드
+
+## 📋 목차
+
+- [전체 아키텍처 개요](#전체-아키텍처-개요)
+- [레이어별 구조](#레이어별-구조)
+- [데이터 플로우](#데이터-플로우)
+- [모듈 의존성](#모듈-의존성)
+- [디자인 패턴](#디자인-패턴)
+- [성능 고려사항](#성능-고려사항)
+
+## 🏗️ 전체 아키텍처 개요
+
+### 아키텍처 다이어그램
+
+```
+┌─────────────────────────────────────────────────────────────┐
+│ Presentation Layer │
+├─────────────────────────────────────────────────────────────┤
+│ Pages (HomePage, ProductDetailPage, NotFoundPage) │
+│ Components (ProductCard, SearchBar, CartModal, Toast) │
+│ PageWrapper (공통 레이아웃) │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ Business Logic Layer │
+├─────────────────────────────────────────────────────────────┤
+│ Services (productService, cartService) │
+│ API Layer (productApi) │
+│ Router (라우팅 로직) │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ State Management │
+├─────────────────────────────────────────────────────────────┤
+│ Stores (productStore, cartStore, uiStore) │
+│ Redux Pattern (Actions, Reducers) │
+│ Local Storage (cartStorage) │
+└─────────────────────────────────────────────────────────────┘
+ │
+ ▼
+┌─────────────────────────────────────────────────────────────┐
+│ Core Libraries │
+├─────────────────────────────────────────────────────────────┤
+│ createStore, createObserver, Router, createStorage │
+│ Event System (이벤트 위임) │
+│ Utils (DOM 조작, 배치 처리) │
+└─────────────────────────────────────────────────────────────┘
+```
+
+### 핵심 설계 원칙
+
+1. **관심사 분리**: 각 레이어는 명확한 책임을 가짐
+2. **단방향 데이터 플로우**: 데이터는 위에서 아래로 흐름
+3. **컴포지션**: 작은 단위의 함수들을 조합하여 복잡한 기능 구현
+4. **불변성**: 상태 변경 시 새로운 객체 생성
+5. **이벤트 기반**: 느슨한 결합을 위한 이벤트 시스템
+
+## 📁 레이어별 구조
+
+### 1. Presentation Layer (표현 계층)
+
+#### Pages
+```javascript
+// 페이지 컴포넌트 구조
+export const HomePage = withLifecycle(
+ {
+ onMount: () => loadProductsAndCategories(),
+ watches: [() => [router.query], () => loadProducts(true)]
+ },
+ () => {
+ const state = productStore.getState();
+ return PageWrapper({
+ headerLeft: `쇼핑몰
`,
+ children: `${SearchBar()} ${ProductList()}`
+ });
+ }
+);
+```
+
+#### Components
+```javascript
+// 재사용 가능한 UI 컴포넌트
+export function ProductCard(product) {
+ return `
+
+

+
${product.title}
+
${product.brand}
+
${Number(product.lprice).toLocaleString()}원
+
+
+ `;
+}
+```
+
+#### PageWrapper
+```javascript
+// 공통 레이아웃 컴포넌트
+export const PageWrapper = ({ headerLeft, children }) => {
+ const cart = cartStore.getState();
+ const { cartModal, toast } = uiStore.getState();
+
+ return `
+
+
+ ${children}
+ ${CartModal({ ...cart, isOpen: cartModal.isOpen })}
+ ${Toast(toast)}
+
+ `;
+};
+```
+
+### 2. Business Logic Layer (비즈니스 로직 계층)
+
+#### Services
+```javascript
+// 상품 관련 비즈니스 로직
+export const loadProducts = async (resetList = true) => {
+ try {
+ productStore.dispatch({
+ type: PRODUCT_ACTIONS.SETUP,
+ payload: { loading: true, status: "pending", error: null }
+ });
+
+ const { products, pagination: { total } } = await getProducts(router.query);
+
+ if (resetList) {
+ productStore.dispatch({
+ type: PRODUCT_ACTIONS.SET_PRODUCTS,
+ payload: { products, totalCount: total }
+ });
+ } else {
+ productStore.dispatch({
+ type: PRODUCT_ACTIONS.ADD_PRODUCTS,
+ payload: { products, totalCount: total }
+ });
+ }
+ } catch (error) {
+ productStore.dispatch({
+ type: PRODUCT_ACTIONS.SET_ERROR,
+ payload: error.message
+ });
+ }
+};
+```
+
+#### API Layer
+```javascript
+// API 통신 추상화
+export async function getProducts(params = {}) {
+ const { limit = 20, search = "", category1 = "", category2 = "", sort = "price_asc" } = params;
+ const page = params.current ?? params.page ?? 1;
+
+ const searchParams = new URLSearchParams({
+ page: page.toString(),
+ limit: limit.toString(),
+ ...(search && { search }),
+ ...(category1 && { category1 }),
+ ...(category2 && { category2 }),
+ sort,
+ });
+
+ const response = await fetch(`/api/products?${searchParams}`);
+ return await response.json();
+}
+```
+
+### 3. State Management (상태 관리 계층)
+
+#### Store 구조
+```javascript
+// Redux 패턴 구현
+export const createStore = (reducer, initialState) => {
+ const { subscribe, notify } = createObserver();
+ let state = initialState;
+
+ const getState = () => state;
+ const dispatch = (action) => {
+ const newState = reducer(state, action);
+ if (newState !== state) {
+ state = newState;
+ notify();
+ }
+ };
+
+ return { getState, dispatch, subscribe };
+};
+```
+
+#### Reducer 패턴
+```javascript
+// 상품 스토어 리듀서
+const productReducer = (state, action) => {
+ switch (action.type) {
+ case PRODUCT_ACTIONS.SET_PRODUCTS:
+ return {
+ ...state,
+ products: action.payload.products,
+ totalCount: action.payload.totalCount,
+ loading: false,
+ error: null,
+ status: "done"
+ };
+ case PRODUCT_ACTIONS.SET_LOADING:
+ return { ...state, loading: action.payload };
+ case PRODUCT_ACTIONS.SET_ERROR:
+ return {
+ ...state,
+ error: action.payload,
+ loading: false,
+ status: "done"
+ };
+ default:
+ return state;
+ }
+};
+```
+
+### 4. Core Libraries (핵심 라이브러리 계층)
+
+#### Observer Pattern
+```javascript
+// 옵저버 패턴 구현
+export const createObserver = () => {
+ const listeners = new Set();
+ const subscribe = (fn) => listeners.add(fn);
+ const notify = () => listeners.forEach((listener) => listener());
+
+ return { subscribe, notify };
+};
+```
+
+#### Router System
+```javascript
+// SPA 라우터 구현
+export class Router {
+ #routes = new Map();
+ #route = null;
+ #observer = createObserver();
+
+ addRoute(path, handler) {
+ const paramNames = [];
+ const regexPath = path
+ .replace(/:\w+/g, (match) => {
+ paramNames.push(match.slice(1));
+ return "([^/]+)";
+ })
+ .replace(/\//g, "\\/");
+
+ const regex = new RegExp(`^${this.#baseUrl}${regexPath}$`);
+ this.#routes.set(path, { regex, paramNames, handler });
+ }
+
+ push(url) {
+ window.history.pushState(null, "", url);
+ this.#route = this.#findRoute(url);
+ this.#observer.notify();
+ }
+}
+```
+
+## 🔄 데이터 플로우
+
+### 1. 사용자 액션 → 상태 변경 플로우
+
+```
+사용자 클릭 → 이벤트 핸들러 → Service 함수 → Store Dispatch → Reducer → 상태 업데이트 → UI 리렌더링
+```
+
+### 2. 상세 플로우 예시
+
+```javascript
+// 1. 사용자가 장바구니 버튼 클릭
+addEvent("click", ".add-to-cart-btn", (e) => {
+ const productId = e.target.getAttribute("data-product-id");
+ addToCart(productId); // 2. Service 함수 호출
+});
+
+// 3. Service에서 Store에 액션 디스패치
+export const addToCart = (product, quantity = 1) => {
+ cartStore.dispatch({
+ type: CART_ACTIONS.ADD_ITEM,
+ payload: { product, quantity }
+ });
+
+ saveCartToStorage(); // 4. 로컬 스토리지 동기화
+};
+
+// 5. Reducer에서 상태 업데이트
+const cartReducer = (state, action) => {
+ switch (action.type) {
+ case CART_ACTIONS.ADD_ITEM:
+ return {
+ ...state,
+ items: [...state.items, newItem]
+ };
+ }
+};
+
+// 6. Store 변경 감지하여 UI 리렌더링
+cartStore.subscribe(render);
+```
+
+### 3. 라우팅 플로우
+
+```
+URL 변경 → Router.push() → 히스토리 업데이트 → 라우트 매칭 → 페이지 컴포넌트 렌더링 → 라이프사이클 실행
+```
+
+## 🔗 모듈 의존성
+
+### 의존성 그래프
+
+```
+main.js
+├── render.js
+│ ├── stores/
+│ ├── router/
+│ └── pages/
+├── events.js
+│ ├── services/
+│ └── utils/
+└── utils/
+ ├── eventUtils.js
+ ├── domUtils.js
+ └── withBatch.js
+
+pages/
+├── PageWrapper.js
+│ ├── stores/
+│ └── components/
+└── HomePage.js
+ ├── components/
+ ├── stores/
+ ├── services/
+ └── router/
+
+services/
+├── productService.js
+│ ├── api/
+│ ├── stores/
+│ └── router/
+└── cartService.js
+ ├── stores/
+ └── storage/
+
+stores/
+├── productStore.js
+│ └── lib/
+├── cartStore.js
+│ ├── lib/
+│ └── storage/
+└── uiStore.js
+ └── lib/
+
+lib/
+├── createStore.js
+│ └── createObserver.js
+├── Router.js
+│ └── createObserver.js
+└── createStorage.js
+```
+
+### 순환 의존성 방지
+
+1. **단방향 의존성**: 상위 레이어에서 하위 레이어로만 의존
+2. **인터페이스 분리**: 필요한 기능만 노출
+3. **의존성 주입**: 런타임에 의존성 주입
+
+## 🎨 디자인 패턴
+
+### 1. Observer Pattern (옵저버 패턴)
+
+```javascript
+// 상태 변경 시 구독자들에게 알림
+const { subscribe, notify } = createObserver();
+
+// 구독
+productStore.subscribe(render);
+
+// 알림
+const dispatch = (action) => {
+ const newState = reducer(state, action);
+ if (newState !== state) {
+ state = newState;
+ notify(); // 모든 구독자에게 알림
+ }
+};
+```
+
+### 2. Redux Pattern (리덕스 패턴)
+
+```javascript
+// 액션 → 리듀서 → 상태 업데이트
+const action = { type: "SET_PRODUCTS", payload: { products, totalCount } };
+const newState = reducer(currentState, action);
+```
+
+### 3. Event Delegation (이벤트 위임)
+
+```javascript
+// 상위 요소에서 하위 요소의 이벤트 처리
+document.body.addEventListener("click", (e) => {
+ const target = e.target.closest(".add-to-cart-btn");
+ if (target) {
+ const productId = target.getAttribute("data-product-id");
+ addToCart(productId);
+ }
+});
+```
+
+### 4. Higher-Order Function (고차 함수)
+
+```javascript
+// 라이프사이클 관리
+export const withLifecycle = ({ onMount, onUnmount, watches }, page) => {
+ return (...args) => {
+ // 마운트 로직
+ if (wasNewPage) {
+ mount(page);
+ }
+
+ // 의존성 감시
+ if (lifecycle.watches) {
+ lifecycle.watches.forEach(([getDeps, callback]) => {
+ const newDeps = getDeps();
+ if (depsChanged(newDeps, oldDeps)) {
+ callback();
+ }
+ });
+ }
+
+ return page(...args);
+ };
+};
+```
+
+### 5. Factory Pattern (팩토리 패턴)
+
+```javascript
+// 스토어 생성 팩토리
+export const createStore = (reducer, initialState) => {
+ // 스토어 인스턴스 생성 로직
+ return { getState, dispatch, subscribe };
+};
+
+// 스토리지 생성 팩토리
+export const createStorage = (key, storage = window.localStorage) => {
+ // 스토리지 인스턴스 생성 로직
+ return { get, set, reset };
+};
+```
+
+## ⚡ 성능 고려사항
+
+### 1. 렌더링 최적화
+
+```javascript
+// 배치 렌더링으로 불필요한 리렌더링 방지
+export const withBatch = (fn) => {
+ let scheduled = false;
+
+ return (...args) => {
+ if (scheduled) return;
+ scheduled = true;
+
+ queueMicrotask(() => {
+ scheduled = false;
+ fn(...args);
+ });
+ };
+};
+```
+
+### 2. 메모리 관리
+
+```javascript
+// WeakMap을 사용한 메모리 누수 방지
+const lifeCycles = new WeakMap();
+
+// 이벤트 리스너 정리
+const cleanup = () => {
+ document.body.removeEventListener("click", handleGlobalEvents);
+};
+```
+
+### 3. 네트워크 최적화
+
+```javascript
+// 무한 스크롤로 초기 로딩 시간 단축
+export const loadMoreProducts = async () => {
+ const state = productStore.getState();
+ const hasMore = state.products.length < state.totalCount;
+
+ if (!hasMore || state.loading) return;
+
+ router.query = { current: Number(router.query.current ?? 1) + 1 };
+ await loadProducts(false); // 기존 목록에 추가
+};
+```
+
+### 4. 이미지 최적화
+
+```html
+
+
+```
+
+### 5. 코드 분할
+
+```javascript
+// 동적 임포트로 초기 번들 크기 최적화
+const enableMocking = () =>
+ import("./mocks/browser.js").then(({ worker }) =>
+ worker.start({
+ serviceWorker: { url: `${BASE_URL}mockServiceWorker.js` },
+ onUnhandledRequest: "bypass"
+ })
+ );
+```
+
+## 🔧 확장성 고려사항
+
+### 1. 모듈화
+
+- 각 기능별로 독립적인 모듈 구성
+- 명확한 인터페이스 정의
+- 느슨한 결합, 강한 응집
+
+### 2. 플러그인 시스템
+
+```javascript
+// 라우터에 미들웨어 추가 가능
+router.use((req, res, next) => {
+ // 인증, 로깅 등
+ next();
+});
+```
+
+### 3. 테스트 가능성
+
+- 순수 함수 중심 설계
+- 의존성 주입을 통한 모킹 가능
+- 단위 테스트 친화적 구조
+
+이러한 아키텍처 설계를 통해 **유지보수성**, **확장성**, **성능**을 모두 고려한 견고한 애플리케이션을 구축할 수 있습니다.
diff --git a/docs/components-summary.md b/docs/components-summary.md
new file mode 100644
index 00000000..e48cb5b6
--- /dev/null
+++ b/docs/components-summary.md
@@ -0,0 +1,89 @@
+# 컴포넌트 시스템 요약
+
+## 🧩 컴포넌트 구조
+
+### 1. 함수형 컴포넌트 패턴
+```javascript
+// 기본 구조
+export function ComponentName(props) {
+ return `${props.content}
`;
+}
+```
+
+### 2. 주요 컴포넌트들
+
+#### ProductCard
+- 상품 정보 표시
+- 장바구니 추가 버튼
+- 이미지, 제목, 가격, 브랜드
+
+#### ProductList
+- 상품 목록 그리드
+- 로딩/에러 상태 처리
+- 무한 스크롤 지원
+
+#### SearchBar
+- 검색 입력창
+- 카테고리 필터
+- 정렬/개수 선택
+
+#### CartModal
+- 장바구니 모달
+- 수량 조절
+- 선택/삭제 기능
+
+#### Toast
+- 알림 메시지
+- 성공/에러/경고 타입
+
+### 3. 페이지 래퍼 패턴
+```javascript
+export const PageWrapper = ({ headerLeft, children }) => {
+ return `
+
+
+ ${children}
+ ${CartModal()}
+ ${Toast()}
+
+ `;
+};
+```
+
+### 4. 라이프사이클 관리
+```javascript
+export const HomePage = withLifecycle(
+ {
+ onMount: () => loadData(),
+ watches: [() => [router.query], () => updateData()]
+ },
+ () => renderComponent()
+);
+```
+
+## 🎯 핵심 특징
+
+- **함수형**: 모든 컴포넌트는 함수로 구현
+- **템플릿 리터럴**: HTML 문자열 반환
+- **Props 기반**: 매개변수로 데이터 전달
+- **조합 가능**: 작은 컴포넌트들을 조합하여 복잡한 UI 구성
+- **이벤트 위임**: data-* 속성으로 이벤트 처리
+
+## 📝 사용 예시
+
+```javascript
+// 컴포넌트 사용
+const productCard = ProductCard({
+ productId: "123",
+ title: "상품명",
+ price: 10000
+});
+
+// 페이지에서 조합
+const homePage = PageWrapper({
+ headerLeft: `쇼핑몰
`,
+ children: `${SearchBar()} ${ProductList()}`
+});
+```
+
+이런 식으로 간단하고 핵심적인 내용만 포함하여 작성하면 토큰 제한을 피할 수 있습니다.
diff --git a/packages/vanilla/server.js b/packages/vanilla/server.js
index b9a56d98..8b6beddb 100644
--- a/packages/vanilla/server.js
+++ b/packages/vanilla/server.js
@@ -1,34 +1,49 @@
import express from "express";
+import { getConfig } from "./server/config.js";
+import { asyncHandler, errorHandler, notFoundHandler } from "./server/errorHandler.js";
+import { setupMiddleware } from "./server/middleware.js";
+import { renderWithInitialData } from "./server/render.js";
+import { createHTMLTemplate } from "./server/template.js";
+import { server } from "./src/mocks/server-browser.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 config = getConfig();
+const { port, base } = config;
+
+// MSW 서버 시작
+server.listen({
+ onUnhandledRequest: "bypass",
+});
const app = express();
-const render = () => {
- return `안녕하세요
`;
-};
-
-app.get("*all", (req, res) => {
- res.send(
- `
-
-
-
-
-
- Vanilla Javascript SSR
-
-
-${render()}
-
-
- `.trim(),
- );
-});
+// 미들웨어 설정
+const vite = await setupMiddleware(app, config);
+
+// 라우트 설정
+app.get(
+ "*all",
+ asyncHandler(async (req, res) => {
+ // SSR 렌더링 (초기 데이터 포함)
+ const { appHtml, initialData } = await renderWithInitialData(req.url, req.query, vite);
+
+ // HTML 템플릿 생성 (초기 데이터 주입)
+ const html = createHTMLTemplate(appHtml, "", initialData);
+
+ res.send(html);
+ }),
+);
+
+// 404 에러 처리
+app.use(notFoundHandler);
+
+// 에러 처리 미들웨어
+app.use(errorHandler);
-// Start http server
+// 서버 시작
app.listen(port, () => {
- console.log(`React Server started at http://localhost:${port}`);
+ console.log(`🚀 Vanilla SSR Server started at http://localhost:${port}`);
+ console.log(`🌍 Environment: ${process.env.NODE_ENV || "development"}`);
+ console.log(`📍 Base URL: ${base}`);
+ console.log(`⏰ Started at: ${new Date().toISOString()}`);
});
diff --git a/packages/vanilla/server/config.js b/packages/vanilla/server/config.js
new file mode 100644
index 00000000..9682b617
--- /dev/null
+++ b/packages/vanilla/server/config.js
@@ -0,0 +1,25 @@
+/**
+ * 서버 설정
+ */
+export const config = {
+ development: {
+ port: 5173,
+ base: "/",
+ enableVite: true,
+ enableMSW: true,
+ },
+ production: {
+ port: process.env.PORT || 3000,
+ base: "/front_6th_chapter4-1/vanilla/",
+ enableVite: false,
+ enableMSW: false,
+ },
+};
+
+/**
+ * 현재 환경 설정 가져오기
+ */
+export const getConfig = () => {
+ const env = process.env.NODE_ENV || "development";
+ return config[env];
+};
diff --git a/packages/vanilla/server/errorHandler.js b/packages/vanilla/server/errorHandler.js
new file mode 100644
index 00000000..f6ba2d8c
--- /dev/null
+++ b/packages/vanilla/server/errorHandler.js
@@ -0,0 +1,41 @@
+/**
+ * 에러 처리 미들웨어
+ */
+export const errorHandler = (err, req, res, next) => {
+ console.error("Server Error:", err.stack);
+
+ // 이미 응답이 전송된 경우
+ if (res.headersSent) {
+ return next(err);
+ }
+
+ // 에러 상태 코드 설정
+ const status = err.status || err.statusCode || 500;
+
+ // 프로덕션 환경에서는 상세 에러 정보 숨김
+ const message = process.env.NODE_ENV === "production" ? "서버 오류가 발생했습니다." : err.message;
+
+ res.status(status).json({
+ error: message,
+ ...(process.env.NODE_ENV !== "production" && { stack: err.stack }),
+ });
+};
+
+/**
+ * 404 에러 처리 미들웨어
+ */
+export const notFoundHandler = (req, res) => {
+ res.status(404).json({
+ error: "요청한 리소스를 찾을 수 없습니다.",
+ path: req.path,
+ });
+};
+
+/**
+ * 비동기 에러 래퍼
+ */
+export const asyncHandler = (fn) => {
+ return (req, res, next) => {
+ Promise.resolve(fn(req, res, next)).catch(next);
+ };
+};
diff --git a/packages/vanilla/server/middleware.js b/packages/vanilla/server/middleware.js
new file mode 100644
index 00000000..b915c702
--- /dev/null
+++ b/packages/vanilla/server/middleware.js
@@ -0,0 +1,66 @@
+import express from "express";
+
+/**
+ * Express 미들웨어 설정
+ */
+export const setupMiddleware = async (app, config) => {
+ // 기본 미들웨어
+ app.use(express.json());
+ app.use(express.urlencoded({ extended: true }));
+
+ let vite;
+
+ // 개발 환경에서만 Vite 미들웨어 사용
+ if (config.enableVite) {
+ vite = await setupViteMiddleware(app, config);
+ } else {
+ await setupProductionMiddleware(app, config);
+ }
+
+ // MSW 서버 설정 (개발 환경에서만)
+ if (config.enableMSW) {
+ await setupMSWServer();
+ }
+
+ return vite;
+};
+
+/**
+ * Vite 개발 미들웨어 설정
+ */
+const setupViteMiddleware = async (app, config) => {
+ const { createServer } = await import("vite");
+ const vite = await createServer({
+ server: { middlewareMode: true },
+ appType: "custom",
+ base: config.base,
+ });
+
+ app.use(vite.middlewares);
+ return vite;
+};
+
+/**
+ * 프로덕션 미들웨어 설정
+ */
+const setupProductionMiddleware = async (app, config) => {
+ const compression = (await import("compression")).default;
+ const sirv = (await import("sirv")).default;
+
+ app.use(compression());
+ app.use(config.base, sirv("./dist/client", { extensions: [] }));
+};
+
+/**
+ * MSW 서버 설정
+ */
+const setupMSWServer = async () => {
+ try {
+ const { server } = await import("../src/mocks/server-browser.js");
+ server.listen();
+ console.log("✅ MSW 서버가 시작되었습니다.");
+ } catch (error) {
+ console.warn("⚠️ MSW 서버 시작 실패:", error.message);
+ console.log("💡 MSW 없이 서버를 계속 실행합니다.");
+ }
+};
diff --git a/packages/vanilla/server/render.js b/packages/vanilla/server/render.js
new file mode 100644
index 00000000..96159fef
--- /dev/null
+++ b/packages/vanilla/server/render.js
@@ -0,0 +1,296 @@
+import { productStore } from "../src/stores/productStore.js";
+import { serverStateManager } from "./stateManager.js";
+
+/**
+ * 서버 사이드 렌더링 함수
+ */
+export const render = async (url, query, vite = null) => {
+ console.log("🚀 SSR Render 시작:", { url, query, timestamp: new Date().toISOString() });
+
+ try {
+ // URL 파싱 및 라우트 매칭
+ const route = matchRoute(url);
+ console.log("🎯 매칭된 라우트:", route);
+
+ let result;
+ if (route.type === "product-detail") {
+ console.log("📦 상품 상세 페이지 렌더링");
+ result = await renderProductDetail(route.params.id, query, vite);
+ } else if (route.type === "home") {
+ console.log("🏠 홈페이지 렌더링");
+ result = await renderHomePage(query, vite);
+ } else {
+ console.log("❓ 알 수 없는 라우트, 404 페이지 렌더링");
+ result = await renderNotFoundPage(vite);
+ }
+
+ return result;
+ } catch (error) {
+ console.error("❌ SSR Render Error:", error);
+ return renderErrorPage(error);
+ }
+};
+
+/**
+ * 서버 사이드 렌더링 함수 (초기 데이터 포함)
+ */
+export const renderWithInitialData = async (url, query, vite = null) => {
+ console.log("🚀 SSR Render with Initial Data 시작:", { url, query, timestamp: new Date().toISOString() });
+
+ try {
+ // URL 파싱 및 라우트 매칭
+ const route = matchRoute(url);
+ console.log("🎯 매칭된 라우트:", route);
+
+ let appHtml, initialData;
+
+ if (route.type === "product-detail") {
+ console.log("📦 상품 상세 페이지 렌더링 (초기 데이터 포함)");
+ const result = await renderProductDetailWithData(route.params.id, query, vite);
+ appHtml = result.html;
+ initialData = result.initialData;
+ } else if (route.type === "home") {
+ console.log("🏠 홈페이지 렌더링 (초기 데이터 포함)");
+ const result = await renderHomePageWithData(query, vite);
+ appHtml = result.html;
+ initialData = result.initialData;
+ } else {
+ console.log("❓ 알 수 없는 라우트, 404 페이지 렌더링");
+ appHtml = await renderNotFoundPage(vite);
+ initialData = null;
+ }
+
+ return { appHtml, initialData };
+ } catch (error) {
+ console.error("❌ SSR Render with Initial Data Error:", error);
+ return {
+ appHtml: renderErrorPage(error),
+ initialData: null,
+ };
+ }
+};
+
+/**
+ * URL을 기반으로 라우트 매칭
+ */
+const matchRoute = (url) => {
+ // 상품 상세 페이지 패턴: /product/:id/
+ const productDetailMatch = url.match(/^\/product\/([^\/]+)\/?$/);
+ if (productDetailMatch) {
+ return {
+ type: "product-detail",
+ params: { id: productDetailMatch[1] },
+ };
+ }
+
+ // 홈페이지 패턴: / 또는 /?query=...
+ if (url === "/" || url.startsWith("/?")) {
+ return {
+ type: "home",
+ params: {},
+ };
+ }
+
+ // 매칭되지 않는 경우
+ return {
+ type: "not-found",
+ params: {},
+ };
+};
+
+/**
+ * 홈페이지 렌더링
+ */
+const renderHomePage = async (query, vite = null) => {
+ // 서버 상태 관리자를 통해 상태 초기화
+ const state = await serverStateManager.initializeHomeState(query);
+
+ // SSR용 HomePage 컴포넌트 동적 import
+ let HomePage;
+ if (vite) {
+ console.log("🔧 Vite SSR 모듈 로딩 (HomePage)");
+ const module = await vite.ssrLoadModule("./src/pages/HomePage.js");
+ HomePage = module.HomePage;
+ } else {
+ console.log("📦 일반 모듈 로딩 (HomePage)");
+ const module = await import("../src/pages/HomePage.js");
+ HomePage = module.HomePage;
+ }
+
+ console.log("🎨 홈페이지 컴포넌트 렌더링 시작");
+ const html = HomePage("", query, state);
+
+ console.log("✅ 홈페이지 SSR 렌더링 완료, HTML 길이:", html.length);
+ return html;
+};
+
+/**
+ * 홈페이지 렌더링 (초기 데이터 포함)
+ */
+const renderHomePageWithData = async (query, vite = null) => {
+ // 서버 상태 관리자를 통해 상태 초기화
+ const state = await serverStateManager.initializeHomeState(query);
+
+ // SSR용 HomePage 컴포넌트 동적 import
+ let HomePage;
+ if (vite) {
+ console.log("🔧 Vite SSR 모듈 로딩 (HomePage)");
+ const module = await vite.ssrLoadModule("./src/pages/HomePage.js");
+ HomePage = module.HomePage;
+ } else {
+ console.log("📦 일반 모듈 로딩 (HomePage)");
+ const module = await import("../src/pages/HomePage.js");
+ HomePage = module.HomePage;
+ }
+
+ console.log("🎨 홈페이지 컴포넌트 렌더링 시작 (초기 데이터 포함)");
+
+ // 서버 상태를 productStore에 주입
+ productStore.dispatch({
+ type: "SETUP",
+ payload: {
+ products: state.products,
+ totalCount: state.totalCount,
+ loading: false,
+ error: null,
+ status: "done",
+ categories: state.categories,
+ },
+ });
+
+ const html = HomePage("", query, state);
+
+ console.log("✅ 홈페이지 SSR 렌더링 완료 (초기 데이터 포함), HTML 길이:", html.length);
+
+ return {
+ html,
+ initialData: {
+ type: "home",
+ state,
+ query,
+ timestamp: new Date().toISOString(),
+ },
+ };
+};
+
+/**
+ * 상품 상세 페이지 렌더링
+ */
+const renderProductDetail = async (productId, query, vite = null) => {
+ // 서버 상태 관리자를 통해 상태 초기화
+ const state = await serverStateManager.initializeProductDetailState(productId);
+
+ // SSR용 ProductDetailPage 컴포넌트 동적 import
+ let ProductDetailPage;
+ if (vite) {
+ console.log("🔧 Vite SSR 모듈 로딩 (ProductDetailPage)");
+ const module = await vite.ssrLoadModule("./src/pages/ProductDetailPage.js");
+ ProductDetailPage = module.ProductDetailPage;
+ } else {
+ console.log("📦 일반 모듈 로딩 (ProductDetailPage)");
+ const module = await import("../src/pages/ProductDetailPage.js");
+ ProductDetailPage = module.ProductDetailPage;
+ }
+
+ console.log("🎨 상품 상세 컴포넌트 렌더링 시작");
+ const html = ProductDetailPage(`/product/${productId}/`, query, state);
+
+ console.log("✅ 상품 상세 SSR 렌더링 완료, HTML 길이:", html.length);
+ return html;
+};
+
+/**
+ * 상품 상세 페이지 렌더링 (초기 데이터 포함)
+ */
+const renderProductDetailWithData = async (productId, query, vite = null) => {
+ // 서버 상태 관리자를 통해 상태 초기화
+ const state = await serverStateManager.initializeProductDetailState(productId);
+
+ // SSR용 ProductDetailPage 컴포넌트 동적 import
+ let ProductDetailPage;
+ if (vite) {
+ console.log("🔧 Vite SSR 모듈 로딩 (ProductDetailPage)");
+ const module = await vite.ssrLoadModule("./src/pages/ProductDetailPage.js");
+ ProductDetailPage = module.ProductDetailPage;
+ } else {
+ console.log("📦 일반 모듈 로딩 (ProductDetailPage)");
+ const module = await import("../src/pages/ProductDetailPage.js");
+ ProductDetailPage = module.ProductDetailPage;
+ }
+
+ console.log("🎨 상품 상세 컴포넌트 렌더링 시작 (초기 데이터 포함)");
+
+ // 서버 상태를 productStore에 주입
+ productStore.dispatch({
+ type: "SETUP",
+ payload: {
+ currentProduct: state.product,
+ relatedProducts: [],
+ loading: false,
+ error: null,
+ status: "done",
+ categories: state.categories,
+ },
+ });
+
+ const html = ProductDetailPage(`/product/${productId}/`, query, state);
+
+ console.log("✅ 상품 상세 SSR 렌더링 완료 (초기 데이터 포함), HTML 길이:", html.length);
+
+ return {
+ html,
+ initialData: {
+ type: "product-detail",
+ state,
+ query,
+ productId,
+ timestamp: new Date().toISOString(),
+ },
+ };
+};
+
+/**
+ * 404 페이지 렌더링
+ */
+const renderNotFoundPage = async (vite = null) => {
+ console.log("❓ 404 페이지 렌더링");
+
+ // SSR용 NotFoundPage 컴포넌트 동적 import
+ let NotFoundPage;
+ if (vite) {
+ console.log("🔧 Vite SSR 모듈 로딩 (NotFoundPage)");
+ const module = await vite.ssrLoadModule("./src/pages/NotFoundPage.js");
+ NotFoundPage = module.NotFoundPage;
+ } else {
+ console.log("📦 일반 모듈 로딩 (NotFoundPage)");
+ const module = await import("../src/pages/NotFoundPage.js");
+ NotFoundPage = module.NotFoundPage;
+ }
+
+ console.log("🎨 404 컴포넌트 렌더링 시작");
+ const html = NotFoundPage(
+ "",
+ {},
+ {
+ loading: false,
+ status: "done",
+ },
+ );
+
+ console.log("✅ 404 SSR 렌더링 완료, HTML 길이:", html.length);
+ return html;
+};
+
+/**
+ * 에러 페이지 렌더링
+ */
+const renderErrorPage = (error) => {
+ return `
+
+
+
오류가 발생했습니다
+
${error.message}
+
+
+ `;
+};
diff --git a/packages/vanilla/server/stateManager.js b/packages/vanilla/server/stateManager.js
new file mode 100644
index 00000000..ff374ff9
--- /dev/null
+++ b/packages/vanilla/server/stateManager.js
@@ -0,0 +1,130 @@
+import { getCategories, getProduct, getProducts } from "../src/api/productApi.js";
+
+/**
+ * 서버 상태 관리자
+ */
+export class ServerStateManager {
+ constructor() {
+ this.cache = new Map();
+ this.cacheTimeout = 5 * 60 * 1000; // 5분 캐시
+ }
+
+ /**
+ * 홈페이지 상태 초기화
+ */
+ async initializeHomeState(query = {}) {
+ const cacheKey = `home-${JSON.stringify(query)}`;
+
+ // 캐시 확인
+ if (this.cache.has(cacheKey)) {
+ const cached = this.cache.get(cacheKey);
+ if (Date.now() - cached.timestamp < this.cacheTimeout) {
+ console.log("📋 홈페이지 상태 캐시 사용");
+ return cached.data;
+ }
+ }
+
+ console.log("🔄 홈페이지 상태 초기화 시작");
+
+ try {
+ const [productsData, categories] = await Promise.all([getProducts(query), getCategories()]);
+
+ const state = {
+ products: productsData.products,
+ categories,
+ pagination: productsData.pagination,
+ totalCount: productsData.pagination.total,
+ loading: false,
+ status: "done",
+ query,
+ };
+
+ // 캐시 저장
+ this.cache.set(cacheKey, {
+ data: state,
+ timestamp: Date.now(),
+ });
+
+ console.log("✅ 홈페이지 상태 초기화 완료:", {
+ productsCount: state.products.length,
+ totalCount: state.totalCount,
+ categoriesCount: Object.keys(state.categories).length,
+ });
+
+ return state;
+ } catch (error) {
+ console.error("❌ 홈페이지 상태 초기화 실패:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * 상품 상세 상태 초기화
+ */
+ async initializeProductDetailState(productId) {
+ const cacheKey = `product-${productId}`;
+
+ // 캐시 확인
+ if (this.cache.has(cacheKey)) {
+ const cached = this.cache.get(cacheKey);
+ if (Date.now() - cached.timestamp < this.cacheTimeout) {
+ console.log("📋 상품 상세 상태 캐시 사용");
+ return cached.data;
+ }
+ }
+
+ console.log("🔄 상품 상세 상태 초기화 시작:", productId);
+
+ try {
+ const [product, categories] = await Promise.all([getProduct(productId), getCategories()]);
+
+ const state = {
+ product,
+ categories,
+ loading: false,
+ status: "done",
+ productId,
+ };
+
+ // 캐시 저장
+ this.cache.set(cacheKey, {
+ data: state,
+ timestamp: Date.now(),
+ });
+
+ console.log("✅ 상품 상세 상태 초기화 완료:", {
+ productId,
+ productName: product?.name,
+ categoriesCount: Object.keys(categories).length,
+ });
+
+ return state;
+ } catch (error) {
+ console.error("❌ 상품 상세 상태 초기화 실패:", error);
+ throw error;
+ }
+ }
+
+ /**
+ * 캐시 클리어
+ */
+ clearCache() {
+ this.cache.clear();
+ console.log("🗑️ 서버 상태 캐시 클리어");
+ }
+
+ /**
+ * 만료된 캐시 정리
+ */
+ cleanupExpiredCache() {
+ const now = Date.now();
+ for (const [key, value] of this.cache.entries()) {
+ if (now - value.timestamp >= this.cacheTimeout) {
+ this.cache.delete(key);
+ }
+ }
+ }
+}
+
+// 전역 상태 관리자 인스턴스
+export const serverStateManager = new ServerStateManager();
diff --git a/packages/vanilla/server/template.js b/packages/vanilla/server/template.js
new file mode 100644
index 00000000..83d0c2a5
--- /dev/null
+++ b/packages/vanilla/server/template.js
@@ -0,0 +1,78 @@
+/**
+ * HTML 템플릿 생성 및 치환 유틸리티
+ */
+
+/**
+ * 기본 HTML 템플릿
+ */
+export const createHTMLTemplate = (appHtml, appHead = "", initialData = null) => {
+ const initialDataScript = initialData
+ ? ``
+ : "";
+
+ // SSR 확인을 위한 메타 정보
+ const ssrMeta = `
+
+
+
+
+ `;
+
+ return `
+
+
+
+
+
+ 쇼핑몰 (SSR)
+
+ ${ssrMeta}
+ ${appHead}
+
+
+ ${initialDataScript}
+
+
+
+ ${appHtml}
+
+
+
+ `.trim();
+};
+
+/**
+ * 기존 HTML 템플릿에서 치환
+ */
+export const replaceHTMLTemplate = (template, appHtml, appHead = "", initialData = null) => {
+ let result = template;
+
+ // app-html 치환
+ if (result.includes("")) {
+ result = result.replace("", appHtml);
+ }
+
+ // app-head 치환
+ if (result.includes("")) {
+ result = result.replace("", appHead);
+ }
+
+ // 초기 데이터 주입
+ if (initialData && !result.includes("window.__INITIAL_DATA__")) {
+ const initialDataScript = ``;
+ result = result.replace("", ` ${initialDataScript}\n`);
+ }
+
+ return result;
+};
diff --git a/packages/vanilla/src/api/productApi.js b/packages/vanilla/src/api/productApi.js
index c2330fbe..e20adba4 100644
--- a/packages/vanilla/src/api/productApi.js
+++ b/packages/vanilla/src/api/productApi.js
@@ -11,17 +11,17 @@ export async function getProducts(params = {}) {
sort,
});
- const response = await fetch(`/api/products?${searchParams}`);
+ const response = await fetch(`http://localhost/api/products?${searchParams}`);
return await response.json();
}
export async function getProduct(productId) {
- const response = await fetch(`/api/products/${productId}`);
+ const response = await fetch(`http://localhost/api/products/${productId}`);
return await response.json();
}
export async function getCategories() {
- const response = await fetch("/api/categories");
+ const response = await fetch("http://localhost/api/categories");
return await response.json();
}
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/events.js b/packages/vanilla/src/events.js
index 4d66284f..1b1af296 100644
--- a/packages/vanilla/src/events.js
+++ b/packages/vanilla/src/events.js
@@ -1,4 +1,3 @@
-import { addEvent, isNearBottom } from "./utils";
import { router } from "./router";
import {
addToCart,
@@ -16,7 +15,8 @@ import {
toggleCartSelect,
updateCartQuantity,
} from "./services";
-import { productStore, uiStore, UI_ACTIONS } from "./stores";
+import { productStore, UI_ACTIONS, uiStore } from "./stores";
+import { addEvent, isNearBottom } from "./utils";
/**
* 상품 관련 이벤트 등록
@@ -138,14 +138,33 @@ export function registerProductEvents() {
* 상품 상세 페이지 관련 이벤트 등록
*/
export function registerProductDetailEvents() {
- // 상품 클릭 시 상품 상세 페이지로 이동 (이미지 또는 제목)
- addEvent("click", ".product-image, .product-info", async (e) => {
+ // 상품 클릭 시 상품 상세 페이지로 이동 (전체 카드)
+ addEvent("click", ".product-card", async (e) => {
+ console.log("🖱️ 상품 카드 클릭 이벤트 발생:", e.target);
+
+ // 장바구니 버튼 클릭은 제외
+ if (e.target.classList.contains("add-to-cart-btn") || e.target.closest(".add-to-cart-btn")) {
+ console.log("🛒 장바구니 버튼 클릭으로 인한 이벤트 무시");
+ return;
+ }
+
const productCard = e.target.closest(".product-card");
- if (!productCard) return;
+ console.log("🔍 찾은 상품 카드:", productCard);
+
+ if (!productCard) {
+ console.log("❌ 상품 카드를 찾을 수 없음");
+ return;
+ }
const productId = productCard.getAttribute("data-product-id");
- if (!productId) return;
+ console.log("🆔 상품 ID:", productId);
+
+ if (!productId) {
+ console.log("❌ 상품 ID를 찾을 수 없음");
+ return;
+ }
+ console.log("🚀 상품 상세 페이지로 이동:", `/product/${productId}/`);
// 상품 상세 페이지로 이동
router.push(`/product/${productId}/`);
});
diff --git a/packages/vanilla/src/lib/RouterSSR.js b/packages/vanilla/src/lib/RouterSSR.js
new file mode 100644
index 00000000..a86730b4
--- /dev/null
+++ b/packages/vanilla/src/lib/RouterSSR.js
@@ -0,0 +1,108 @@
+/**
+ * 서버 사이드 렌더링용 라우터
+ */
+export class RouterSSR {
+ #routes;
+ #baseUrl;
+
+ constructor(baseUrl = "") {
+ this.#routes = new Map();
+ this.#baseUrl = baseUrl.replace(/\/$/, "");
+ }
+
+ get baseUrl() {
+ return this.#baseUrl;
+ }
+
+ get query() {
+ return {};
+ }
+
+ set query(newQuery) {
+ // SSR에서는 쿼리 변경 불가
+ }
+
+ get params() {
+ return {};
+ }
+
+ get route() {
+ return null;
+ }
+
+ get target() {
+ return null;
+ }
+
+ subscribe(fn) {
+ // SSR에서는 구독 불가
+ }
+
+ /**
+ * 라우트 등록 (SSR에서는 실제로 사용되지 않음)
+ * @param {string} path - 경로 패턴
+ * @param {Function} handler - 라우트 핸들러
+ */
+ addRoute(path, handler) {
+ // SSR에서는 라우트 등록만 하고 실제 매칭은 서버에서 처리
+ this.#routes.set(path, handler);
+ }
+
+ /**
+ * 네비게이션 (SSR에서는 사용되지 않음)
+ * @param {string} url - 이동할 경로
+ */
+ push(url) {
+ // SSR에서는 네비게이션 불가
+ }
+
+ /**
+ * 라우터 시작 (SSR에서는 사용되지 않음)
+ */
+ start() {
+ // SSR에서는 시작 불가
+ }
+
+ /**
+ * 쿼리 파라미터를 객체로 파싱
+ * @param {string} 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, baseUrl = "", pathname = "/") => {
+ const updatedQuery = { ...newQuery };
+
+ // 빈 값들 제거
+ Object.keys(updatedQuery).forEach((key) => {
+ if (updatedQuery[key] === null || updatedQuery[key] === undefined || updatedQuery[key] === "") {
+ delete updatedQuery[key];
+ }
+ });
+
+ const queryString = RouterSSR.stringifyQuery(updatedQuery);
+ return `${baseUrl}${pathname}${queryString ? `?${queryString}` : ""}`;
+ };
+}
diff --git a/packages/vanilla/src/lib/createStorage.js b/packages/vanilla/src/lib/createStorage.js
index 08b504f2..d68dd5c9 100644
--- a/packages/vanilla/src/lib/createStorage.js
+++ b/packages/vanilla/src/lib/createStorage.js
@@ -33,3 +33,11 @@ export const createStorage = (key, storage = window.localStorage) => {
return { get, set, reset };
};
+
+export const createSSRStorage = (key) => {
+ return {
+ get: () => null,
+ set: () => {},
+ reset: () => {},
+ };
+};
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..cc5805e0 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 "./createStorage.js";
+export * from "./createStore.js";
+export * from "./Router.js";
+export * from "./server-router.js";
diff --git a/packages/vanilla/src/lib/server-router.js b/packages/vanilla/src/lib/server-router.js
new file mode 100644
index 00000000..315aceb5
--- /dev/null
+++ b/packages/vanilla/src/lib/server-router.js
@@ -0,0 +1,146 @@
+/**
+ * 간단한 SPA 라우터
+ */
+import { createObserver } from "./createObserver.js";
+
+export class RouterSSR {
+ #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 RouterSSR.parseQuery();
+ return {};
+ }
+
+ set query(newQuery) {
+ const newUrl = RouterSSR.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) {
+ const { pathname } = new URL(url, window.location.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() {
+ //
+ }
+
+ /**
+ * 라우터 시작
+ */
+ 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, baseUrl = "") => {
+ //
+ };
+}
diff --git a/packages/vanilla/src/main-server.js b/packages/vanilla/src/main-server.js
index 40b58858..1b90d71d 100644
--- a/packages/vanilla/src/main-server.js
+++ b/packages/vanilla/src/main-server.js
@@ -1,4 +1,2 @@
-export const render = async (url, query) => {
- console.log({ url, query });
- return "";
-};
+// render.js의 함수들을 re-export
+export { render, renderWithInitialData } from "../server/render.js";
diff --git a/packages/vanilla/src/main.js b/packages/vanilla/src/main.js
index 4c3f2765..8d1317c1 100644
--- a/packages/vanilla/src/main.js
+++ b/packages/vanilla/src/main.js
@@ -1,9 +1,10 @@
-import { registerGlobalEvents } from "./utils";
-import { initRender } from "./render";
+import { BASE_URL } from "./constants.js";
import { registerAllEvents } from "./events";
-import { loadCartFromStorage } from "./services";
+import { initRender } from "./render";
import { router } from "./router";
-import { BASE_URL } from "./constants.js";
+import { loadCartFromStorage } from "./services";
+import { initializeFromSSR } from "./stores";
+import { getRegisteredEvents, registerGlobalEvents } from "./utils";
const enableMocking = () =>
import("./mocks/browser.js").then(({ worker }) =>
@@ -16,8 +17,19 @@ const enableMocking = () =>
);
function main() {
+ // SSR 초기 데이터가 있으면 상태 초기화
+ if (window.__INITIAL_DATA__) {
+ console.log("🔄 SSR 초기 데이터로 상태 초기화:", window.__INITIAL_DATA__);
+ initializeFromSSR(window.__INITIAL_DATA__);
+ }
+
+ // 이벤트 등록 순서 중요: 먼저 이벤트 핸들러들을 등록하고, 그 다음에 전역 이벤트 리스너를 등록
registerAllEvents();
registerGlobalEvents();
+
+ // 이벤트 등록 상태 확인 (디버깅용)
+ console.log("📋 등록된 이벤트 핸들러:", getRegisteredEvents());
+
loadCartFromStorage();
initRender();
router.start();
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-browser.js b/packages/vanilla/src/mocks/server-browser.js
new file mode 100644
index 00000000..1099169f
--- /dev/null
+++ b/packages/vanilla/src/mocks/server-browser.js
@@ -0,0 +1,6 @@
+import { setupServer } from "msw/node";
+import { handlers } from "./handlers.js";
+
+const server = setupServer(...handlers);
+
+export { server };
diff --git a/packages/vanilla/src/pages/HomePage.js b/packages/vanilla/src/pages/HomePage.js
index ca08c26c..b96e7fec 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 } from "../stores";
import { router, withLifecycle } from "../router";
import { loadProducts, loadProductsAndCategories } from "../services";
+import { productStore } from "../stores";
import { PageWrapper } from "./PageWrapper.js";
export const HomePage = withLifecycle(
@@ -17,9 +17,15 @@ export const HomePage = withLifecycle(
() => loadProducts(true),
],
},
- () => {
- const productState = productStore.getState();
- const { search: searchQuery, limit, sort, category1, category2 } = router.query;
+ (url, query, param) => {
+ const productState = typeof window !== "undefined" ? productStore.getState() : param;
+ const {
+ search: searchQuery,
+ limit,
+ sort,
+ category1,
+ category2,
+ } = typeof window !== "undefined" ? router.query : query;
const { products, loading, error, totalCount, categories } = productState;
const category = { category1, category2 };
const hasMore = products.length < totalCount;
diff --git a/packages/vanilla/src/pages/ProductDetailPage.js b/packages/vanilla/src/pages/ProductDetailPage.js
index 73d0ec30..e7934e23 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 { loadProductDetailForPage } from "../services";
+import { productStore } from "../stores";
import { PageWrapper } from "./PageWrapper.js";
const loadingContent = `
@@ -35,6 +35,18 @@ const ErrorContent = ({ error }) => `
`;
function ProductDetail({ product, relatedProducts = [] }) {
+ // product가 null인 경우 처리
+ if (!product) {
+ return `
+
+
+
상품을 찾을 수 없습니다
+
요청하신 상품이 존재하지 않거나 삭제되었습니다.
+
+
+ `;
+ }
+
const {
productId,
title,
diff --git a/packages/vanilla/src/render.js b/packages/vanilla/src/render.js
index 87f30f19..0699d4cf 100644
--- a/packages/vanilla/src/render.js
+++ b/packages/vanilla/src/render.js
@@ -1,6 +1,6 @@
-import { cartStore, productStore, uiStore } from "./stores";
-import { router } from "./router";
-import { HomePage, NotFoundPage, ProductDetailPage } from "./pages";
+import { HomePage, NotFoundPage, ProductDetailPage } from "./pages/index.js";
+import { router } from "./router/index.js";
+import { cartStore, productStore, uiStore } from "./stores/index.js";
import { withBatch } from "./utils";
// 홈 페이지 (상품 목록)
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 d897ee76..1e04e904 100644
--- a/packages/vanilla/src/router/router.js
+++ b/packages/vanilla/src/router/router.js
@@ -1,5 +1,5 @@
// 글로벌 라우터 인스턴스
-import { Router } from "../lib";
import { BASE_URL } from "../constants.js";
+import { Router, RouterSSR } from "../lib/index.js";
-export const router = new Router(BASE_URL);
+export const router = typeof window !== "undefined" ? new Router(BASE_URL) : new RouterSSR();
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..418fe413 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 { router } from "../router/index.js";
+import { initialProductState, PRODUCT_ACTIONS, productStore } from "../stores/index.js";
export const loadProductsAndCategories = async () => {
router.query = { current: undefined }; // 항상 첫 페이지로 초기화
@@ -26,6 +26,7 @@ export const loadProductsAndCategories = async () => {
productStore.dispatch({
type: PRODUCT_ACTIONS.SETUP,
payload: {
+ // 이 값을 서버에서 똑같이 생성해서 hompage 에 인자로 넣는다.
products,
categories,
totalCount: total,
@@ -123,7 +124,7 @@ export const loadProductDetailForPage = async (productId) => {
const currentProduct = productStore.getState().currentProduct;
if (productId === currentProduct?.productId) {
// 관련 상품 로드 (같은 category2 기준)
- if (currentProduct.category2) {
+ if (currentProduct && currentProduct.category2) {
await loadRelatedProducts(currentProduct.category2, productId);
}
return;
@@ -148,7 +149,7 @@ export const loadProductDetailForPage = async (productId) => {
});
// 관련 상품 로드 (같은 category2 기준)
- if (product.category2) {
+ if (product && product.category2) {
await loadRelatedProducts(product.category2, productId);
}
} catch (error) {
diff --git a/packages/vanilla/src/storage/cartStorage.js b/packages/vanilla/src/storage/cartStorage.js
index 7aa68383..95f70d3d 100644
--- a/packages/vanilla/src/storage/cartStorage.js
+++ b/packages/vanilla/src/storage/cartStorage.js
@@ -1,3 +1,4 @@
-import { createStorage } from "../lib";
+import { createSSRStorage, createStorage } from "../lib/index.js";
-export const cartStorage = createStorage("shopping_cart");
+export const cartStorage =
+ typeof window !== "undefined" ? createStorage("shopping_cart") : createSSRStorage("shopping_cart");
diff --git a/packages/vanilla/src/stores/cartStore.js b/packages/vanilla/src/stores/cartStore.js
index fe61f167..45bb172c 100644
--- a/packages/vanilla/src/stores/cartStore.js
+++ b/packages/vanilla/src/stores/cartStore.js
@@ -1,6 +1,6 @@
-import { createStore } from "../lib";
-import { CART_ACTIONS } from "./actionTypes";
+import { createStore } from "../lib/index.js";
import { cartStorage } from "../storage/index.js";
+import { CART_ACTIONS } from "./actionTypes.js";
/**
* 장바구니 스토어 초기 상태
diff --git a/packages/vanilla/src/stores/index.js b/packages/vanilla/src/stores/index.js
index 36fefd54..1e81d14c 100644
--- a/packages/vanilla/src/stores/index.js
+++ b/packages/vanilla/src/stores/index.js
@@ -1,4 +1,77 @@
-export * from "./actionTypes";
-export * from "./productStore";
-export * from "./cartStore";
-export * from "./uiStore";
+import { productStore } from "./productStore.js";
+import { uiStore } from "./uiStore.js";
+
+export * from "./actionTypes.js";
+export * from "./cartStore.js";
+export * from "./productStore.js";
+export * from "./uiStore.js";
+
+/**
+ * SSR 초기 데이터로 상태 초기화
+ */
+export const initializeFromSSR = (initialData) => {
+ console.log("🔄 SSR 초기 데이터로 상태 초기화 시작:", initialData);
+ console.log(productStore);
+ if (!initialData || !initialData.state) {
+ console.log("⚠️ 유효하지 않은 초기 데이터");
+ return;
+ }
+
+ const { type, state } = initialData;
+
+ if (type === "home") {
+ console.log("🏠 홈페이지 상태 초기화");
+ // productStore와 uiStore 상태 초기화
+ if (state.products) {
+ productStore.dispatch({
+ type: "SET_PRODUCTS",
+ payload: state.products,
+ });
+ }
+ if (state.categories) {
+ productStore.dispatch({
+ type: "SET_CATEGORIES",
+ payload: state.categories,
+ });
+ }
+ if (state.totalCount !== undefined) {
+ productStore.dispatch({
+ type: "SET_TOTAL_COUNT",
+ payload: state.totalCount,
+ });
+ }
+ if (state.query) {
+ productStore.dispatch({
+ type: "SET_QUERY",
+ payload: state.query,
+ });
+ }
+ } else if (type === "product-detail") {
+ console.log("📦 상품 상세 상태 초기화");
+ if (state.product) {
+ productStore.dispatch({
+ type: "SET_PRODUCT",
+ payload: state.product,
+ });
+ }
+ if (state.categories) {
+ productStore.dispatch({
+ type: "SET_CATEGORIES",
+ payload: state.categories,
+ });
+ }
+ }
+
+ // UI 상태 초기화
+ uiStore.dispatch({
+ type: "SET_LOADING",
+ payload: false,
+ });
+
+ uiStore.dispatch({
+ type: "SET_STATUS",
+ payload: "done",
+ });
+
+ console.log("✅ SSR 초기 데이터로 상태 초기화 완료");
+};
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 스토어 초기 상태
diff --git a/packages/vanilla/src/utils/eventUtils.js b/packages/vanilla/src/utils/eventUtils.js
index d6031d41..17a1e9b4 100644
--- a/packages/vanilla/src/utils/eventUtils.js
+++ b/packages/vanilla/src/utils/eventUtils.js
@@ -9,14 +9,21 @@ const eventHandlers = {};
*/
const handleGlobalEvents = (e) => {
const handlers = eventHandlers[e.type];
- if (!handlers) return;
+ if (!handlers) {
+ console.log(`🔍 이벤트 핸들러 없음: ${e.type}`, e.target);
+ return;
+ }
+
+ console.log(`🎯 이벤트 발생: ${e.type}`, e.target, "등록된 핸들러:", Object.keys(handlers));
// 각 선택자에 대해 확인
for (const [selector, handler] of Object.entries(handlers)) {
const targetElement = e.target.closest(selector);
+ console.log(`🔍 선택자 매칭 시도: ${selector}`, targetElement);
// 일치하는 요소가 있으면 핸들러 실행
if (targetElement) {
+ console.log(`✅ 핸들러 실행: ${selector}`);
try {
handler(e);
} catch (error) {
@@ -36,7 +43,9 @@ export const registerGlobalEvents = (() => {
return;
}
- Object.keys(eventHandlers).forEach((eventType) => {
+ // 모든 이벤트 타입에 대해 전역 이벤트 리스너 등록
+ const eventTypes = ["click", "change", "keydown", "keyup", "submit", "scroll"];
+ eventTypes.forEach((eventType) => {
document.body.addEventListener(eventType, handleGlobalEvents);
});
@@ -56,4 +65,12 @@ export const addEvent = (eventType, selector, handler) => {
}
eventHandlers[eventType][selector] = handler;
+ console.log(`🎯 이벤트 등록: ${eventType} -> ${selector}`);
+};
+
+/**
+ * 등록된 이벤트 핸들러 확인 (디버깅용)
+ */
+export const getRegisteredEvents = () => {
+ return eventHandlers;
};