Skip to content

Commit 5061b88

Browse files
author
YeongseoYoon
committed
fix: 서버별 독립적인 전역상태를 위해 컨텍스트 구현
1 parent e643beb commit 5061b88

File tree

8 files changed

+97
-51
lines changed

8 files changed

+97
-51
lines changed

packages/vanilla/src/lib/ServerRouter.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ export class ServerRouter {
66
#routes;
77
#route;
88
#notFoundHandler;
9+
#createContext;
910

10-
constructor(routes = null) {
11+
constructor(routes = null, createContext = null) {
1112
this.#routes = new Map();
1213
this.#route = null;
1314
this.#notFoundHandler = null;
15+
this.#createContext = createContext;
1416
if (routes) {
1517
this.addRoutes(routes);
1618
}
@@ -105,6 +107,13 @@ export class ServerRouter {
105107
*/
106108
start(pathname) {
107109
this.#route = this.#findRoute(pathname);
110+
// 요청 스코프 컨텍스트 생성 및 params에 주입
111+
if (this.#route && typeof this.#createContext === "function") {
112+
const ctx = this.#createContext();
113+
// router는 자신으로 설정
114+
ctx.router = this;
115+
this.#route.params = { ...(this.#route.params || {}), ctx };
116+
}
108117
}
109118

110119
/** 현재 매칭된 라우트 반환 */

packages/vanilla/src/main-server.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { ServerRouter } from "./lib";
2-
import { routes } from "./router/router";
1+
import { createRequestContextBase } from "./ssr/context.js";
2+
import { ServerRouter } from "./lib/ServerRouter.js";
3+
import { routes } from "./router/router.js";
34

45
export const render = async (pathname, query) => {
5-
const router = new ServerRouter(routes);
6+
const router = new ServerRouter(routes, createRequestContextBase);
67
router.start(pathname);
78
const params = { pathname, query, params: router.params };
89

packages/vanilla/src/pages/HomePage.js

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ import { PageWrapper } from "./PageWrapper.js";
88
export const HomePage = withLifecycle(
99
{
1010
ssr: async (params) => {
11-
await loadProductsAndCategories(params?.query);
12-
const data = productStore.getState();
11+
const ctx = params?.params?.ctx;
12+
await loadProductsAndCategories(params?.query, ctx);
13+
const data = (ctx?.store || productStore).getState();
1314
return {
1415
products: data.products,
1516
categories: data.categories,
@@ -32,10 +33,16 @@ export const HomePage = withLifecycle(
3233
() => loadProducts(true),
3334
],
3435
},
35-
() => {
36-
const productState = productStore.getState();
37-
const { search: searchQuery, limit, sort, category1, category2 } = router.query || {};
38-
const { products, loading, error, totalCount, categories } = productState;
36+
(params, props) => {
37+
const source = props?.data || productStore.getState();
38+
const { products, loading, error, totalCount, categories } = source;
39+
const {
40+
search: searchQuery,
41+
limit,
42+
sort,
43+
category1,
44+
category2,
45+
} = props?.data ? params?.query || {} : router.query || {};
3946
const category = { category1, category2 };
4047
const hasMore = products.length < totalCount;
4148

packages/vanilla/src/pages/ProductDetailPage.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,12 +239,13 @@ function ProductDetail({ product, relatedProducts = [] }) {
239239
export const ProductDetailPage = withLifecycle(
240240
{
241241
ssr: async (params) => {
242+
const ctx = params?.params?.ctx;
242243
const productId = params.params?.id;
243244
if (!productId) {
244-
return productStore.getState();
245+
return (ctx?.store || productStore).getState();
245246
}
246-
await loadProductDetailForPage(productId);
247-
return productStore.getState();
247+
await loadProductDetailForPage(productId, ctx);
248+
return (ctx?.store || productStore).getState();
248249
},
249250
metadata: async (params) => {
250251
try {
@@ -262,8 +263,9 @@ export const ProductDetailPage = withLifecycle(
262263
},
263264
watches: [() => [router.params.id], () => loadProductDetailForPage(router.params.id)],
264265
},
265-
() => {
266-
const { currentProduct: product, relatedProducts = [], error, loading } = productStore.getState();
266+
(params, props) => {
267+
const state = props?.data || productStore.getState();
268+
const { currentProduct: product, relatedProducts = [], error, loading } = state;
267269

268270
return PageWrapper({
269271
headerLeft: `

packages/vanilla/src/services/productService.js

Lines changed: 51 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ import { getCategories, getProduct, getProducts } from "../api/productApi";
22
import { initialProductState, productStore, PRODUCT_ACTIONS } from "../stores";
33
import { router } from "../router";
44

5-
export const loadProductsAndCategories = async (queryParams = null) => {
6-
const query = queryParams || router.query || {};
7-
router.query = { ...query, current: undefined };
8-
productStore.dispatch({
5+
export const loadProductsAndCategories = async (queryParams = null, ctx = null) => {
6+
const usingCtx = ctx && ctx.store && ctx.router;
7+
const activeRouter = usingCtx ? ctx.router : router;
8+
const activeStore = usingCtx ? ctx.store : productStore;
9+
10+
const query = queryParams || activeRouter.query || {};
11+
activeRouter.query = { ...query, current: undefined };
12+
activeStore.dispatch({
913
type: PRODUCT_ACTIONS.SETUP,
1014
payload: {
1115
...initialProductState,
@@ -21,10 +25,10 @@ export const loadProductsAndCategories = async (queryParams = null) => {
2125
pagination: { total },
2226
},
2327
categories,
24-
] = await Promise.all([getProducts(router.query), getCategories()]);
28+
] = await Promise.all([getProducts(activeRouter.query), getCategories()]);
2529

2630
// 페이지 리셋이면 새로 설정, 아니면 기존에 추가
27-
productStore.dispatch({
31+
activeStore.dispatch({
2832
type: PRODUCT_ACTIONS.SETUP,
2933
payload: {
3034
products,
@@ -35,7 +39,7 @@ export const loadProductsAndCategories = async (queryParams = null) => {
3539
},
3640
});
3741
} catch (error) {
38-
productStore.dispatch({
42+
activeStore.dispatch({
3943
type: PRODUCT_ACTIONS.SET_ERROR,
4044
payload: error.message,
4145
});
@@ -46,27 +50,30 @@ export const loadProductsAndCategories = async (queryParams = null) => {
4650
/**
4751
* 상품 목록 로드 (새로고침)
4852
*/
49-
export const loadProducts = async (resetList = true) => {
53+
export const loadProducts = async (resetList = true, ctx = null) => {
54+
const usingCtx = ctx && ctx.store && ctx.router;
55+
const activeRouter = usingCtx ? ctx.router : router;
56+
const activeStore = usingCtx ? ctx.store : productStore;
5057
try {
51-
productStore.dispatch({
58+
activeStore.dispatch({
5259
type: PRODUCT_ACTIONS.SETUP,
5360
payload: { loading: true, status: "pending", error: null },
5461
});
5562

5663
const {
5764
products,
5865
pagination: { total },
59-
} = await getProducts(router.query);
66+
} = await getProducts(activeRouter.query);
6067
const payload = { products, totalCount: total };
6168

6269
// 페이지 리셋이면 새로 설정, 아니면 기존에 추가
6370
if (resetList) {
64-
productStore.dispatch({ type: PRODUCT_ACTIONS.SET_PRODUCTS, payload });
71+
activeStore.dispatch({ type: PRODUCT_ACTIONS.SET_PRODUCTS, payload });
6572
return;
6673
}
67-
productStore.dispatch({ type: PRODUCT_ACTIONS.ADD_PRODUCTS, payload });
74+
activeStore.dispatch({ type: PRODUCT_ACTIONS.ADD_PRODUCTS, payload });
6875
} catch (error) {
69-
productStore.dispatch({
76+
activeStore.dispatch({
7077
type: PRODUCT_ACTIONS.SET_ERROR,
7178
payload: error.message,
7279
});
@@ -77,60 +84,68 @@ export const loadProducts = async (resetList = true) => {
7784
/**
7885
* 다음 페이지 로드 (무한 스크롤)
7986
*/
80-
export const loadMoreProducts = async () => {
81-
const state = productStore.getState();
87+
export const loadMoreProducts = async (ctx = null) => {
88+
const usingCtx = ctx && ctx.store && ctx.router;
89+
const activeRouter = usingCtx ? ctx.router : router;
90+
const activeStore = usingCtx ? ctx.store : productStore;
91+
const state = activeStore.getState();
8292
const hasMore = state.products.length < state.totalCount;
8393

8494
if (!hasMore || state.loading) {
8595
return;
8696
}
8797

88-
router.query = { current: Number(router.query.current ?? 1) + 1 };
89-
await loadProducts(false);
98+
activeRouter.query = { current: Number(activeRouter.query.current ?? 1) + 1 };
99+
await loadProducts(false, ctx);
90100
};
91101
/**
92102
* 상품 검색
93103
*/
94-
export const searchProducts = (search) => {
95-
router.query = { search, current: 1 };
104+
export const searchProducts = (search, ctx = null) => {
105+
const activeRouter = ctx?.router || router;
106+
activeRouter.query = { search, current: 1 };
96107
};
97108

98109
/**
99110
* 카테고리 필터 설정
100111
*/
101-
export const setCategory = (categoryData) => {
102-
router.query = { ...categoryData, current: 1 };
112+
export const setCategory = (categoryData, ctx = null) => {
113+
const activeRouter = ctx?.router || router;
114+
activeRouter.query = { ...categoryData, current: 1 };
103115
};
104116

105117
/**
106118
* 정렬 옵션 변경
107119
*/
108-
export const setSort = (sort) => {
109-
router.query = { sort, current: 1 };
120+
export const setSort = (sort, ctx = null) => {
121+
const activeRouter = ctx?.router || router;
122+
activeRouter.query = { sort, current: 1 };
110123
};
111124

112125
/**
113126
* 페이지당 상품 수 변경
114127
*/
115-
export const setLimit = (limit) => {
116-
router.query = { limit, current: 1 };
128+
export const setLimit = (limit, ctx = null) => {
129+
const activeRouter = ctx?.router || router;
130+
activeRouter.query = { limit, current: 1 };
117131
};
118132

119133
/**
120134
* 상품 상세 페이지용 상품 조회 및 관련 상품 로드
121135
*/
122-
export const loadProductDetailForPage = async (productId) => {
136+
export const loadProductDetailForPage = async (productId, ctx = null) => {
137+
const activeStore = ctx?.store || productStore;
123138
try {
124-
const currentProduct = productStore.getState().currentProduct;
139+
const currentProduct = activeStore.getState().currentProduct;
125140
if (productId === currentProduct?.productId) {
126141
// 관련 상품 로드 (같은 category2 기준)
127142
if (currentProduct.category2) {
128-
await loadRelatedProducts(currentProduct.category2, productId);
143+
await loadRelatedProducts(currentProduct.category2, productId, ctx);
129144
}
130145
return;
131146
}
132147
// 현재 상품 클리어
133-
productStore.dispatch({
148+
activeStore.dispatch({
134149
type: PRODUCT_ACTIONS.SETUP,
135150
payload: {
136151
...initialProductState,
@@ -143,18 +158,18 @@ export const loadProductDetailForPage = async (productId) => {
143158
const product = await getProduct(productId);
144159

145160
// 현재 상품 설정
146-
productStore.dispatch({
161+
activeStore.dispatch({
147162
type: PRODUCT_ACTIONS.SET_CURRENT_PRODUCT,
148163
payload: product,
149164
});
150165

151166
// 관련 상품 로드 (같은 category2 기준)
152167
if (product.category2) {
153-
await loadRelatedProducts(product.category2, productId);
168+
await loadRelatedProducts(product.category2, productId, ctx);
154169
}
155170
} catch (error) {
156171
console.error("상품 상세 페이지 로드 실패:", error);
157-
productStore.dispatch({
172+
activeStore.dispatch({
158173
type: PRODUCT_ACTIONS.SET_ERROR,
159174
payload: error.message,
160175
});
@@ -165,7 +180,8 @@ export const loadProductDetailForPage = async (productId) => {
165180
/**
166181
* 관련 상품 로드 (같은 카테고리의 다른 상품들)
167182
*/
168-
export const loadRelatedProducts = async (category2, excludeProductId) => {
183+
export const loadRelatedProducts = async (category2, excludeProductId, ctx = null) => {
184+
const activeStore = ctx?.store || productStore;
169185
try {
170186
const params = {
171187
category2,
@@ -178,14 +194,14 @@ export const loadRelatedProducts = async (category2, excludeProductId) => {
178194
// 현재 상품 제외
179195
const relatedProducts = response.products.filter((product) => product.productId !== excludeProductId);
180196

181-
productStore.dispatch({
197+
activeStore.dispatch({
182198
type: PRODUCT_ACTIONS.SET_RELATED_PRODUCTS,
183199
payload: relatedProducts,
184200
});
185201
} catch (error) {
186202
console.error("관련 상품 로드 실패:", error);
187203
// 관련 상품 로드 실패는 전체 페이지에 영향주지 않도록 조용히 처리
188-
productStore.dispatch({
204+
activeStore.dispatch({
189205
type: PRODUCT_ACTIONS.SET_RELATED_PRODUCTS,
190206
payload: [],
191207
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { createProductStore } from "../stores/productStore.js";
2+
3+
export const createRequestContextBase = () => {
4+
const store = createProductStore();
5+
return { store, router: null, query: {} };
6+
};

packages/vanilla/src/stores/productStore.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,8 @@ const productReducer = (state, action) => {
104104
* 상품 스토어 생성
105105
*/
106106
export const productStore = createStore(productReducer, initialProductState);
107+
108+
/**
109+
* 요청 단위 상품 스토어 생성 팩토리
110+
*/
111+
export const createProductStore = () => createStore(productReducer, initialProductState);

packages/vanilla/static-site-generate.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from "node:fs";
22
import path from "node:path";
33
import { createServer } from "vite";
4-
import { mswServer } from "./src/mocks/node.js";
4+
import { server as mswServer } from "./src/mocks/nodeServer.js";
55

66
const DIST_DIR = "../../dist/vanilla";
77

0 commit comments

Comments
 (0)