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
28 changes: 24 additions & 4 deletions packages/lib/src/createStorage.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
import { createObserver } from "./createObserver.ts";

export const createStorage = <T>(key: string, storage = window.localStorage) => {
let data: T | null = JSON.parse(storage.getItem(key) ?? "null");
export const createStorage = <T>(key: string, storage = typeof window !== "undefined" ? window.localStorage : null) => {
let data: T | null = null;

// SSR 환경에서는 storage가 null일 수 있음
if (storage) {
try {
const storedValue = storage.getItem(key);
if (storedValue !== null) {
data = JSON.parse(storedValue);
}
} catch (error) {
console.error(`Error parsing storage item for key "${key}":`, error);
// 손상된 데이터 제거
storage.removeItem(key);
data = null;
}
}

const { subscribe, notify } = createObserver();

const get = () => data;

const set = (value: T) => {
try {
data = value;
storage.setItem(key, JSON.stringify(data));
if (storage) {
storage.setItem(key, JSON.stringify(data));
}
notify();
} catch (error) {
console.error(`Error setting storage item for key "${key}":`, error);
Expand All @@ -19,7 +37,9 @@ export const createStorage = <T>(key: string, storage = window.localStorage) =>
const reset = () => {
try {
data = null;
storage.removeItem(key);
if (storage) {
storage.removeItem(key);
}
notify();
} catch (error) {
console.error(`Error removing storage item for key "${key}":`, error);
Expand Down
47 changes: 24 additions & 23 deletions packages/react/index.html
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--app-head-->
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/src/styles.css">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: "#3b82f6",
secondary: "#6b7280"
}
}
}
};
</script>
</head>
<body class="bg-gray-50">
<div id="root"><!--app-html--></div>
<script type="module" src="/src/main.tsx"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--app-head-->
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/src/styles.css" />
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: "#3b82f6",
secondary: "#6b7280",
},
},
},
};
</script>
</head>
<body class="bg-gray-50">
<div id="root"><!--app-html--></div>
<!-- app-data -->
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
99 changes: 77 additions & 22 deletions packages/react/server.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,87 @@
import compression from "compression";
import express from "express";
import { renderToString } from "react-dom/server";
import { createElement } from "react";
import fs from "node:fs/promises";
import path from "node:path";
import sirv from "sirv";
import { createServer } from "vite";

const prod = process.env.NODE_ENV === "production";
// 환경 변수 설정
const isProduction = process.env.NODE_ENV === "production";
const port = process.env.PORT || 5174;
const base = process.env.BASE || (prod ? "/front_6th_chapter4-1/react/" : "/");
const base = process.env.BASE || (isProduction ? "/front_6th_chapter4-1/react/" : "/");

// 프로덕션 환경에서 사용할 HTML 템플릿 미리 로드
const templateHtml = isProduction ? await fs.readFile("./dist/react/index.html", "utf-8") : "";

// Vite 개발 서버 생성 (개발 환경에서 HMR과 트랜스파일링 담당)
const vite = await createServer({
server: { middlewareMode: true }, // Express와 통합하여 미들웨어 모드로 실행
appType: "custom",
base,
});

// MSW(Mock Service Worker) 서버 시작 - API 모킹용
const { mswServer } = await vite.ssrLoadModule("./src/mocks/node.ts");
mswServer.listen({ onUnhandledRequest: "bypass" });

const app = express();

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>React SSR</title>
</head>
<body>
<div id="app">${renderToString(createElement("div", null, "안녕하세요"))}</div>
</body>
</html>
`.trim(),
);
// 개발/프로덕션 환경에 따른 미들웨어 설정
if (!isProduction) {
// 개발 환경: Vite 미들웨어 사용 (HMR, 실시간 변환 등)
app.use(vite.middlewares);
} else {
app.use(compression()); // gzip 압축
app.use(base, sirv("./dist/react", { extensions: [] })); // 빌드된 정적 파일 서빙
}

// SSR 렌더링 미들웨어 - API 경로 제외한 모든 요청 처리
app.use(/^(?!.*\/api).*$/, async (req, res) => {
try {
const url = req.originalUrl.replace(base, "");
const pathname = path.normalize(`/${url.split("?")[0]}`);

let template;
let render;

if (!isProduction) {
// 개발 환경: HTML 템플릿을 실시간으로 읽고 변환
template = await fs.readFile("./index.html", "utf-8");
template = await vite.transformIndexHtml(url, template);
render = (await vite.ssrLoadModule("/src/main-server.tsx")).render;
} else {
// 프로덕션 환경: 미리 빌드된 템플릿과 렌더 함수 사용
template = templateHtml;
render = (await import("./dist/react-ssr/main-server.js")).render;
}

// React 컴포넌트를 서버에서 렌더링
const rendered = await render(pathname, req.query);

// HTML 템플릿에 렌더링된 내용 주입
const html = template
.replace(`<!--app-head-->`, rendered.head ?? "")
.replace(`<!--app-html-->`, rendered.html ?? "")
.replace(
`<!-- app-data -->`,
`<script>window.__INITIAL_DATA__ = ${JSON.stringify(rendered.__INITIAL_DATA__)};</script>`,
); // 클라이언트 하이드레이션용 초기 데이터 스크립트 생성

// 완성된 HTML 응답
res.status(200).set({ "Content-Type": "text/html" }).send(html);
} catch (error) {
// 개발 환경에서 스택 트레이스 정리
if (!isProduction && vite) {
vite.ssrFixStacktrace(error);
}

console.error("SSR 렌더링 에러:", error.message);
res.status(500).end(error.message);
}
});

// Start http server
// HTTP 서버 시작
app.listen(port, () => {
console.log(`React Server started at http://localhost:${port}`);
console.log(`🚀 React SSR 서버 실행: http://localhost:${port}`);
console.log(`📦 환경: ${isProduction ? "프로덕션" : "개발"} 모드`);
});
10 changes: 7 additions & 3 deletions packages/react/src/entities/products/productStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export const PRODUCT_ACTIONS = {
/**
* 상품 스토어 초기 상태
*/
// SSG 초기 데이터가 있는지 확인
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hasInitialData = typeof window !== "undefined" && (window as any)?.__INITIAL_DATA__;

export const initialProductState = {
// 상품 목록
products: [] as Product[],
Expand All @@ -35,10 +39,10 @@ export const initialProductState = {
currentProduct: null as Product | null,
relatedProducts: [] as Product[],

// 로딩 및 에러 상태
loading: true,
// 로딩 및 에러 상태 - 초기 데이터가 있으면 loading: false
loading: !hasInitialData,
error: null as string | null,
status: "idle",
status: hasInitialData ? "done" : "idle",

// 카테고리 목록
categories: {} as Categories,
Expand Down
Loading