Skip to content

Commit 83ad8e4

Browse files
committed
fix: router issue in csr
1 parent 280d107 commit 83ad8e4

File tree

5 files changed

+102
-92
lines changed

5 files changed

+102
-92
lines changed

packages/lib/src/Router.ts

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -104,52 +104,49 @@ export class Router<Handler extends (...args: any[]) => any> {
104104
}
105105

106106
#findRoute(url?: string) {
107-
if (this.#isServer) {
108-
// SSR에서는 기본 라우트 반환
109-
const defaultRoute = this.#routes.get("/");
110-
if (defaultRoute) {
111-
return {
112-
...defaultRoute,
113-
params: {},
114-
path: "/",
115-
};
116-
}
117-
return null;
118-
}
107+
const actualUrl = url || (this.#isServer ? "/" : window.location.pathname);
108+
const { pathname } = new URL(actualUrl, "http://localhost"); // 서버/클라 모두 안전
109+
const normalizedPath = pathname.replace(/\/+$/, "") || "/"; // ✅ 마지막 '/' 제거
110+
111+
const routeEntries = Array.from(this.#routes.entries()).sort(([pathA], [pathB]) => {
112+
if (pathA === "*" || pathA === "/*") return 1;
113+
if (pathB === "*" || pathB === "/*") return -1;
114+
return 0;
115+
});
119116

120-
const actualUrl = url || window.location.pathname;
121-
const { pathname } = new URL(actualUrl, window.location.origin);
122-
for (const [routePath, route] of this.#routes) {
123-
const match = pathname.match(route.regex);
117+
for (const [routePath, route] of routeEntries) {
118+
const match = normalizedPath.match(route.regex); // ✅ 정규화된 경로 사용
124119
if (match) {
125-
// 매치된 파라미터들을 객체로 변환
126120
const params: StringRecord = {};
127121
route.paramNames.forEach((name, index) => {
128122
params[name] = match[index + 1];
129123
});
130-
131-
return {
132-
...route,
133-
params,
134-
path: routePath,
135-
};
124+
return { ...route, params, path: routePath };
136125
}
137126
}
127+
138128
return null;
139129
}
140130

141131
push(url: string) {
142-
if (this.#isServer) {
143-
return;
144-
}
132+
if (this.#isServer) return;
145133

146134
try {
147-
// baseUrl이 없으면 자동으로 붙여줌
148-
const fullUrl = url.startsWith(this.#baseUrl) ? url : this.#baseUrl + (url.startsWith("/") ? url : "/" + url);
135+
let cleanUrl = url;
149136

150-
const prevFullUrl = `${window.location.pathname}${window.location.search}`;
137+
// ✅ 쿼리가 없는 경우에만 trailing slash 강제
138+
if (!cleanUrl.includes("?")) {
139+
cleanUrl = cleanUrl.replace(/\/+$/, "");
140+
if (!cleanUrl.endsWith("/")) {
141+
cleanUrl += "/";
142+
}
143+
}
144+
145+
const fullUrl = cleanUrl.startsWith(this.#baseUrl)
146+
? cleanUrl
147+
: this.#baseUrl + (cleanUrl.startsWith("/") ? cleanUrl : "/" + cleanUrl);
151148

152-
// 히스토리 업데이트
149+
const prevFullUrl = `${window.location.pathname}${window.location.search}`;
153150
if (prevFullUrl !== fullUrl) {
154151
window.history.pushState(null, "", fullUrl);
155152
}
@@ -162,7 +159,9 @@ export class Router<Handler extends (...args: any[]) => any> {
162159
}
163160

164161
start() {
162+
console.log("Router starting...");
165163
this.#route = this.#findRoute();
164+
console.log("Initial route:", this.#route);
166165
this.#observer.notify();
167166
}
168167

packages/lib/src/hooks/useRouter.ts

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,39 @@ export const useRouter = <T extends RouterInstance<AnyFunction>, S = T>(
1212
) => {
1313
const shallowSelector = useShallowSelector(selector);
1414

15-
// 라우터 인스턴스 자체를 읽는다 (getState 기대 X)
16-
const getSnapshot = () => shallowSelector(router);
15+
// 라우터 인스턴스 자체를 읽는다
16+
const getSnapshot = () => {
17+
const result = shallowSelector(router);
18+
console.log("getSnapshot called:", {
19+
target: router.target?.name,
20+
route: router.route?.path,
21+
params: router.params,
22+
});
23+
return result;
24+
};
1725

18-
// subscribe는 필수. 없으면 no-op
26+
// subscribe 함수
27+
const subscribe = (callback: () => void) => {
28+
console.log("Subscribing to router changes");
1929

20-
const subscribe = typeof (router as any).subscribe === "function" ? (router as any).subscribe : () => () => {};
30+
// 콜백을 래핑해서 언제 호출되는지 확인
31+
const wrappedCallback = () => {
32+
console.log("Router state changed! Triggering rerender...");
33+
callback();
34+
};
2135

22-
// ✅ SSR 호환: 3번째 인자(getServerSnapshot) 추가
23-
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
36+
if (router.subscribe && typeof router.subscribe === "function") {
37+
console.log("Router has subscribe method, using it");
38+
const unsubscribe = router.subscribe(wrappedCallback);
39+
console.log("Subscribe returned:", typeof unsubscribe);
40+
return unsubscribe;
41+
}
42+
43+
console.error("Router subscribe method not found:", router);
44+
return () => {};
45+
};
46+
47+
const result = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
48+
49+
return result;
2450
};

packages/react/server.js

Lines changed: 13 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ const app = express();
1010
// ✅ 모든 요청 로깅 미들웨어 (가장 먼저)
1111
app.use((req, res, next) => {
1212
console.log(`🌍 [${new Date().toISOString()}] ${req.method} ${req.originalUrl}`);
13-
console.log(`🔍 User-Agent: ${req.headers["user-agent"]}`);
1413
next();
1514
});
1615

@@ -57,12 +56,11 @@ app.get("*all", async (req, res) => {
5756
}
5857

5958
try {
60-
console.log("🎯 === SSR 요청 디버그 ===");
61-
console.log("🎯 URL:", req.originalUrl);
62-
console.log("🎯 Path:", req.path);
63-
console.log("🎯 Query:", req.query);
64-
console.log("🎯 Method:", req.method);
65-
console.log("🎯 //=== SSR 요청 디버그 ===");
59+
console.log("🎯 SSR 요청:", {
60+
originalUrl: req.originalUrl,
61+
path: req.path,
62+
query: req.query,
63+
});
6664

6765
const url = req.originalUrl.replace(base, "");
6866
const query = req.query;
@@ -72,71 +70,40 @@ app.get("*all", async (req, res) => {
7270
try {
7371
if (!prod) {
7472
// 개발 환경
75-
console.log("📖 개발 모드: index.html 읽기...");
7673
template = fs.readFileSync("./index.html", "utf-8");
7774
template = await vite.transformIndexHtml(url, template);
78-
79-
console.log("📦 개발 모드: main-server.tsx 로드...");
80-
try {
81-
render = (await vite.ssrLoadModule("./src/main-server.tsx")).render;
82-
} catch (ssrError) {
83-
console.error("❌ SSR 모듈 로드 실패:", ssrError);
84-
// SSR 실패 시 클라이언트 사이드 렌더링으로 폴백
85-
render = () => ({
86-
html: '<div id="root"><!-- SSR 실패, 클라이언트에서 렌더링됩니다 --></div>',
87-
head: "<title>쇼핑몰</title>",
88-
initialData: {},
89-
});
90-
}
75+
render = (await vite.ssrLoadModule("./src/main-server.tsx")).render;
9176
} else {
9277
// 프로덕션 환경
93-
console.log("📖 프로덕션 모드: 템플릿 사용...");
9478
template = templateHtml;
95-
try {
96-
render = (await import("./dist/react-ssr/main-server.js")).render;
97-
} catch (ssrError) {
98-
console.error("❌ 프로덕션 SSR 모듈 로드 실패:", ssrError);
99-
render = () => ({
100-
html: '<div id="root"><!-- SSR 실패, 클라이언트에서 렌더링됩니다 --></div>',
101-
head: "<title>쇼핑몰</title>",
102-
initialData: {},
103-
});
104-
}
79+
render = (await import("./dist/react-ssr/main-server.js")).render;
10580
}
10681

10782
console.log("✅ 템플릿 및 render 함수 로드 성공");
10883

10984
// render 함수 호출
110-
console.log("🔄 SSR 렌더링 시작...");
111-
console.log(url);
85+
console.log("🔄 SSR 렌더링 시작...", { url });
11286

11387
const { html, head, initialData } = await render(url, query);
11488

115-
console.log("✅ SSR 렌더링 완료");
116-
console.log("📄 HTML 길이:", html?.length || 0);
117-
console.log("🏷️ Head:", head?.substring(0, 100) + "...");
118-
console.log("💾 Initial Data keys:", Object.keys(initialData || {}));
119-
120-
console.log(initialData);
89+
console.log("✅ SSR 렌더링 완료", {
90+
htmlLength: html?.length || 0,
91+
hasCurrentProduct: !!initialData?.currentProduct,
92+
productTitle: initialData?.currentProduct?.title,
93+
});
12194

12295
// 초기 데이터 스크립트 생성
12396
const initialDataScript =
12497
Object.keys(initialData || {}).length > 0
12598
? `<script>window.__INITIAL_DATA__ = ${JSON.stringify(initialData).replace(/</g, "\\u003c")};</script>`
12699
: "";
127100

128-
console.log("server html");
129-
130-
console.log(html);
131-
132101
// 템플릿 교체
133102
const finalHtml = template
134103
.replace("<!--app-html-->", html)
135104
.replace("<!--app-head-->", head)
136105
.replace("</head>", `${initialDataScript}</head>`);
137106

138-
console.log(finalHtml);
139-
140107
console.log("🎉 최종 HTML 생성 완료, 길이:", finalHtml.length);
141108

142109
res.setHeader("Content-Type", "text/html");

packages/react/src/App.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,12 @@ router.addRoute("/product/:id", ProductDetailPage);
99
router.addRoute("*", NotFoundPage);
1010

1111
const CartInitializer = () => {
12-
if (typeof window !== "undefined") {
13-
useLoadCartStore();
14-
}
12+
useLoadCartStore();
1513
return null;
1614
};
1715

1816
/**
19-
* 클라이언트 사이드 애플리케이션
17+
* 클라이언트 사이드 애플리케이션 (원래 방식)
2018
*/
2119
const ClientApp = () => {
2220
const PageComponent = useCurrentPage();

packages/react/src/entities/carts/hooks/useLoadCartStore.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,37 @@ export const useLoadCartStore = () => {
88
const data = useStore(cartStore);
99

1010
useEffect(() => {
11-
cartStore.dispatch({
12-
type: CART_ACTIONS.LOAD_FROM_STORAGE,
13-
payload: cartStorage.get(),
14-
});
11+
// 서버사이드에서는 스토리지 접근하지 않음
12+
if (typeof window === "undefined") {
13+
setInit(true);
14+
return;
15+
}
16+
17+
try {
18+
const savedData = cartStorage.get();
19+
if (savedData) {
20+
cartStore.dispatch({
21+
type: CART_ACTIONS.LOAD_FROM_STORAGE,
22+
payload: savedData,
23+
});
24+
}
25+
} catch (error) {
26+
console.warn("장바구니 데이터 로드 실패:", error);
27+
}
28+
1529
setInit(true);
1630
}, []);
1731

1832
useEffect(() => {
19-
if (!init) {
33+
// 서버사이드에서는 스토리지에 저장하지 않음
34+
if (!init || typeof window === "undefined") {
2035
return;
2136
}
22-
cartStorage.set(data);
37+
38+
try {
39+
cartStorage.set(data);
40+
} catch (error) {
41+
console.warn("장바구니 데이터 저장 실패:", error);
42+
}
2343
}, [init, data]);
2444
};

0 commit comments

Comments
 (0)