diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index c8ec1fa4..b39ce2c9 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -13,62 +13,486 @@
### 기본과제 (Vanilla SSR & SSG)
#### Express SSR 서버
-- [ ] Express 미들웨어 기반 서버 구현
-- [ ] 개발/프로덕션 환경 분기 처리
-- [ ] HTML 템플릿 치환 (``, ``)
+
+- [x] Express 미들웨어 기반 서버 구현
+- [x] 개발/프로덕션 환경 분기 처리
+- [x] HTML 템플릿 치환 (``, ``)
#### 서버 사이드 렌더링
-- [ ] 서버에서 동작하는 Router 구현
-- [ ] 서버 데이터 프리페칭 (상품 목록, 상품 상세)
-- [ ] 서버 상태관리 초기화
+
+- [x] 서버에서 동작하는 Router 구현
+- [x] 서버 데이터 프리페칭 (상품 목록, 상품 상세)
+- [x] 서버 상태관리 초기화
#### 클라이언트 Hydration
-- [ ] `window.__INITIAL_DATA__` 스크립트 주입
-- [ ] 클라이언트 상태 복원
-- [ ] 서버-클라이언트 데이터 일치
+
+- [x] `window.__INITIAL_DATA__` 스크립트 주입
+- [x] 클라이언트 상태 복원
+- [x] 서버-클라이언트 데이터 일치
#### Static Site Generation
-- [ ] 동적 라우트 SSG (상품 상세 페이지들)
-- [ ] 빌드 타임 페이지 생성
-- [ ] 파일 시스템 기반 배포
+
+- [x] 동적 라우트 SSG (상품 상세 페이지들)
+- [x] 빌드 타임 페이지 생성
+- [x] 파일 시스템 기반 배포
### 심화과제 (React SSR & SSG)
#### React SSR
+
- [ ] `renderToString` 서버 렌더링
- [ ] TypeScript SSR 모듈 빌드
- [ ] Universal React Router (서버/클라이언트 분기)
- [ ] React 상태관리 서버 초기화
#### React Hydration
+
- [ ] Hydration 불일치 방지
- [ ] 클라이언트 상태 복원
#### Static Site Generation
+
- [ ] 동적 라우트 SSG (상품 상세 페이지들)
- [ ] 빌드 타임 페이지 생성
- [ ] 파일 시스템 기반 배포
## 아하! 모먼트 (A-ha! Moment)
-
+### AsyncLocalStorage를 통한 요청 격리의 중요성
+
+처음에는 서버 렌더링에서 `globalThis`를 사용해 요청 컨텍스트를 저장했습니다. 그러다 동시 요청이 들어올 때 데이터가 섞이는 버그를 발견했고, Node.js의 `AsyncLocalStorage`를 알게 되었습니다. 이를 통해 각 요청이 독립적인 컨텍스트를 가질 수 있다는 것을 깨달았습니다.
+
+```javascript
+// packages/vanilla/src/lib/asyncContext.js
+await runWithContext(context, async () => {
+ const html = await render(route.component);
+ // 이 스코프 안에서 getContext()는 항상 올바른 요청의 context를 반환
+});
+```
+
+### withLifecycle의 서버/클라이언트 분기
+
+라이프사이클을 HOC 패턴으로 추상화하면서, 서버에서는 async 함수를, 클라이언트에서는 동기 함수를 반환해야 한다는 것을 알게 되었습니다. 서버는 데이터 프리페칭을 기다려야 하지만, 클라이언트는 즉시 렌더링 후 비동기로 업데이트하는 것이 UX에 더 좋기 때문입니다.
## 자유롭게 회고하기
-
+### 구현하면서 집중한 부분들
+
+#### 1. 요청별 컨텍스트 격리
+
+서버 사이드 렌더링의 핵심은 동시 요청 처리입니다. 각 요청이 독립적인 상태를 유지해야 하므로 `AsyncLocalStorage`를 활용했습니다.
+
+```javascript
+// packages/vanilla/src/lib/asyncContext.js
+export const runWithContext = async (context, callback) => {
+ await initAsyncLocalStorage();
+ return asyncLocalStorage.run(context, callback);
+};
+```
+
+#### 2. 라이프사이클 관리
+
+기존 클라이언트 전용 라이프사이클을 서버에서도 동작하도록 확장했습니다. `withLifecycle` HOC를 통해:
+
+- 서버: `onMount`를 실행하고 데이터를 `initialData`에 저장
+- 클라이언트: `window.__INITIAL_DATA__`에서 초기 데이터 복원 후 필요시 재요청
+
+#### 3. 메타태그 동적 생성
+
+상품 상세 페이지의 경우, SEO를 위해 동적 메타태그가 필수입니다. `updateInitialData`를 통해 렌더링 중에 메타 정보를 수집하고, 서버에서 HTML에 주입하는 방식을 구현했습니다.
+
+```javascript
+// packages/vanilla/src/pages/ProductDetailPage.js
+updateInitialData("meta", {
+ title: `${product.title} - 쇼핑몰`,
+ description: `${product.title} - ${product.brand || "쇼핑몰"}`,
+ image: product.image,
+});
+```
+
+#### 4. Universal Code
+
+가능한 한 많은 코드를 서버/클라이언트에서 공유하도록 설계했습니다:
+
+- `Router`: 동일한 라우팅 로직
+- `withLifecycle`: 환경에 따라 다른 동작이지만 동일한 인터페이스
+- `createStorage`: 서버에서는 no-op, 클라이언트에서는 localStorage 사용
+
+#### 5. Static Site Generation (SSG)
+
+**SSR 인프라 100% 재사용**: 기존 `render()`, `runWithContext()`, `withLifecycle` 등 모든 SSR 로직을 그대로 사용했습니다. 별도의 SSG 전용 코드를 작성하지 않고, 빌드 타임에 SSR을 실행하는 방식으로 구현했습니다.
+
+**Global fetch 폴리필**: MSW는 실제 네트워크 요청을 보내려고 해서 샌드박스 환경에서 실패했습니다. 대신 `globalThis.fetch`를 직접 폴리필해서 `items.json` 데이터를 반환하도록 구현했습니다.
+
+```javascript
+// packages/vanilla/static-site-generate.js
+globalThis.fetch = async (url) => {
+ const urlObj = new URL(url, "http://localhost");
+ // /api/products, /api/products/:id, /api/categories 모두 처리
+ return { ok: true, json: async () => mockData };
+};
+```
+
+**340개 페이지 자동 생성**: `items.json`에서 상품 ID를 추출해 각 상품마다 `/product/:id/index.html` 생성. 홈페이지와 404 페이지까지 총 342개의 정적 HTML 파일이 생성됩니다.
+
+**SEO 최적화 완료**: 각 상품 페이지는 동적 메타태그가 포함되어 있고, 완전히 렌더링된 HTML을 제공하므로 검색 엔진 크롤러가 내용을 바로 인덱싱할 수 있습니다.
+
+### 아쉬운 부분들
+
+#### 1. Store의 서버 격리 미흡
+
+`productStore`, `cartStore` 등은 싱글톤으로 동작합니다. 서버 환경에서는 각 요청마다 독립적인 store 인스턴스가 필요할 수 있는데, 현재는 `initialData`를 통해 클라이언트에 전달하는 방식으로만 해결했습니다. 동시 요청이 많아지면 race condition 가능성이 있습니다.
+
+#### 2. 에러 바운더리 부재
+
+서버 렌더링 중 에러 발생 시 적절한 fallback이 없습니다. 현재는 try-catch로 잡힌 에러만 처리하고 있어, 예상치 못한 에러가 발생하면 서버가 크래시할 수 있습니다.
+
+#### 3. 성능 최적화 여지
+
+- HTML 템플릿이 매 요청마다 문자열로 생성됨 (캐싱 가능)
+- CSS 파일을 매번 읽음 (메모리 캐싱 필요)
+- 중복되는 HTML 템플릿 코드 (404와 일반 페이지)
+
+### 기술적 도전과 해결
+
+#### 1. AsyncLocalStorage 동작 원리 이해하기
+
+**문제 상황:**
+초기에는 각 요청의 컨텍스트를 `globalThis`에 저장했는데, 동시에 2개의 요청이 들어오면 나중 요청이 먼저 요청의 데이터를 덮어쓰는 문제가 발생했습니다.
+
+```javascript
+// 잘못된 접근 (초기 버전)
+app.get("/product/:id", async (req, res) => {
+ globalThis.pathname = req.url;
+ globalThis.params = req.params;
+ const html = await render(); // 비동기 중에 다른 요청이 globalThis를 덮어쓸 수 있음
+});
+```
+
+**해결 방법:**
+`AsyncLocalStorage`는 비동기 콜백 체인 전체에서 격리된 스토리지를 제공합니다. `async_hooks` 모듈의 실행 컨텍스트 추적을 활용해, 같은 요청에서 파생된 모든 비동기 작업이 동일한 컨텍스트를 공유하도록 했습니다.
+
+```javascript
+// packages/vanilla/src/lib/asyncContext.js
+const { AsyncLocalStorage } = await import("node:async_hooks");
+const asyncLocalStorage = new AsyncLocalStorage();
+
+// 각 요청마다 독립적인 컨텍스트 실행
+export const runWithContext = async (context, callback) => {
+ return asyncLocalStorage.run(context, callback);
+};
+
+// 어디서든 현재 요청의 컨텍스트 접근 가능
+export const getContext = () => {
+ return asyncLocalStorage?.getStore();
+};
+```
+
+**핵심 포인트:**
+
+- `asyncLocalStorage.run()`으로 시작된 비동기 체인 내부의 모든 함수는 같은 store에 접근
+- `await`, `Promise`, `setTimeout` 등을 거쳐도 컨텍스트가 유지됨
+- 다른 요청의 실행 컨텍스트와 완전히 격리됨
+
+#### 2. Universal Router - 하나의 코드, 두 가지 환경
+
+**도전 과제:**
+클라이언트의 `window.location`과 서버의 `req` 객체는 완전히 다른 API입니다. 하지만 라우팅 로직은 동일해야 합니다.
+
+**구현 전략:**
+환경 감지 레이어를 통해 통일된 인터페이스를 제공했습니다.
+
+```javascript
+// packages/vanilla/src/lib/Router.js
+function getOrigin() {
+ if ("window" in globalThis) {
+ return window.location.origin;
+ } else {
+ const context = getContext(); // AsyncLocalStorage에서 가져옴
+ return context.origin;
+ }
+}
+
+function getPathname() {
+ if ("window" in globalThis) {
+ return window.location.pathname;
+ } else {
+ const context = getContext();
+ return String(context.pathname);
+ }
+}
+```
+
+이렇게 하면 Router 클래스의 다른 메서드들은 환경에 상관없이 `getPathname()`, `getOrigin()`만 호출하면 됩니다.
+
+**쿼리 파라미터 통합:**
+Express는 `req.query`로 객체를 주지만, 클라이언트는 `location.search`로 문자열을 줍니다. `URLSearchParams`로 통일했습니다.
+
+```javascript
+// packages/vanilla/src/lib/Router.js
+get query() {
+ return Router.parseQuery(getSearch());
+}
+
+static parseQuery = (search) => {
+ const params = new URLSearchParams(search);
+ const query = {};
+ for (const [key, value] of params) {
+ query[key] = value;
+ }
+ return query;
+};
+```
+
+서버에서는 `context.search` 객체를 받아서 다시 쿼리 문자열로 변환해 `URLSearchParams`에 넣으므로, 파싱 로직이 완전히 동일하게 동작합니다.
+
+#### 3. withLifecycle의 서버/클라이언트 이중 동작
+
+**설계 결정:**
+`withLifecycle`은 동일한 HOC지만, 반환하는 함수의 동작 방식이 환경에 따라 달라야 합니다.
+
+```javascript
+// packages/vanilla/src/router/withLifecycle.js
+export const withLifecycle = ({ onMount, onUnmount, watches } = {}, page) => {
+ const lifecycle = getPageLifecycle(page);
+
+ // 라이프사이클 설정
+ if (typeof onMount === "function") lifecycle.mount = onMount;
+ if (typeof onUnmount === "function") lifecycle.unmount = onUnmount;
+ if (Array.isArray(watches)) lifecycle.watches = watches;
+
+ // 서버 환경: async 함수 반환
+ if (isServer) {
+ return async (...args) => {
+ await mount(page); // 데이터 페칭 완료 대기
+ return page(...args); // 렌더링
+ };
+ }
+
+ // 클라이언트 환경: 동기 함수 반환
+ return (...args) => {
+ const wasNewPage = pageState.current !== page;
+
+ if (pageState.current && wasNewPage) {
+ unmount(pageState.current); // 이전 페이지 정리
+ }
+
+ pageState.current = page;
+
+ if (wasNewPage) {
+ mount(page); // 비동기지만 await 안 함 (UX를 위해 즉시 렌더링)
+ } else {
+ // 같은 페이지 재렌더링 시 watches 체크
+ lifecycle.watches?.forEach(([getDeps, callback], index) => {
+ const newDeps = getDeps();
+ if (depsChanged(newDeps, lifecycle.deps[index])) {
+ callback();
+ }
+ lifecycle.deps[index] = Array.isArray(newDeps) ? [...newDeps] : [];
+ });
+ }
+
+ return page(...args);
+ };
+};
+```
+
+**왜 이렇게 분기했나:**
+
+- **서버**: SEO와 초기 로딩을 위해 완전히 렌더링된 HTML이 필요 → `await`로 데이터 로딩 완료를 기다림
+- **클라이언트**: 빠른 화면 전환을 위해 즉시 렌더링하고 데이터는 백그라운드에서 로드 → `await` 없이 비동기 실행
+
+#### 4. initialData를 통한 Hydration
+
+**Hydration 불일치 방지:**
+서버에서 렌더링한 HTML과 클라이언트의 첫 렌더링이 다르면 React Hydration Error와 비슷한 문제가 발생합니다.
+
+**해결 방법:**
+
+```javascript
+// packages/vanilla/server.js
+await runWithContext(context, async () => {
+ const html = await render(route.component);
+
+ // 서버 렌더링 중 수집된 데이터를 스크립트로 주입
+ res.send(`
+
${html}
+
+ `);
+});
+```
+
+클라이언트는 페이지 로드 시 `window.__INITIAL_DATA__`가 있으면 API 재호출을 건너뜁니다:
+
+```javascript
+// packages/vanilla/src/services/productService.js
+export const loadProductsAndCategories = async () => {
+ if ("window" in globalThis && productStore.getState().status === "done") {
+ return productStore.getState(); // 이미 서버에서 로드됨
+ }
+
+ // API 호출...
+};
+```
+
+**메타태그 동적 생성도 동일한 패턴:**
+
+```javascript
+// packages/vanilla/src/pages/ProductDetailPage.js
+updateInitialData("meta", {
+ title: `${product.title} - 쇼핑몰`,
+ description: product.description,
+ image: product.image,
+});
+```
+
+렌더링 중에 `initialData.meta`를 설정하면, 서버가 이를 읽어서 HTML ``에 주입합니다.
+
+#### 5. createStorage의 환경별 no-op 처리
+
+**문제:**
+`localStorage`는 브라우저에만 존재하므로, 서버에서 실행하면 에러가 발생합니다.
+
+**해결:**
+
+```javascript
+// packages/vanilla/src/lib/createStorage.js
+export const createStorage = (key, storage) => {
+ if (!("window" in globalThis)) {
+ // 서버 환경: no-op 반환
+ return {
+ get: () => null,
+ set: () => {},
+ reset: () => {},
+ };
+ }
+
+ storage = storage ?? window.localStorage;
+ // 실제 로직...
+};
+```
+
+이렇게 하면 `cartStorage.get()` 같은 코드를 서버/클라이언트 양쪽에서 안전하게 호출할 수 있습니다.
+
+#### 6. SSG - Global fetch 폴리필로 네트워크 격리
+
+**문제:**
+MSW의 `setupServer`를 사용하려 했으나, 실제 네트워크 요청을 시도해서 샌드박스 환경에서 `EPERM` 에러가 발생했습니다.
+
+**해결:**
+빌드 타임에는 네트워크가 필요 없습니다. `globalThis.fetch`를 직접 폴리필해서 `items.json` 데이터를 반환하도록 구현했습니다.
+
+```javascript
+// packages/vanilla/static-site-generate.js
+globalThis.fetch = async (url) => {
+ const urlObj = new URL(url, "http://localhost");
+ const pathname = urlObj.pathname;
+
+ // /api/products
+ if (pathname === "/api/products") {
+ const filtered = filterAndSortProducts(query);
+ return { ok: true, json: async () => ({ products: filtered, ... }) };
+ }
+
+ // /api/products/:id
+ const productMatch = pathname.match(/^\/api\/products\/(.+)$/);
+ if (productMatch) {
+ const product = items.find(item => item.productId === productMatch[1]);
+ return { ok: true, json: async () => product };
+ }
+};
+```
+
+**왜 이 방법이 좋은가:**
+
+- MSW 같은 무거운 라이브러리 없이 순수 JavaScript로 해결
+- 네트워크 요청이 전혀 발생하지 않아 빌드 속도가 빠름
+- `items.json` 데이터를 직접 사용하므로 SSR handlers와 로직이 완전히 일치
+- 샌드박스 환경에서도 문제없이 동작
+
+**SSG와 SSR 코드 100% 재사용:**
+
+```javascript
+// 동일한 render 함수 사용
+import { render } from "./src/main-server.js";
+import { runWithContext } from "./src/lib/asyncContext.js";
+
+// SSR과 똑같은 방식으로 페이지 렌더링
+await runWithContext(context, async () => {
+ const html = await render(route.component);
+});
+```
+
+SSG는 본질적으로 "빌드 타임에 실행하는 SSR"입니다. 기존 SSR 인프라를 전혀 수정하지 않고, fetch만 폴리필해서 340개 페이지를 자동 생성했습니다.
+
+### 배운 점
+
+- **AsyncLocalStorage의 강력함**: 동시 요청을 격리하는 Node.js의 핵심 메커니즘. 이게 없으면 모든 상태를 요청 객체에 직접 전달해야 함
+- **Universal Code의 핵심은 추상화**: `window` 체크만으로 대부분의 환경 차이를 흡수할 수 있음
+- **서버는 동기적, 클라이언트는 비동기적**: 같은 기능도 UX와 SEO 요구사항에 따라 다른 실행 전략이 필요
+- **Hydration은 데이터 동기화**: 서버 렌더링 시점의 데이터를 클라이언트에 전달하는 게 핵심
+- **SSG = 빌드 타임 SSR**: SSG를 위해 새로운 코드를 작성할 필요 없음. SSR 로직을 빌드 타임에 실행하고 결과를 파일로 저장하면 됨
+- **프레임워크의 가치 재발견**: Next.js가 이 모든 것을 자동으로 처리해준다는 사실에 감사. 특히 `getStaticPaths`와 `getStaticProps`의 편리함을 실감
## 리뷰 받고 싶은 내용
-
+### 1. AsyncLocalStorage와 Store의 관계
+
+현재 `AsyncLocalStorage`로 요청 컨텍스트(`origin`, `pathname`, `params`, `initialData`)는 격리했지만, `productStore` 자체는 여전히 전역 싱글톤입니다.
+
+```javascript
+// packages/vanilla/src/stores/productStore.js
+export const productStore = createStore(initialProductState, productReducer);
+```
+
+서버 렌더링 중에 `productStore.dispatch()`를 호출하는데, 동시에 두 요청이 다른 상품을 조회하면 store 상태가 섞일 수 있습니다.
+
+**질문:**
+
+- `productStore`도 `AsyncLocalStorage`에 넣어서 각 요청마다 독립적인 인스턴스를 만들어야 할까요?
+- 아니면 서버에서는 store를 사용하지 않고, `initialData`에 직접 데이터를 담는 방식으로 리팩토링해야 할까요?
+- Redux의 SSR 방식처럼 각 요청마다 `createStore()`를 새로 호출하는 게 정답일까요?
+
+현재는 다행히 렌더링이 빠르게 끝나서 실질적인 충돌이 없지만, 부하 테스트를 하면 문제가 발생할 것 같습니다.
+
+### 2. Router의 params 접근 방식
+
+Router에서 `params`를 가져올 때 두 가지 방법을 혼용하고 있습니다:
+
+```javascript
+// packages/vanilla/src/lib/Router.js
+get params() {
+ if (this.#route?.params) {
+ return this.#route.params; // 클라이언트: 라우트 매칭 결과에서
+ }
+ if ("window" in globalThis) {
+ return {};
+ }
+ const context = getContext(); // 서버: AsyncLocalStorage에서
+ return context.params ?? {};
+}
+```
+
+**문제점:**
+서버에서는 Express의 `req.params`를 컨텍스트에 저장했지만, 클라이언트에서는 자체 정규식 매칭으로 추출합니다. 두 방식의 결과가 항상 일치한다고 보장할 수 있을까요?
+
+특히 인코딩 문제(`/product/한글` 같은 URL)나 특수문자가 있을 때 차이가 생길 수 있을 것 같습니다. 서버에서도 Express Router가 아닌 자체 정규식으로 통일해야 할까요?
+
+### 3. initialData의 Hydration 타이밍
+
+현재 `window.__INITIAL_DATA__`를 확인하는 로직이 각 서비스에 흩어져 있습니다:
+
+```javascript
+// packages/vanilla/src/services/productService.js
+if ("window" in globalThis && productStore.getState().status === "done") {
+ return productStore.getState();
+}
+```
+
+**질문:**
+
+- `main.js`에서 앱 시작 시 `window.__INITIAL_DATA__`를 읽어서 모든 store를 한 번에 초기화하는 게 더 깔끔하지 않을까요?
+- 현재 방식은 각 페이지가 마운트될 때마다 `status === "done"` 체크를 하는데, 이게 정말 안전할까요?
+- `window.__INITIAL_DATA__`를 사용한 후에는 삭제해서 메모리를 해제하는 게 좋을까요?
diff --git a/e2e/createTests.ts b/e2e/createTests.ts
index 9b6246b7..b301a9fa 100644
--- a/e2e/createTests.ts
+++ b/e2e/createTests.ts
@@ -666,6 +666,7 @@ export const createSSRTest = (baseUrl: string) => {
// SSR로 렌더링된 초기 HTML에 상품 목록이 포함되어야 함
const bodyContent = await page.locator("body").textContent();
+
expect(bodyContent).toContain("총");
expect(bodyContent).toContain("개");
@@ -684,8 +685,12 @@ export const createSSRTest = (baseUrl: string) => {
// HTML에 window.__INITIAL_DATA__ 스크립트가 포함되어 있는지 확인
expect(html).toContain(
- `"products":[{"title":"PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장","link":"https://smartstore.naver.com/main/products/7522712674","image":"https://shopping-phinf.pstatic.net/main_8506721/85067212996.1.jpg","lprice":"220","hprice":"","mallName":"기브N기브","productId":"85067212996","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이","link":"https://smartstore.naver.com/main/products/9396357056","image":"https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg","lprice":"230","hprice":"","mallName":"EASYWAY","productId":"86940857379","productType":"2","brand":"이지웨이건축자재","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"실리카겔 50g 습기제거제 제품 /산업 신발 의류 방습제","link":"https://smartstore.naver.com/main/products/4549948287","image":"https://shopping-phinf.pstatic.net/main_8209446/82094468339.4.jpg","lprice":"280","hprice":"","mallName":"제이제이상사","productId":"82094468339","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"제습/방향/탈취","category4":"제습제"},{"title":"두꺼운 고급 무지쇼핑백 종이쇼핑백 주문제작 소량 로고인쇄 선물용 종이가방 세로형1호","link":"https://smartstore.naver.com/main/products/8643964296","image":"https://shopping-phinf.pstatic.net/main_8618846/86188464619.14.jpg","lprice":"350","hprice":"","mallName":"세모쇼핑백","productId":"86188464619","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방충망 셀프교체 미세먼지 롤 창문 모기장 알루미늄망 60cmX20cm","link":"https://smartstore.naver.com/main/products/4814730329","image":"https://shopping-phinf.pstatic.net/main_8235925/82359253087.18.jpg","lprice":"420","hprice":"","mallName":"파머스홈","productId":"82359253087","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"방충망 미세먼지 롤 창문 모기장 DIY 100cmx10cm","link":"https://smartstore.naver.com/main/products/668979777","image":"https://shopping-phinf.pstatic.net/main_1112415/11124150101.10.jpg","lprice":"450","hprice":"","mallName":"동백물산","productId":"11124150101","productType":"2","brand":"메쉬코리아","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"현관문고무패킹 문틈막이 방화문가스켓 현관 우풍 소음 벌레 외풍차단 틈새막이 방음재 일반형","link":"https://smartstore.naver.com/main/products/4976480580","image":"https://shopping-phinf.pstatic.net/main_8252100/82521000904.2.jpg","lprice":"1390","hprice":"","mallName":"나라종합","productId":"82521000904","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"풍지판 창문 틈새막이 샷시 바람막이 창틀 벌레차단 외풍차단","link":"https://smartstore.naver.com/main/products/261719599","image":"https://shopping-phinf.pstatic.net/main_8131970/8131970722.30.jpg","lprice":"1690","hprice":"","mallName":"리빙포유","productId":"8131970722","productType":"2","brand":"리빙포유","maker":"세일인터내셔널","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"태양 홈키파 엘비이 알파 수성 에어졸 500ml, 1개","link":"https://search.shopping.naver.com/catalog/52481568603","image":"https://shopping-phinf.pstatic.net/main_5248156/52481568603.20250114124554.jpg","lprice":"1820","hprice":"","mallName":"네이버","productId":"52481568603","productType":"1","brand":"홈키파","maker":"태양","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"에어졸/스프레이"},{"title":"탈부착 방충망 자석쫄대 방풍비닐 창문방충망 셀프시공 DIY 백색 100cm","link":"https://smartstore.naver.com/main/products/2042376373","image":"https://shopping-phinf.pstatic.net/main_1179488/11794889307.3.jpg","lprice":"2190","hprice":"","mallName":"한반도철망","productId":"11794889307","productType":"2","brand":"한반도철망","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"창틀벌레 모풍지판 창문 벌레 차단 틈새 창문틈 막이 방충망","link":"https://smartstore.naver.com/main/products/6293889960","image":"https://shopping-phinf.pstatic.net/main_8383839/83838392449.1.jpg","lprice":"2300","hprice":"","mallName":"우예스토어","productId":"83838392449","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"나노 아트2 전기 매립 콘센트 커버 2구","link":"https://smartstore.naver.com/main/products/7170895087","image":"https://shopping-phinf.pstatic.net/main_8471539/84715395409.1.jpg","lprice":"2500","hprice":"","mallName":"터치전기","productId":"84715395409","productType":"2","brand":"나노","maker":"나노","category1":"생활/건강","category2":"공구","category3":"전기용품","category4":"기타 전기용품"},{"title":"날파리 퇴치 초파리 트랩 뿌리파리 벌레 파리 벼룩파리 끈끈이 플라이스틱","link":"https://smartstore.naver.com/main/products/6792117787","image":"https://shopping-phinf.pstatic.net/main_8433661/84336618109.2.jpg","lprice":"2700","hprice":"","mallName":"메디데이","productId":"84336618109","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"끈끈이"},{"title":"나이키 리유저블 쇼핑백 소형 타포린백 쇼퍼백 에코백 장바구니 운동 헬스 가방 방수","link":"https://smartstore.naver.com/main/products/6642533357","image":"https://shopping-phinf.pstatic.net/main_8418703/84187033679.6.jpg","lprice":"2890","hprice":"","mallName":"소울 컴퍼니sc","productId":"84187033679","productType":"2","brand":"나이키","maker":"나이키","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방문방음 문틈 창문 방문 틈새막이 소음차단 문틈막이 방음재 고무 문풍지 현관문 패킹 I형","link":"https://smartstore.naver.com/main/products/6106851858","image":"https://shopping-phinf.pstatic.net/main_8365135/83651351346.10.jpg","lprice":"2900","hprice":"","mallName":"주알보","productId":"83651351346","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"에어컨 세정제 세척제 청소 셀프 클리너 곰팡이 냄새 제거제 스프레이 330ml","link":"https://smartstore.naver.com/main/products/4426750526","image":"https://shopping-phinf.pstatic.net/main_8197127/81971273079.7.jpg","lprice":"3000","hprice":"","mallName":"-에띠리얼-","productId":"81971273079","productType":"2","brand":"산도깨비","maker":"산도깨비","category1":"생활/건강","category2":"생활용품","category3":"세제/세정제","category4":"에어컨세정제"},{"title":"포장용 롤 에어캡 뽁뽁이 0.2T 경포장용 20cm x 50M 1롤","link":"https://smartstore.naver.com/main/products/5182465882","image":"https://shopping-phinf.pstatic.net/main_8272698/82726987088.5.jpg","lprice":"3500","hprice":"","mallName":"황금상사스토어","productId":"82726987088","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"단열시트"},{"title":"하수구트랩 배수구 냄새제거 차단 화장실 욕실 40-99mm","link":"https://smartstore.naver.com/main/products/5008920074","image":"https://shopping-phinf.pstatic.net/main_8255344/82553440741.14.jpg","lprice":"4000","hprice":"","mallName":"낭만 탐구소","productId":"82553440741","productType":"2","brand":"낭만탐구소","maker":"","category1":"생활/건강","category2":"욕실용품","category3":"샤워기/수전용품","category4":"배수구캡"},{"title":"땡큐 순수 천연펄프 3겹 14m, 30롤, 1팩","link":"https://search.shopping.naver.com/catalog/54647347924","image":"https://shopping-phinf.pstatic.net/main_5464734/54647347924.20250508140616.jpg","lprice":"4990","hprice":"","mallName":"네이버","productId":"54647347924","productType":"1","brand":"땡큐","maker":"","category1":"생활/건강","category2":"생활용품","category3":"화장지","category4":"롤화장지"},{"title":"고양이 난간 안전망 복층 베란다 방묘창 방묘문 방충망 캣도어 일반형검정1mx1m","link":"https://smartstore.naver.com/main/products/6187449408","image":"https://shopping-phinf.pstatic.net/main_8373194/83731948985.5.jpg","lprice":"5000","hprice":"","mallName":"나이스메쉬","productId":"83731948985","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"반려동물","category3":"리빙용품","category4":"안전문"}],"categories":{"생활/건강":{"생활용품":{},"주방용품":{},"문구/사무용품":{},"자동차용품":{},"구강위생용품":{},"수납/정리용품":{},"욕실용품":{},"세탁용품":{},"공구":{},"청소용품":{},"정원/원예용품":{},"수집품":{},"관상어용품":{},"반려동물":{}},"디지털/가전":{"태블릿PC":{},"노트북":{}}},"totalCount":340`,
+ `"products":[{"title":"PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장","link":"https://smartstore.naver.com/main/products/7522712674","image":"https://shopping-phinf.pstatic.net/main_8506721/85067212996.1.jpg","lprice":"220","hprice":"","mallName":"기브N기브","productId":"85067212996","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이","link":"https://smartstore.naver.com/main/products/9396357056","image":"https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg","lprice":"230","hprice":"","mallName":"EASYWAY","productId":"86940857379","productType":"2","brand":"이지웨이건축자재","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"실리카겔 50g 습기제거제 제품 /산업 신발 의류 방습제","link":"https://smartstore.naver.com/main/products/4549948287","image":"https://shopping-phinf.pstatic.net/main_8209446/82094468339.4.jpg","lprice":"280","hprice":"","mallName":"제이제이상사","productId":"82094468339","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"제습/방향/탈취","category4":"제습제"},{"title":"두꺼운 고급 무지쇼핑백 종이쇼핑백 주문제작 소량 로고인쇄 선물용 종이가방 세로형1호","link":"https://smartstore.naver.com/main/products/8643964296","image":"https://shopping-phinf.pstatic.net/main_8618846/86188464619.14.jpg","lprice":"350","hprice":"","mallName":"세모쇼핑백","productId":"86188464619","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방충망 셀프교체 미세먼지 롤 창문 모기장 알루미늄망 60cmX20cm","link":"https://smartstore.naver.com/main/products/4814730329","image":"https://shopping-phinf.pstatic.net/main_8235925/82359253087.18.jpg","lprice":"420","hprice":"","mallName":"파머스홈","productId":"82359253087","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"방충망 미세먼지 롤 창문 모기장 DIY 100cmx10cm","link":"https://smartstore.naver.com/main/products/668979777","image":"https://shopping-phinf.pstatic.net/main_1112415/11124150101.10.jpg","lprice":"450","hprice":"","mallName":"동백물산","productId":"11124150101","productType":"2","brand":"메쉬코리아","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"현관문고무패킹 문틈막이 방화문가스켓 현관 우풍 소음 벌레 외풍차단 틈새막이 방음재 일반형","link":"https://smartstore.naver.com/main/products/4976480580","image":"https://shopping-phinf.pstatic.net/main_8252100/82521000904.2.jpg","lprice":"1390","hprice":"","mallName":"나라종합","productId":"82521000904","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"풍지판 창문 틈새막이 샷시 바람막이 창틀 벌레차단 외풍차단","link":"https://smartstore.naver.com/main/products/261719599","image":"https://shopping-phinf.pstatic.net/main_8131970/8131970722.30.jpg","lprice":"1690","hprice":"","mallName":"리빙포유","productId":"8131970722","productType":"2","brand":"리빙포유","maker":"세일인터내셔널","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"태양 홈키파 엘비이 알파 수성 에어졸 500ml, 1개","link":"https://search.shopping.naver.com/catalog/52481568603","image":"https://shopping-phinf.pstatic.net/main_5248156/52481568603.20250114124554.jpg","lprice":"1820","hprice":"","mallName":"네이버","productId":"52481568603","productType":"1","brand":"홈키파","maker":"태양","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"에어졸/스프레이"},{"title":"탈부착 방충망 자석쫄대 방풍비닐 창문방충망 셀프시공 DIY 백색 100cm","link":"https://smartstore.naver.com/main/products/2042376373","image":"https://shopping-phinf.pstatic.net/main_1179488/11794889307.3.jpg","lprice":"2190","hprice":"","mallName":"한반도철망","productId":"11794889307","productType":"2","brand":"한반도철망","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"창틀벌레 모풍지판 창문 벌레 차단 틈새 창문틈 막이 방충망","link":"https://smartstore.naver.com/main/products/6293889960","image":"https://shopping-phinf.pstatic.net/main_8383839/83838392449.1.jpg","lprice":"2300","hprice":"","mallName":"우예스토어","productId":"83838392449","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"나노 아트2 전기 매립 콘센트 커버 2구","link":"https://smartstore.naver.com/main/products/7170895087","image":"https://shopping-phinf.pstatic.net/main_8471539/84715395409.1.jpg","lprice":"2500","hprice":"","mallName":"터치전기","productId":"84715395409","productType":"2","brand":"나노","maker":"나노","category1":"생활/건강","category2":"공구","category3":"전기용품","category4":"기타 전기용품"},{"title":"날파리 퇴치 초파리 트랩 뿌리파리 벌레 파리 벼룩파리 끈끈이 플라이스틱","link":"https://smartstore.naver.com/main/products/6792117787","image":"https://shopping-phinf.pstatic.net/main_8433661/84336618109.2.jpg","lprice":"2700","hprice":"","mallName":"메디데이","productId":"84336618109","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"끈끈이"},{"title":"나이키 리유저블 쇼핑백 소형 타포린백 쇼퍼백 에코백 장바구니 운동 헬스 가방 방수","link":"https://smartstore.naver.com/main/products/6642533357","image":"https://shopping-phinf.pstatic.net/main_8418703/84187033679.6.jpg","lprice":"2890","hprice":"","mallName":"소울 컴퍼니sc","productId":"84187033679","productType":"2","brand":"나이키","maker":"나이키","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방문방음 문틈 창문 방문 틈새막이 소음차단 문틈막이 방음재 고무 문풍지 현관문 패킹 I형","link":"https://smartstore.naver.com/main/products/6106851858","image":"https://shopping-phinf.pstatic.net/main_8365135/83651351346.10.jpg","lprice":"2900","hprice":"","mallName":"주알보","productId":"83651351346","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"에어컨 세정제 세척제 청소 셀프 클리너 곰팡이 냄새 제거제 스프레이 330ml","link":"https://smartstore.naver.com/main/products/4426750526","image":"https://shopping-phinf.pstatic.net/main_8197127/81971273079.7.jpg","lprice":"3000","hprice":"","mallName":"-에띠리얼-","productId":"81971273079","productType":"2","brand":"산도깨비","maker":"산도깨비","category1":"생활/건강","category2":"생활용품","category3":"세제/세정제","category4":"에어컨세정제"},{"title":"포장용 롤 에어캡 뽁뽁이 0.2T 경포장용 20cm x 50M 1롤","link":"https://smartstore.naver.com/main/products/5182465882","image":"https://shopping-phinf.pstatic.net/main_8272698/82726987088.5.jpg","lprice":"3500","hprice":"","mallName":"황금상사스토어","productId":"82726987088","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"단열시트"},{"title":"하수구트랩 배수구 냄새제거 차단 화장실 욕실 40-99mm","link":"https://smartstore.naver.com/main/products/5008920074","image":"https://shopping-phinf.pstatic.net/main_8255344/82553440741.14.jpg","lprice":"4000","hprice":"","mallName":"낭만 탐구소","productId":"82553440741","productType":"2","brand":"낭만탐구소","maker":"","category1":"생활/건강","category2":"욕실용품","category3":"샤워기/수전용품","category4":"배수구캡"},{"title":"땡큐 순수 천연펄프 3겹 14m, 30롤, 1팩","link":"https://search.shopping.naver.com/catalog/54647347924","image":"https://shopping-phinf.pstatic.net/main_5464734/54647347924.20250508140616.jpg","lprice":"4990","hprice":"","mallName":"네이버","productId":"54647347924","productType":"1","brand":"땡큐","maker":"","category1":"생활/건강","category2":"생활용품","category3":"화장지","category4":"롤화장지"},{"title":"고양이 난간 안전망 복층 베란다 방묘창 방묘문 방충망 캣도어 일반형검정1mx1m","link":"https://smartstore.naver.com/main/products/6187449408","image":"https://shopping-phinf.pstatic.net/main_8373194/83731948985.5.jpg","lprice":"5000","hprice":"","mallName":"나이스메쉬","productId":"83731948985","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"반려동물","category3":"리빙용품","category4":"안전문"}]`,
+ );
+ expect(html).toContain(
+ `"categories":{"생활/건강":{"생활용품":{},"주방용품":{},"문구/사무용품":{},"자동차용품":{},"구강위생용품":{},"수납/정리용품":{},"욕실용품":{},"세탁용품":{},"공구":{},"청소용품":{},"정원/원예용품":{},"수집품":{},"관상어용품":{},"반려동물":{}},"디지털/가전":{"태블릿PC":{},"노트북":{}}}`,
);
+ expect(html).toContain(`"totalCount":340`);
});
});
@@ -856,8 +861,12 @@ export const createSSGTest = (baseUrl: string) => {
// HTML에 window.__INITIAL_DATA__ 스크립트가 포함되어 있는지 확인
expect(html).toContain(
- `"products":[{"title":"PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장","link":"https://smartstore.naver.com/main/products/7522712674","image":"https://shopping-phinf.pstatic.net/main_8506721/85067212996.1.jpg","lprice":"220","hprice":"","mallName":"기브N기브","productId":"85067212996","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이","link":"https://smartstore.naver.com/main/products/9396357056","image":"https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg","lprice":"230","hprice":"","mallName":"EASYWAY","productId":"86940857379","productType":"2","brand":"이지웨이건축자재","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"실리카겔 50g 습기제거제 제품 /산업 신발 의류 방습제","link":"https://smartstore.naver.com/main/products/4549948287","image":"https://shopping-phinf.pstatic.net/main_8209446/82094468339.4.jpg","lprice":"280","hprice":"","mallName":"제이제이상사","productId":"82094468339","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"제습/방향/탈취","category4":"제습제"},{"title":"두꺼운 고급 무지쇼핑백 종이쇼핑백 주문제작 소량 로고인쇄 선물용 종이가방 세로형1호","link":"https://smartstore.naver.com/main/products/8643964296","image":"https://shopping-phinf.pstatic.net/main_8618846/86188464619.14.jpg","lprice":"350","hprice":"","mallName":"세모쇼핑백","productId":"86188464619","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방충망 셀프교체 미세먼지 롤 창문 모기장 알루미늄망 60cmX20cm","link":"https://smartstore.naver.com/main/products/4814730329","image":"https://shopping-phinf.pstatic.net/main_8235925/82359253087.18.jpg","lprice":"420","hprice":"","mallName":"파머스홈","productId":"82359253087","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"방충망 미세먼지 롤 창문 모기장 DIY 100cmx10cm","link":"https://smartstore.naver.com/main/products/668979777","image":"https://shopping-phinf.pstatic.net/main_1112415/11124150101.10.jpg","lprice":"450","hprice":"","mallName":"동백물산","productId":"11124150101","productType":"2","brand":"메쉬코리아","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"현관문고무패킹 문틈막이 방화문가스켓 현관 우풍 소음 벌레 외풍차단 틈새막이 방음재 일반형","link":"https://smartstore.naver.com/main/products/4976480580","image":"https://shopping-phinf.pstatic.net/main_8252100/82521000904.2.jpg","lprice":"1390","hprice":"","mallName":"나라종합","productId":"82521000904","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"풍지판 창문 틈새막이 샷시 바람막이 창틀 벌레차단 외풍차단","link":"https://smartstore.naver.com/main/products/261719599","image":"https://shopping-phinf.pstatic.net/main_8131970/8131970722.30.jpg","lprice":"1690","hprice":"","mallName":"리빙포유","productId":"8131970722","productType":"2","brand":"리빙포유","maker":"세일인터내셔널","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"태양 홈키파 엘비이 알파 수성 에어졸 500ml, 1개","link":"https://search.shopping.naver.com/catalog/52481568603","image":"https://shopping-phinf.pstatic.net/main_5248156/52481568603.20250114124554.jpg","lprice":"1820","hprice":"","mallName":"네이버","productId":"52481568603","productType":"1","brand":"홈키파","maker":"태양","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"에어졸/스프레이"},{"title":"탈부착 방충망 자석쫄대 방풍비닐 창문방충망 셀프시공 DIY 백색 100cm","link":"https://smartstore.naver.com/main/products/2042376373","image":"https://shopping-phinf.pstatic.net/main_1179488/11794889307.3.jpg","lprice":"2190","hprice":"","mallName":"한반도철망","productId":"11794889307","productType":"2","brand":"한반도철망","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"창틀벌레 모풍지판 창문 벌레 차단 틈새 창문틈 막이 방충망","link":"https://smartstore.naver.com/main/products/6293889960","image":"https://shopping-phinf.pstatic.net/main_8383839/83838392449.1.jpg","lprice":"2300","hprice":"","mallName":"우예스토어","productId":"83838392449","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"나노 아트2 전기 매립 콘센트 커버 2구","link":"https://smartstore.naver.com/main/products/7170895087","image":"https://shopping-phinf.pstatic.net/main_8471539/84715395409.1.jpg","lprice":"2500","hprice":"","mallName":"터치전기","productId":"84715395409","productType":"2","brand":"나노","maker":"나노","category1":"생활/건강","category2":"공구","category3":"전기용품","category4":"기타 전기용품"},{"title":"날파리 퇴치 초파리 트랩 뿌리파리 벌레 파리 벼룩파리 끈끈이 플라이스틱","link":"https://smartstore.naver.com/main/products/6792117787","image":"https://shopping-phinf.pstatic.net/main_8433661/84336618109.2.jpg","lprice":"2700","hprice":"","mallName":"메디데이","productId":"84336618109","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"끈끈이"},{"title":"나이키 리유저블 쇼핑백 소형 타포린백 쇼퍼백 에코백 장바구니 운동 헬스 가방 방수","link":"https://smartstore.naver.com/main/products/6642533357","image":"https://shopping-phinf.pstatic.net/main_8418703/84187033679.6.jpg","lprice":"2890","hprice":"","mallName":"소울 컴퍼니sc","productId":"84187033679","productType":"2","brand":"나이키","maker":"나이키","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방문방음 문틈 창문 방문 틈새막이 소음차단 문틈막이 방음재 고무 문풍지 현관문 패킹 I형","link":"https://smartstore.naver.com/main/products/6106851858","image":"https://shopping-phinf.pstatic.net/main_8365135/83651351346.10.jpg","lprice":"2900","hprice":"","mallName":"주알보","productId":"83651351346","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"에어컨 세정제 세척제 청소 셀프 클리너 곰팡이 냄새 제거제 스프레이 330ml","link":"https://smartstore.naver.com/main/products/4426750526","image":"https://shopping-phinf.pstatic.net/main_8197127/81971273079.7.jpg","lprice":"3000","hprice":"","mallName":"-에띠리얼-","productId":"81971273079","productType":"2","brand":"산도깨비","maker":"산도깨비","category1":"생활/건강","category2":"생활용품","category3":"세제/세정제","category4":"에어컨세정제"},{"title":"포장용 롤 에어캡 뽁뽁이 0.2T 경포장용 20cm x 50M 1롤","link":"https://smartstore.naver.com/main/products/5182465882","image":"https://shopping-phinf.pstatic.net/main_8272698/82726987088.5.jpg","lprice":"3500","hprice":"","mallName":"황금상사스토어","productId":"82726987088","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"단열시트"},{"title":"하수구트랩 배수구 냄새제거 차단 화장실 욕실 40-99mm","link":"https://smartstore.naver.com/main/products/5008920074","image":"https://shopping-phinf.pstatic.net/main_8255344/82553440741.14.jpg","lprice":"4000","hprice":"","mallName":"낭만 탐구소","productId":"82553440741","productType":"2","brand":"낭만탐구소","maker":"","category1":"생활/건강","category2":"욕실용품","category3":"샤워기/수전용품","category4":"배수구캡"},{"title":"땡큐 순수 천연펄프 3겹 14m, 30롤, 1팩","link":"https://search.shopping.naver.com/catalog/54647347924","image":"https://shopping-phinf.pstatic.net/main_5464734/54647347924.20250508140616.jpg","lprice":"4990","hprice":"","mallName":"네이버","productId":"54647347924","productType":"1","brand":"땡큐","maker":"","category1":"생활/건강","category2":"생활용품","category3":"화장지","category4":"롤화장지"},{"title":"고양이 난간 안전망 복층 베란다 방묘창 방묘문 방충망 캣도어 일반형검정1mx1m","link":"https://smartstore.naver.com/main/products/6187449408","image":"https://shopping-phinf.pstatic.net/main_8373194/83731948985.5.jpg","lprice":"5000","hprice":"","mallName":"나이스메쉬","productId":"83731948985","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"반려동물","category3":"리빙용품","category4":"안전문"}],"categories":{"생활/건강":{"생활용품":{},"주방용품":{},"문구/사무용품":{},"자동차용품":{},"구강위생용품":{},"수납/정리용품":{},"욕실용품":{},"세탁용품":{},"공구":{},"청소용품":{},"정원/원예용품":{},"수집품":{},"관상어용품":{},"반려동물":{}},"디지털/가전":{"태블릿PC":{},"노트북":{}}},"totalCount":340`,
+ `"products":[{"title":"PVC 투명 젤리 쇼핑백 1호 와인 답례품 구디백 비닐 손잡이 미니 간식 선물포장","link":"https://smartstore.naver.com/main/products/7522712674","image":"https://shopping-phinf.pstatic.net/main_8506721/85067212996.1.jpg","lprice":"220","hprice":"","mallName":"기브N기브","productId":"85067212996","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"샷시 풍지판 창문 바람막이 베란다 문 틈막이 창틀 벌레 차단 샤시 방충망 틈새막이","link":"https://smartstore.naver.com/main/products/9396357056","image":"https://shopping-phinf.pstatic.net/main_8694085/86940857379.1.jpg","lprice":"230","hprice":"","mallName":"EASYWAY","productId":"86940857379","productType":"2","brand":"이지웨이건축자재","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"실리카겔 50g 습기제거제 제품 /산업 신발 의류 방습제","link":"https://smartstore.naver.com/main/products/4549948287","image":"https://shopping-phinf.pstatic.net/main_8209446/82094468339.4.jpg","lprice":"280","hprice":"","mallName":"제이제이상사","productId":"82094468339","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"제습/방향/탈취","category4":"제습제"},{"title":"두꺼운 고급 무지쇼핑백 종이쇼핑백 주문제작 소량 로고인쇄 선물용 종이가방 세로형1호","link":"https://smartstore.naver.com/main/products/8643964296","image":"https://shopping-phinf.pstatic.net/main_8618846/86188464619.14.jpg","lprice":"350","hprice":"","mallName":"세모쇼핑백","productId":"86188464619","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방충망 셀프교체 미세먼지 롤 창문 모기장 알루미늄망 60cmX20cm","link":"https://smartstore.naver.com/main/products/4814730329","image":"https://shopping-phinf.pstatic.net/main_8235925/82359253087.18.jpg","lprice":"420","hprice":"","mallName":"파머스홈","productId":"82359253087","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"방충망 미세먼지 롤 창문 모기장 DIY 100cmx10cm","link":"https://smartstore.naver.com/main/products/668979777","image":"https://shopping-phinf.pstatic.net/main_1112415/11124150101.10.jpg","lprice":"450","hprice":"","mallName":"동백물산","productId":"11124150101","productType":"2","brand":"메쉬코리아","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"현관문고무패킹 문틈막이 방화문가스켓 현관 우풍 소음 벌레 외풍차단 틈새막이 방음재 일반형","link":"https://smartstore.naver.com/main/products/4976480580","image":"https://shopping-phinf.pstatic.net/main_8252100/82521000904.2.jpg","lprice":"1390","hprice":"","mallName":"나라종합","productId":"82521000904","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"풍지판 창문 틈새막이 샷시 바람막이 창틀 벌레차단 외풍차단","link":"https://smartstore.naver.com/main/products/261719599","image":"https://shopping-phinf.pstatic.net/main_8131970/8131970722.30.jpg","lprice":"1690","hprice":"","mallName":"리빙포유","productId":"8131970722","productType":"2","brand":"리빙포유","maker":"세일인터내셔널","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"태양 홈키파 엘비이 알파 수성 에어졸 500ml, 1개","link":"https://search.shopping.naver.com/catalog/52481568603","image":"https://shopping-phinf.pstatic.net/main_5248156/52481568603.20250114124554.jpg","lprice":"1820","hprice":"","mallName":"네이버","productId":"52481568603","productType":"1","brand":"홈키파","maker":"태양","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"에어졸/스프레이"},{"title":"탈부착 방충망 자석쫄대 방풍비닐 창문방충망 셀프시공 DIY 백색 100cm","link":"https://smartstore.naver.com/main/products/2042376373","image":"https://shopping-phinf.pstatic.net/main_1179488/11794889307.3.jpg","lprice":"2190","hprice":"","mallName":"한반도철망","productId":"11794889307","productType":"2","brand":"한반도철망","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"모기장"},{"title":"창틀벌레 모풍지판 창문 벌레 차단 틈새 창문틈 막이 방충망","link":"https://smartstore.naver.com/main/products/6293889960","image":"https://shopping-phinf.pstatic.net/main_8383839/83838392449.1.jpg","lprice":"2300","hprice":"","mallName":"우예스토어","productId":"83838392449","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"나노 아트2 전기 매립 콘센트 커버 2구","link":"https://smartstore.naver.com/main/products/7170895087","image":"https://shopping-phinf.pstatic.net/main_8471539/84715395409.1.jpg","lprice":"2500","hprice":"","mallName":"터치전기","productId":"84715395409","productType":"2","brand":"나노","maker":"나노","category1":"생활/건강","category2":"공구","category3":"전기용품","category4":"기타 전기용품"},{"title":"날파리 퇴치 초파리 트랩 뿌리파리 벌레 파리 벼룩파리 끈끈이 플라이스틱","link":"https://smartstore.naver.com/main/products/6792117787","image":"https://shopping-phinf.pstatic.net/main_8433661/84336618109.2.jpg","lprice":"2700","hprice":"","mallName":"메디데이","productId":"84336618109","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"해충퇴치용품","category4":"끈끈이"},{"title":"나이키 리유저블 쇼핑백 소형 타포린백 쇼퍼백 에코백 장바구니 운동 헬스 가방 방수","link":"https://smartstore.naver.com/main/products/6642533357","image":"https://shopping-phinf.pstatic.net/main_8418703/84187033679.6.jpg","lprice":"2890","hprice":"","mallName":"소울 컴퍼니sc","productId":"84187033679","productType":"2","brand":"나이키","maker":"나이키","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"쇼핑백"},{"title":"방문방음 문틈 창문 방문 틈새막이 소음차단 문틈막이 방음재 고무 문풍지 현관문 패킹 I형","link":"https://smartstore.naver.com/main/products/6106851858","image":"https://shopping-phinf.pstatic.net/main_8365135/83651351346.10.jpg","lprice":"2900","hprice":"","mallName":"주알보","productId":"83651351346","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"문풍지"},{"title":"에어컨 세정제 세척제 청소 셀프 클리너 곰팡이 냄새 제거제 스프레이 330ml","link":"https://smartstore.naver.com/main/products/4426750526","image":"https://shopping-phinf.pstatic.net/main_8197127/81971273079.7.jpg","lprice":"3000","hprice":"","mallName":"-에띠리얼-","productId":"81971273079","productType":"2","brand":"산도깨비","maker":"산도깨비","category1":"생활/건강","category2":"생활용품","category3":"세제/세정제","category4":"에어컨세정제"},{"title":"포장용 롤 에어캡 뽁뽁이 0.2T 경포장용 20cm x 50M 1롤","link":"https://smartstore.naver.com/main/products/5182465882","image":"https://shopping-phinf.pstatic.net/main_8272698/82726987088.5.jpg","lprice":"3500","hprice":"","mallName":"황금상사스토어","productId":"82726987088","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"생활용품","category3":"생활잡화","category4":"단열시트"},{"title":"하수구트랩 배수구 냄새제거 차단 화장실 욕실 40-99mm","link":"https://smartstore.naver.com/main/products/5008920074","image":"https://shopping-phinf.pstatic.net/main_8255344/82553440741.14.jpg","lprice":"4000","hprice":"","mallName":"낭만 탐구소","productId":"82553440741","productType":"2","brand":"낭만탐구소","maker":"","category1":"생활/건강","category2":"욕실용품","category3":"샤워기/수전용품","category4":"배수구캡"},{"title":"땡큐 순수 천연펄프 3겹 14m, 30롤, 1팩","link":"https://search.shopping.naver.com/catalog/54647347924","image":"https://shopping-phinf.pstatic.net/main_5464734/54647347924.20250508140616.jpg","lprice":"4990","hprice":"","mallName":"네이버","productId":"54647347924","productType":"1","brand":"땡큐","maker":"","category1":"생활/건강","category2":"생활용품","category3":"화장지","category4":"롤화장지"},{"title":"고양이 난간 안전망 복층 베란다 방묘창 방묘문 방충망 캣도어 일반형검정1mx1m","link":"https://smartstore.naver.com/main/products/6187449408","image":"https://shopping-phinf.pstatic.net/main_8373194/83731948985.5.jpg","lprice":"5000","hprice":"","mallName":"나이스메쉬","productId":"83731948985","productType":"2","brand":"","maker":"","category1":"생활/건강","category2":"반려동물","category3":"리빙용품","category4":"안전문"}]`,
+ );
+ expect(html).toContain(
+ `"categories":{"생활/건강":{"생활용품":{},"주방용품":{},"문구/사무용품":{},"자동차용품":{},"구강위생용품":{},"수납/정리용품":{},"욕실용품":{},"세탁용품":{},"공구":{},"청소용품":{},"정원/원예용품":{},"수집품":{},"관상어용품":{},"반려동물":{}},"디지털/가전":{"태블릿PC":{},"노트북":{}}}`,
);
+ expect(html).toContain(`"totalCount":340`);
});
});
diff --git a/packages/vanilla/package.json b/packages/vanilla/package.json
index ab5ae3fd..666e687e 100644
--- a/packages/vanilla/package.json
+++ b/packages/vanilla/package.json
@@ -38,6 +38,7 @@
"@testing-library/user-event": "^14.6.1",
"@vitest/coverage-v8": "latest",
"@vitest/ui": "^2.1.8",
+ "concurrently": "latest",
"eslint": "^9.16.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
@@ -49,8 +50,7 @@
"msw": "^2.10.2",
"prettier": "^3.4.2",
"vite": "npm:rolldown-vite@latest",
- "vitest": "latest",
- "concurrently": "latest"
+ "vitest": "latest"
},
"msw": {
"workerDirectory": [
@@ -58,6 +58,7 @@
]
},
"dependencies": {
+ "@mswjs/http-middleware": "^0.10.3",
"compression": "^1.8.1",
"express": "^5.1.0",
"sirv": "^3.0.1"
diff --git a/packages/vanilla/server.js b/packages/vanilla/server.js
index 67f03afa..7cf15923 100644
--- a/packages/vanilla/server.js
+++ b/packages/vanilla/server.js
@@ -1,34 +1,172 @@
import express from "express";
+import fs from "fs/promises";
+import routes from "./src/routes.js";
+import { createMiddleware } from "@mswjs/http-middleware";
+import { handlers } from "./src/mocks/handlers.js";
+import { createServer as createViteServer } from "vite";
+import { render } from "./src/main-server.js";
+import { runWithContext } from "./src/lib/asyncContext.js";
+
+const app = express();
const prod = process.env.NODE_ENV === "production";
-const port = process.env.PORT || 5173;
+const port = Number(process.env.PORT) || 5173;
const base = process.env.BASE || (prod ? "/front_7th_chapter4-1/vanilla/" : "/");
-const app = express();
+let vite;
+let htmlTemplate = "";
+
+if (!prod) {
+ // 개발 환경: Vite 미들웨어 사용
+ vite = await createViteServer({
+ server: { middlewareMode: true },
+ appType: "custom",
+ base,
+ });
+ app.use(vite.middlewares);
+} else {
+ // 프로덕션 환경: 빌드된 정적 파일 서빙
+ const distPath = "./dist/vanilla";
+ app.use(base, express.static(distPath));
+
+ // 빌드된 index.html을 템플릿으로 로드
+ htmlTemplate = await fs.readFile(`${distPath}/index.html`, "utf-8");
+}
-const render = () => {
- return `안녕하세요
`;
-};
+app.use(createMiddleware(...handlers));
+app.use(express.static("public"));
-app.get("*all", (req, res) => {
- res.send(
- `
+const styles = fs.readFile("./src/styles.css", "utf-8");
+
+// HTML 생성 헬퍼 함수
+async function generateHtml({ html, title, metaTags = "", initialData }) {
+ if (prod) {
+ // 프로덕션: 빌드된 템플릿 사용
+ let result = htmlTemplate
+ .replace("Document", `${title}`)
+ .replace("", metaTags)
+ .replace("", html);
+
+ // 직전에 initialData 주입
+ result = result.replace(
+ "
-${render()}
+
+ ${html}
+
+
-
- `.trim(),
- );
+",
+ `
+`,
+ );
+
+ return result;
+ } else {
+ // 개발: 간단한 템플릿 (Vite가 /src/main.js 처리)
+ return `
-
+