Skip to content

Commit 6b65049

Browse files
committed
fix: SSR 모듈 로딩 에러 해결
- RouterSSR export 누락 문제 해결 - productApi import 경로 수정
1 parent 8c506a7 commit 6b65049

File tree

12 files changed

+250
-33
lines changed

12 files changed

+250
-33
lines changed

packages/vanilla/server.js

Lines changed: 61 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,76 @@
11
import express from "express";
2+
import { getCategories, getProducts } from "./src/api/productApi.js";
23

34
const prod = process.env.NODE_ENV === "production";
45
const port = process.env.PORT || 5173;
56
const base = process.env.BASE || (prod ? "/front_6th_chapter4-1/vanilla/" : "/");
67

78
const app = express();
89

9-
const render = () => {
10-
return `<div>안녕하세요</div>`;
10+
let vite;
11+
if (!prod) {
12+
const { createServer } = await import("vite");
13+
vite = await createServer({
14+
server: { middlewareMode: true },
15+
appType: "custom",
16+
base,
17+
});
18+
app.use(vite.middlewares);
19+
} else {
20+
const compression = (await import("compression")).default;
21+
const sirv = (await import("sirv")).default;
22+
app.use(compression());
23+
app.use(base, sirv("./dist/client", { extensions: [] }));
24+
}
25+
26+
const { HomePage } = await vite.ssrLoadModule("./src/pages/HomePage.js");
27+
const { server } = await vite.ssrLoadModule("./src/mocks/server-browser.js");
28+
server.listen();
29+
// 뿌려줄 아이
30+
const render = async (url, query) => {
31+
console.log({ url, query });
32+
const [
33+
{
34+
products,
35+
pagination: { total },
36+
},
37+
categories,
38+
] = await Promise.all([getProducts(query), getCategories()]);
39+
return `${HomePage(url, query, { products, categories, totalCount: total, loading: false, status: "done" })}`;
1140
};
1241

13-
app.get("*all", (req, res) => {
42+
// 호출부
43+
// get 이라는 메소드로 호출된 애는 아래로 시작하겠다.
44+
// req: 요청 객체 (url, query)
45+
// res: 응답 객체
46+
app.get("*all", async (req, res) => {
1447
res.send(
1548
`
16-
<!DOCTYPE html>
17-
<html lang="en">
18-
<head>
19-
<meta charset="UTF-8" />
20-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
21-
<title>Vanilla Javascript SSR</title>
22-
</head>
23-
<body>
24-
<div id="app">${render()}</div>
25-
</body>
49+
<!doctype html>
50+
<html lang="ko">
51+
<head>
52+
<meta charset="UTF-8" />
53+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
54+
<script src="https://cdn.tailwindcss.com"></script>
55+
<!--app-head-->
56+
<link rel="stylesheet" href="/src/styles.css">
57+
<script>
58+
tailwind.config = {
59+
theme: {
60+
extend: {
61+
colors: {
62+
primary: '#3b82f6',
63+
secondary: '#6b7280'
64+
}
65+
}
66+
}
67+
}
68+
</script>
69+
</head>
70+
<body class="bg-gray-50">
71+
<div id="app">${await render(req.url, req.query)}</div>
72+
<script type="module" src="/src/main.js"></script>
73+
</body>
2674
</html>
2775
`.trim(),
2876
);

packages/vanilla/src/api/productApi.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ export async function getProducts(params = {}) {
1111
sort,
1212
});
1313

14-
const response = await fetch(`/api/products?${searchParams}`);
14+
const response = await fetch(`http://localhost/api/products?${searchParams}`);
1515

1616
return await response.json();
1717
}
1818

1919
export async function getProduct(productId) {
20-
const response = await fetch(`/api/products/${productId}`);
20+
const response = await fetch(`http://localhost/api/products/${productId}`);
2121
return await response.json();
2222
}
2323

2424
export async function getCategories() {
25-
const response = await fetch("/api/categories");
25+
const response = await fetch("http://localhost/api/categories");
2626
return await response.json();
2727
}

packages/vanilla/src/lib/createStorage.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,11 @@ export const createStorage = (key, storage = window.localStorage) => {
3333

3434
return { get, set, reset };
3535
};
36+
37+
export const createSSRStorage = (key) => {
38+
return {
39+
get: () => null,
40+
set: () => {},
41+
reset: () => {},
42+
};
43+
};

packages/vanilla/src/lib/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./createObserver";
2-
export * from "./createStore";
32
export * from "./createStorage";
3+
export * from "./createStore";
44
export * from "./Router";
5+
export * from "./server-Router";
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* 간단한 SPA 라우터
3+
*/
4+
import { createObserver } from "./createObserver.js";
5+
6+
export class RouterSSR {
7+
#routes;
8+
#route;
9+
#observer = createObserver();
10+
#baseUrl;
11+
12+
constructor(baseUrl = "") {
13+
this.#routes = new Map();
14+
this.#route = null;
15+
this.#baseUrl = baseUrl.replace(/\/$/, "");
16+
17+
// window.addEventListener("popstate", () => {
18+
// this.#route = this.#findRoute();
19+
// this.#observer.notify();
20+
// });
21+
}
22+
23+
get baseUrl() {
24+
return this.#baseUrl;
25+
}
26+
27+
get query() {
28+
// return RouterSSR.parseQuery();
29+
return {};
30+
}
31+
32+
set query(newQuery) {
33+
const newUrl = RouterSSR.getUrl(newQuery, this.#baseUrl);
34+
this.push(newUrl);
35+
}
36+
37+
get params() {
38+
return this.#route?.params ?? {};
39+
}
40+
41+
get route() {
42+
return this.#route;
43+
}
44+
45+
get target() {
46+
return this.#route?.handler;
47+
}
48+
49+
subscribe(fn) {
50+
this.#observer.subscribe(fn);
51+
}
52+
53+
/**
54+
* 라우트 등록
55+
* @param {string} path - 경로 패턴 (예: "/product/:id")
56+
* @param {Function} handler - 라우트 핸들러
57+
*/
58+
addRoute(path, handler) {
59+
// 경로 패턴을 정규식으로 변환
60+
const paramNames = [];
61+
const regexPath = path
62+
.replace(/:\w+/g, (match) => {
63+
paramNames.push(match.slice(1)); // ':id' -> 'id'
64+
return "([^/]+)";
65+
})
66+
.replace(/\//g, "\\/");
67+
68+
const regex = new RegExp(`^${this.#baseUrl}${regexPath}$`);
69+
70+
this.#routes.set(path, {
71+
regex,
72+
paramNames,
73+
handler,
74+
});
75+
}
76+
77+
#findRoute(url = window.location.pathname) {
78+
const { pathname } = new URL(url, window.location.origin);
79+
for (const [routePath, route] of this.#routes) {
80+
const match = pathname.match(route.regex);
81+
if (match) {
82+
// 매치된 파라미터들을 객체로 변환
83+
const params = {};
84+
route.paramNames.forEach((name, index) => {
85+
params[name] = match[index + 1];
86+
});
87+
88+
return {
89+
...route,
90+
params,
91+
path: routePath,
92+
};
93+
}
94+
}
95+
return null;
96+
}
97+
98+
/**
99+
* 네비게이션 실행
100+
* @param {string} url - 이동할 경로
101+
*/
102+
push() {
103+
//
104+
}
105+
106+
/**
107+
* 라우터 시작
108+
*/
109+
start() {
110+
this.#route = this.#findRoute();
111+
this.#observer.notify();
112+
}
113+
114+
/**
115+
* 쿼리 파라미터를 객체로 파싱
116+
* @param {string} search - location.search 또는 쿼리 문자열
117+
* @returns {Object} 파싱된 쿼리 객체
118+
*/
119+
static parseQuery = (search = "") => {
120+
const params = new URLSearchParams(search);
121+
const query = {};
122+
for (const [key, value] of params) {
123+
query[key] = value;
124+
}
125+
return query;
126+
};
127+
128+
/**
129+
* 객체를 쿼리 문자열로 변환
130+
* @param {Object} query - 쿼리 객체
131+
* @returns {string} 쿼리 문자열
132+
*/
133+
static stringifyQuery = (query) => {
134+
const params = new URLSearchParams();
135+
for (const [key, value] of Object.entries(query)) {
136+
if (value !== null && value !== undefined && value !== "") {
137+
params.set(key, String(value));
138+
}
139+
}
140+
return params.toString();
141+
};
142+
143+
static getUrl = (newQuery, baseUrl = "") => {
144+
//
145+
};
146+
}

packages/vanilla/src/main.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { registerGlobalEvents } from "./utils";
2-
import { initRender } from "./render";
1+
import { BASE_URL } from "./constants.js";
32
import { registerAllEvents } from "./events";
4-
import { loadCartFromStorage } from "./services";
3+
import { initRender } from "./render";
54
import { router } from "./router";
6-
import { BASE_URL } from "./constants.js";
5+
import { loadCartFromStorage } from "./services";
6+
import { registerGlobalEvents } from "./utils";
77

88
const enableMocking = () =>
99
import("./mocks/browser.js").then(({ worker }) =>

packages/vanilla/src/mocks/handlers.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ function filterProducts(products, query) {
6464

6565
export const handlers = [
6666
// 상품 목록 API
67-
http.get("/api/products", async ({ request }) => {
67+
http.get("*/api/products", async ({ request }) => {
6868
const url = new URL(request.url);
6969
const page = parseInt(url.searchParams.get("page") ?? url.searchParams.get("current")) || 1;
7070
const limit = parseInt(url.searchParams.get("limit")) || 20;
@@ -111,7 +111,7 @@ export const handlers = [
111111
}),
112112

113113
// 상품 상세 API
114-
http.get("/api/products/:id", ({ params }) => {
114+
http.get("*/api/products/:id", ({ params }) => {
115115
const { id } = params;
116116
const product = items.find((item) => item.productId === id);
117117

@@ -133,7 +133,7 @@ export const handlers = [
133133
}),
134134

135135
// 카테고리 목록 API
136-
http.get("/api/categories", async () => {
136+
http.get("*/api/categories", async () => {
137137
const categories = getUniqueCategories();
138138
await delay();
139139
return HttpResponse.json(categories);
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { setupServer } from "msw/node";
2+
import { handlers } from "./handlers";
3+
4+
const server = setupServer(...handlers);
5+
6+
export { server };

packages/vanilla/src/pages/HomePage.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ProductList, SearchBar } from "../components";
2-
import { productStore } from "../stores";
32
import { router, withLifecycle } from "../router";
43
import { loadProducts, loadProductsAndCategories } from "../services";
4+
import { productStore } from "../stores";
55
import { PageWrapper } from "./PageWrapper.js";
66

77
export const HomePage = withLifecycle(
@@ -17,9 +17,15 @@ export const HomePage = withLifecycle(
1717
() => loadProducts(true),
1818
],
1919
},
20-
() => {
21-
const productState = productStore.getState();
22-
const { search: searchQuery, limit, sort, category1, category2 } = router.query;
20+
(url, query, param) => {
21+
const productState = typeof window !== "undefined" ? productStore.getState() : param;
22+
const {
23+
search: searchQuery,
24+
limit,
25+
sort,
26+
category1,
27+
category2,
28+
} = typeof window !== "undefined" ? router.query : query;
2329
const { products, loading, error, totalCount, categories } = productState;
2430
const category = { category1, category2 };
2531
const hasMore = products.length < totalCount;
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// 글로벌 라우터 인스턴스
2-
import { Router } from "../lib";
32
import { BASE_URL } from "../constants.js";
3+
import { Router, RouterSSR } from "../lib";
44

5-
export const router = new Router(BASE_URL);
5+
export const router = typeof window !== "undefined" ? new Router(BASE_URL) : new RouterSSR();

0 commit comments

Comments
 (0)