Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 78 additions & 28 deletions packages/vanilla/server.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,84 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import express from "express";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

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

const app = express();

const render = () => {
return `<div>안녕하세요</div>`;
};

app.get("*all", (req, res) => {
res.send(
`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vanilla Javascript SSR</title>
</head>
<body>
<div id="app">${render()}</div>
</body>
</html>
`.trim(),
);
});

// Start http server
app.listen(port, () => {
console.log(`React Server started at http://localhost:${port}`);
});
async function createServer() {
const app = express();

let vite;
let templateHtml;
let ssrModule;

// 환경 분기
if (prod) {
// compression + sirv
const compression = (await import("compression")).default;
const sirv = (await import("sirv")).default;

app.use(compression());

const distPath = path.resolve(__dirname, "dist");
templateHtml = fs.readFileSync(path.resolve(distPath, "vanilla/index.html"), "utf-8");
ssrModule = await import(path.resolve(distPath, "vanilla-ssr/main-server.js"));

app.use(base, sirv(path.resolve(distPath, "vanilla"), { extensions: [] }));
} else {
// Vite dev server + middleware
const { createServer: createViteServer } = await import("vite");
vite = await createViteServer({
server: { middlewareMode: true },
appType: "custom",
base,
});
app.use(vite.middlewares);
}

// 렌더링 파이프라인
app.use("*all", async (req, res) => {
try {
const url = req.originalUrl.replace(base, "/");
const query = req.query;

let template;
let render;

if (prod) {
template = templateHtml;
render = ssrModule.render;
} else {
// 개발 환경: 매 요청마다 템플릿과 모듈 새로 로드
template = fs.readFileSync(path.resolve(__dirname, "index.html"), "utf-8");
template = await vite.transformIndexHtml(url, template);
const mod = await vite.ssrLoadModule("/src/main-server.js");
render = mod.render;
}

// SSR 렌더링
const { html: appHtml = "", head: appHead = "" } = (await render(url, query)) || {};

// Template 치환
const finalHtml = template.replace("<!--app-head-->", appHead).replace("<!--app-html-->", appHtml);

res.status(200).set({ "Content-Type": "text/html" }).send(finalHtml);
} catch (e) {
if (!prod && vite) {
vite.ssrFixStacktrace(e);
}
console.error(e.stack);
res.status(500).end(e.stack);
}
});

app.listen(port, () => {
console.log(`Vanilla Server started at http://localhost:${port}`);
});
}

createServer();
2 changes: 1 addition & 1 deletion packages/vanilla/src/components/CartModal.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CartItem } from "./CartItem";
import { CartItem } from "./CartItem.js";

export function CartModal({ items = [], selectedAll = false, isOpen = false }) {
if (!isOpen) {
Expand Down
2 changes: 1 addition & 1 deletion packages/vanilla/src/components/ProductList.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ProductCard, ProductCardSkeleton } from "./ProductCard";
import { ProductCard, ProductCardSkeleton } from "./ProductCard.js";

const loadingSkeleton = Array(6).fill(0).map(ProductCardSkeleton).join("");

Expand Down
16 changes: 8 additions & 8 deletions packages/vanilla/src/components/index.js
Original file line number Diff line number Diff line change
@@ -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";
109 changes: 109 additions & 0 deletions packages/vanilla/src/lib/ServerRouter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* 서버 사이드용 라우터
*/
import { createObserver } from "./createObserver.js";
import { Router } from "./Router.js";

export class ServerRouter {
#routes;
#route;
#observer = createObserver();
#baseUrl;
#query;

constructor(baseUrl = "") {
this.#routes = new Map();
this.#route = null;
this.#baseUrl = baseUrl.replace(/\/$/, "");
this.#query = {};
}

get baseUrl() {
return this.#baseUrl;
}

get query() {
return this.#query;
}

set query(newQuery) {
this.#query = { ...this.#query, ...newQuery };
}

get params() {
return this.#route?.params ?? {};
}

get route() {
return this.#route;
}

get target() {
return this.#route?.handler;
}

subscribe(fn) {
this.#observer.subscribe(fn);
}

/**
* 라우트 등록
*/
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,
});
}

#findRoute(url) {
const pathname = url.split("?")[0];
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;
}

/**
* 서버에서 URL로 라우팅 초기화
*/
navigate(url, query = {}) {
this.#query = query;
this.#route = this.#findRoute(url);
}

push() {
// 서버에서는 no-op
}

start() {
// 서버에서는 no-op
}

static parseQuery = Router.parseQuery;
static stringifyQuery = Router.stringifyQuery;
static getUrl = Router.getUrl;
}
14 changes: 10 additions & 4 deletions packages/vanilla/src/lib/createStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
* @param {Storage} storage - 기본값은 localStorage
* @returns {Object} { get, set, reset }
*/
export const createStorage = (key, storage = window.localStorage) => {
export const createStorage = (key, storage) => {
const resolvedStorage =
storage ?? (typeof window !== "undefined" && window?.localStorage ? window.localStorage : null);

const get = () => {
if (!resolvedStorage) return null;
try {
const item = storage.getItem(key);
const item = resolvedStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch (error) {
console.error(`Error parsing storage item for key "${key}":`, error);
Expand All @@ -16,16 +20,18 @@ export const createStorage = (key, storage = window.localStorage) => {
};

const set = (value) => {
if (!resolvedStorage) return;
try {
storage.setItem(key, JSON.stringify(value));
resolvedStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(`Error setting storage item for key "${key}":`, error);
}
};

const reset = () => {
if (!resolvedStorage) return;
try {
storage.removeItem(key);
resolvedStorage.removeItem(key);
} catch (error) {
console.error(`Error removing storage item for key "${key}":`, error);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vanilla/src/lib/createStore.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createObserver } from "./createObserver";
import { createObserver } from "./createObserver.js";

/**
* Redux-style Store 생성 함수
Expand Down
9 changes: 5 additions & 4 deletions packages/vanilla/src/lib/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./createObserver";
export * from "./createStore";
export * from "./createStorage";
export * from "./Router";
export * from "./createObserver.js";
export * from "./createStore.js";
export * from "./createStorage.js";
export * from "./Router.js";
export * from "./ServerRouter.js";
Loading
Loading