Skip to content

Commit d4d839b

Browse files
committed
feat: 바닐라 SSR Express 서버 구성
1 parent d28ce51 commit d4d839b

File tree

2 files changed

+322
-23
lines changed

2 files changed

+322
-23
lines changed

packages/vanilla/server.js

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,66 @@
11
import express from "express";
2+
import fs from "node:fs/promises";
23

3-
const prod = process.env.NODE_ENV === "production";
4+
const isProduction = process.env.NODE_ENV === "production";
45
const port = process.env.PORT || 5173;
5-
const base = process.env.BASE || (prod ? "/front_6th_chapter4-1/vanilla/" : "/");
6+
const base = process.env.BASE || (isProduction ? "/front_6th_chapter4-1/vanilla/" : "/");
7+
8+
const templateHtml = isProduction ? await fs.readFile("./dist/client/index.html", "utf-8") : "";
69

710
const app = express();
811

9-
const render = () => {
10-
return `<div>안녕하세요</div>`;
11-
};
12-
13-
app.get("*all", (req, res) => {
14-
res.send(
15-
`
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>
26-
</html>
27-
`.trim(),
28-
);
12+
// Add Vite or respective production middlewares
13+
/** @type {import('vite').ViteDevServer | undefined} */
14+
let vite;
15+
if (!isProduction) {
16+
const { createServer } = await import("vite");
17+
vite = await createServer({
18+
server: { middlewareMode: true },
19+
appType: "custom",
20+
base,
21+
});
22+
app.use(vite.middlewares);
23+
} else {
24+
const compression = (await import("compression")).default;
25+
const sirv = (await import("sirv")).default;
26+
app.use(compression());
27+
app.use(base, sirv("./dist/client", { extensions: [] }));
28+
}
29+
30+
//모든 url을 핸들링하고 라우팅 작업을 진행함
31+
32+
app.use("/src", express.static(`${base}src`));
33+
34+
app.get("*all", async (req, res) => {
35+
try {
36+
const url = req.originalUrl.replace(base, "");
37+
38+
/** @type {string} */
39+
let template;
40+
/** @type {import('./src/entry-server.js').render} */
41+
let render;
42+
if (!isProduction) {
43+
// Always read fresh template in development
44+
template = await fs.readFile("./index.html", "utf-8");
45+
template = await vite.transformIndexHtml(url, template);
46+
render = (await vite.ssrLoadModule("/src/main-server.js")).render;
47+
} else {
48+
template = templateHtml;
49+
render = (await import("./dist/server/main-server.js")).render;
50+
}
51+
52+
const rendered = await render(req, res);
53+
54+
const html = template
55+
.replace(`<!--app-head-->`, rendered.head ?? "")
56+
.replace(`<!--app-html-->`, rendered.html ?? "");
57+
58+
res.status(200).set({ "Content-Type": "text/html" }).send(html);
59+
} catch (e) {
60+
vite?.ssrFixStacktrace(e);
61+
console.log(e.stack);
62+
res.status(500).end(e.stack);
63+
}
2964
});
3065

3166
// Start http server

pr.md

Lines changed: 265 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,265 @@
1-
준일님짱짱맨
1+
## 과제 체크포인트
2+
3+
### 배포 링크
4+
5+
<!--
6+
배포 링크를 적어주세요
7+
예시: https://<username>.github.io/front-6th-chapter4-1/
8+
9+
배포가 완료되지 않으면 과제를 통과할 수 없습니다.
10+
배포 후에 정상 작동하는지 확인해주세요.
11+
-->
12+
13+
### 기본과제 (Vanilla SSR & SSG)
14+
15+
#### Express SSR 서버
16+
17+
- [x] Express 미들웨어 기반 서버 구현
18+
- [x] 개발/프로덕션 환경 분기 처리
19+
- [x] HTML 템플릿 치환 (`<!--app-html-->`, `<!--app-head-->`)
20+
21+
#### 서버 사이드 렌더링
22+
23+
- [ ] 서버에서 동작하는 Router 구현
24+
- [ ] 서버 데이터 프리페칭 (상품 목록, 상품 상세)
25+
- [ ] 서버 상태관리 초기화
26+
27+
#### 클라이언트 Hydration
28+
29+
- [ ] `window.__INITIAL_DATA__` 스크립트 주입
30+
- [ ] 클라이언트 상태 복원
31+
- [ ] 서버-클라이언트 데이터 일치
32+
33+
#### Static Site Generation
34+
35+
- [ ] 동적 라우트 SSG (상품 상세 페이지들)
36+
- [ ] 빌드 타임 페이지 생성
37+
- [ ] 파일 시스템 기반 배포
38+
39+
### 심화과제 (React SSR & SSG)
40+
41+
#### React SSR
42+
43+
- [ ] `renderToString` 서버 렌더링
44+
- [ ] TypeScript SSR 모듈 빌드
45+
- [ ] Universal React Router (서버/클라이언트 분기)
46+
- [ ] React 상태관리 서버 초기화
47+
48+
#### React Hydration
49+
50+
- [ ] Hydration 불일치 방지
51+
- [ ] 클라이언트 상태 복원
52+
53+
#### Static Site Generation
54+
55+
- [ ] 동적 라우트 SSG (상품 상세 페이지들)
56+
- [ ] 빌드 타임 페이지 생성
57+
- [ ] 파일 시스템 기반 배포
58+
59+
## 구현 과정 돌아보기
60+
61+
### 가장 어려웠던 부분과 해결 과정
62+
63+
sirv는 스태틱한 파일을 다루는 미들웨어
64+
65+
SSR은 html string을 만들어주고 클라이언트에서 hydration 하는 과정을 겪는다.
66+
당연하게도 SSR에서 각 경로마다 필요한 내용이 작성이 될 것이고, 클라이언트에서도 각 라우터에 따른 hydration 과정이 다를 것이다.
67+
68+
SSR에서 수행하게될 내용은 기본 template, 데이터의 pre-fetch 작업이 될 것이다.
69+
리액트 서버컴포넌트는 그러면 어떻게 동작하는 걸까?
70+
각 렌더링해야될 템플릿을 만들고 구성하는걸까..?
71+
72+
<!--
73+
구체적인 문제와 해결 과정을 적어주세요.
74+
75+
예시:
76+
- "서버와 클라이언트의 라우터 동작이 달라서 Hydration 에러가 발생했는데, typeof window 체크로 환경을 분기하여 해결했습니다."
77+
- "React renderToString에서 비동기 데이터 처리를 어떻게 할지 막막했는데, 서버에서 미리 데이터를 페칭한 후 상태를 초기화하는 방식으로 구현했습니다."
78+
- "SSG에서 동적 라우트 생성 시 Promise.all로 병렬 처리해야 하는지, 순차 처리해야 하는지 고민이었는데, 메모리 사용량을 고려해 배치 처리로 구현했습니다."
79+
-->
80+
81+
### 구현하면서 새롭게 알게 된 개념
82+
83+
<!--
84+
과제를 통해 처음 알게 되거나 더 깊이 이해하게 된 개념들을 적어주세요.
85+
86+
예시:
87+
- "SSR과 SSG의 차이점을 실제 구현을 통해 체감했습니다. SSR은 요청 시마다 렌더링, SSG는 빌드 타임에 미리 생성."
88+
- "Hydration 과정에서 서버 HTML과 클라이언트 Virtual DOM이 일치해야 한다는 것을 알게 되었습니다."
89+
- "Universal JavaScript 패턴의 핵심은 같은 코드가 서버와 클라이언트에서 다르게 동작해야 한다는 점이었습니다."
90+
- "renderToString은 동기적이라 비동기 작업은 사전에 완료해야 한다는 제약이 있었습니다."
91+
-->
92+
93+
### 성능 최적화 관점에서의 인사이트
94+
95+
<!--
96+
성능 측면에서 고려했던 점이나 개선 방안을 적어주세요.
97+
98+
예시:
99+
- "SSG로 생성된 정적 파일들이 CDN 캐싱에 더 유리하다는 것을 확인했습니다."
100+
- "초기 페이지 로드 시간 측정 결과 SSR이 CSR 대비 30% 빠른 FCP를 보여주었습니다."
101+
- "번들 크기 최적화를 위해 서버용과 클라이언트용 빌드를 분리했습니다."
102+
- "메모리 사용량을 고려해 대량의 SSG 페이지 생성 시 배치 처리를 도입했습니다."
103+
-->
104+
105+
## 학습 갈무리
106+
107+
### Q1. 현재 구현한 SSR/SSG 아키텍처에서 확장성을 고려할 때 어떤 부분을 개선하시겠습니까?
108+
109+
<!--
110+
예시 답변 방향:
111+
- 마이크로서비스 아키텍처 적용 시 고려사항
112+
- 다국어 지원을 위한 구조 개선
113+
- 대규모 트래픽 대응을 위한 캐싱 전략
114+
- CDN과 연동한 배포 파이프라인 최적화
115+
-->
116+
117+
### Q2. Express 서버 대신 다른 런타임(Cloudflare Workers, Vercel Edge Functions 등)을 사용한다면 어떤 점을 수정해야 할까요?
118+
119+
<!--
120+
예시 답변 방향:
121+
- 서버리스 환경의 제약사항과 해결책
122+
- Cold Start 최적화 방안
123+
- Edge Computing의 장단점
124+
- 런타임별 API 차이점과 추상화 필요성
125+
-->
126+
127+
### Q3. 현재 구현에서 성능 병목이 될 수 있는 지점은 어디이고, 어떻게 개선하시겠습니까?
128+
129+
<!--
130+
예시 답변 방향:
131+
- 서버 렌더링 시 CPU 사용량 최적화
132+
- 메모리 누수 방지 방안
133+
- 데이터베이스 쿼리 최적화 (N+1 문제 등)
134+
- 번들 크기 최적화 (코드 스플리팅, Tree Shaking)
135+
-->
136+
137+
### Q4. 1000개 이상의 상품 페이지를 SSG로 생성할 때 고려해야 할 사항은 무엇입니까?
138+
139+
<!--
140+
예시 답변 방향:
141+
- 빌드 시간 최적화 (병렬 처리, 증분 빌드)
142+
- 메모리 사용량 관리
143+
- CDN 캐시 무효화 전략
144+
- 부분 재빌드 구현 방안
145+
-->
146+
147+
### Q5. Hydration 과정에서 사용자가 느낄 수 있는 UX 이슈는 무엇이고, 어떻게 개선할 수 있을까요?
148+
149+
<!--
150+
예시 답변 방향:
151+
- 인터랙션 차단 시간 최소화
152+
- 로딩 상태 표시 방안
153+
- Progressive Enhancement 적용
154+
- Skeleton UI 활용
155+
-->
156+
157+
### Q6. 이번 과제에서 학습한 내용을 실제 프로덕션 환경에 적용할 때 추가로 고려해야 할 사항은?
158+
159+
<!--
160+
예시 답변 방향:
161+
- 모니터링 및 로깅 체계
162+
- 에러 핸들링 및 Fallback 전략
163+
- A/B 테스트 적용 방안
164+
- 보안 고려사항 (XSS, CSP 등)
165+
-->
166+
167+
### Q7. Next.js 같은 프레임워크 대신 직접 구현한 SSR/SSG의 장단점은 무엇인가요?
168+
169+
<!--
170+
예시 답변 방향:
171+
- 학습 효과와 깊은 이해
172+
- 커스터마이징 자유도
173+
- 유지보수 비용과 안정성
174+
- 생태계와 커뮤니티 지원
175+
-->
176+
177+
### Q8. Next.js 를 이용하여 SSG 방식으로 배포하려면 어떻게 해야 좋을까요?
178+
179+
<!--
180+
예시 답변 방향:
181+
- 학습 효과와 깊은 이해
182+
- 커스터마이징 자유도
183+
- 유지보수 비용과 안정성
184+
- 생태계와 커뮤니티 지원
185+
-->
186+
187+
## 코드 품질 향상
188+
189+
### 자랑하고 싶은 구현
190+
191+
<!--
192+
구체적인 코드 블록과 함께 설명해주세요.
193+
194+
예시:
195+
"정규식 라우터에서 매개변수 추출 로직을 효율적으로 구현했습니다:"
196+
197+
```javascript
198+
const pathPattern = path.replace(/:([^/]+)/g, (match, paramName) => {
199+
this.paramNames.push(paramName);
200+
return '([^/]+)';
201+
});
202+
```
203+
204+
"이 방식으로 :id, :category 등 다양한 매개변수를 동적으로 처리할 수 있게 되었습니다."
205+
-->
206+
207+
### 개선하고 싶은 부분
208+
209+
<!--
210+
구체적인 개선 방향과 이유를 함께 적어주세요.
211+
212+
예시:
213+
"현재 에러 처리가 try-catch만으로 구성되어 있는데, 에러 타입별로 다른 Fallback 페이지를 보여주도록 개선하고 싶습니다."
214+
215+
"메모리 최적화를 위해 대용량 데이터 처리 시 스트리밍 방식을 도입하고 싶습니다."
216+
-->
217+
218+
### 리팩토링 계획
219+
220+
<!--
221+
코드 구조나 아키텍처 개선 계획을 적어주세요.
222+
223+
예시:
224+
- "라우터 로직을 클래스에서 함수형으로 리팩토링"
225+
- "타입 안전성 강화를 위한 제네릭 활용"
226+
- "의존성 주입 패턴 도입으로 테스트 용이성 개선"
227+
-->
228+
229+
## 학습 연계
230+
231+
### 다음 학습 목표
232+
233+
<!--
234+
이번 과제를 통해 더 배우고 싶어진 주제들을 적어주세요.
235+
236+
예시:
237+
- "Serverless 환경에서의 SSR 최적화"
238+
- "GraphQL과 SSR/SSG 연동 방안"
239+
- "웹 컴포넌트와 SSR 호환성"
240+
- "EdgeSide Includes(ESI) 활용한 부분 캐싱"
241+
-->
242+
243+
### 실무 적용 계획
244+
245+
<!--
246+
현재 또는 향후 프로젝트에 어떻게 적용할지 계획을 적어주세요.
247+
248+
예시:
249+
- "회사 블로그를 SSG로 마이그레이션하여 성능 개선"
250+
- "마케팅 랜딩 페이지에 SSR 적용으로 SEO 최적화"
251+
- "E-commerce 상품 페이지의 초기 로딩 속도 개선"
252+
-->
253+
254+
## 리뷰 받고 싶은 내용
255+
256+
<!--
257+
SSR/SSG 구현과 관련된 구체적인 피드백을 요청해주세요.
258+
259+
구체적인 질문 예시:
260+
- "packages/vanilla/src/main-server.js의 라우터 매개변수 추출 로직에서 정규식 패턴이 복잡한 URL에도 안정적으로 동작할지 검토 부탁드립니다."
261+
- "React SSR에서 서버와 클라이언트의 상태 동기화 로직이 대용량 데이터에서도 성능상 문제없을지 조언 부탁드립니다."
262+
- "현재 구현한 SSG 빌드 과정이 상품 개수가 1000개 이상으로 늘어날 때도 효율적으로 동작할지, 최적화 방안이 있다면 제안해주세요."
263+
- "TypeScript SSR 모듈의 타입 정의에서 놓친 부분이나 더 안전하게 개선할 수 있는 부분이 있는지 검토해주세요."
264+
- "Universal Router 구현에서 메모리 누수나 성능 이슈 가능성은 없는지 확인 부탁드립니다."
265+
-->

0 commit comments

Comments
 (0)