Skip to content

[6팀 한세준] Chapter 4-1 성능최적화: SSR, SSG, Infra#20

Open
hansejun wants to merge 31 commits intohanghae-plus:mainfrom
hansejun:main
Open

[6팀 한세준] Chapter 4-1 성능최적화: SSR, SSG, Infra#20
hansejun wants to merge 31 commits intohanghae-plus:mainfrom
hansejun:main

Conversation

@hansejun
Copy link

@hansejun hansejun commented Dec 15, 2025

과제 체크포인트

배포 링크

기본과제 (Vanilla SSR & SSG)

Express SSR 서버

  • Express 미들웨어 기반 서버 구현
  • 개발/프로덕션 환경 분기 처리
  • HTML 템플릿 치환 (<!--app-html-->, <!--app-head-->)

서버 사이드 렌더링

  • 서버에서 동작하는 Router 구현
  • 서버 데이터 프리페칭 (상품 목록, 상품 상세)
  • 서버 상태관리 초기화

클라이언트 Hydration

  • window.__INITIAL_DATA__ 스크립트 주입
  • 클라이언트 상태 복원
  • 서버-클라이언트 데이터 일치

Static Site Generation

  • 동적 라우트 SSG (상품 상세 페이지들)
  • 빌드 타임 페이지 생성
  • 파일 시스템 기반 배포

심화과제 (React SSR & SSG)

React SSR

  • renderToString 서버 렌더링
  • TypeScript SSR 모듈 빌드
  • Universal React Router (서버/클라이언트 분기)
  • React 상태관리 서버 초기화

React Hydration

  • Hydration 불일치 방지
  • 클라이언트 상태 복원

Static Site Generation

  • 동적 라우트 SSG (상품 상세 페이지들)
  • 빌드 타임 페이지 생성
  • 파일 시스템 기반 배포

아하! 모먼트 (A-ha! Moment)

1. Universal Router가 필요했던 이유

기존 클라이언트 전용 Router는 window.history 등 브라우저 API에 강하게 의존하고 있어, 서버 환경에서 그대로 실행할 경우 에러가 발생하는 문제가 있었습니다. 구현은 분리하되 인터페이스는 동일하게 유지하는 Universal Router 패턴을 적용했습니다.

  • 환경 분기는 모듈 레벨에서 단 한 번만 수행해야 hydration 이슈를 피할 수 있음
  • 서버/클라이언트가 동일한 Router API를 사용할 수 있도록 인터페이스를 맞추는 것이 핵심
  • 서버 라우터는 사이드 이펙트 없이 URL → Route 매칭 역할만 수행
export const router =
  typeof window === "undefined"
    ? new ServerRouter<FunctionComponent>(BASE_URL)
    : new Router<FunctionComponent>(BASE_URL);

2. 서버 환경에서 스토어는 격리되어야 한다

SSR 서버는 여러 요청을 동시에 처리하므로, 전역 스토어를 그대로 공유하면 요청 간 상태가 섞이는 문제가 발생했습니다.
각 SSR의 시작 지점마다 스토어를 명시적으로 초기 상태로 리셋했습니다.

  • SSR에서는 Reset → Prefetch → Render → Snapshot 순서가 매우 중요
  • 요청 단위 상태 격리를 통해 예측 가능한 SSR 결과를 보장
export const render = async (url: string, query = {}) => {
  // 요청 단위 상태 격리
  productStore.dispatch({
    type: PRODUCT_ACTIONS.SETUP,
    payload: initialProductState,
  });

  cartStore.dispatch({
    type: CART_ACTIONS.CLEAR_CART,
  });

  // 라우트별 데이터 프리페칭 후 렌더링
};

3. SSR에서 useSyncExternalStore 하이드레이션 에러

useSyncExternalStore를 사용할 경우, SSR 환경에서는 반드시 getServerSnapshot을 제공해야 hydration 미스매치를 피할 수 있었습니다. (서버 렌더링 기준 스냅샷이 명시되지 않으면, hydration 시점에 상태 불일치 발생)

4. 하이드레이션 전략

SSR로 데이터를 이미 가져왔음에도, 클라이언트에서 다시 API 요청을 보내는 것은 불필요한 중복 비용이었습니다. 서버에서 최종 상태를 직렬화하여 window.__INITIAL_DATA__로 주입하고, 클라이언트 시작 시 이를 이용해 스토어를 복원한 뒤 hydration을 수행했습니다.

  • 클라이언트는 추가 API 요청 없이 즉시 렌더링 가능
  • 상태 복원 → 라우터 시작 → hydrateRoot 순서가 중요
window.__INITIAL_DATA__ = ${JSON.stringify(state).replace(/</g, "\\u003c")}
// 클라이언트
if (initialData?.product) {
  productStore.dispatch({
    type: PRODUCT_ACTIONS.SETUP,
    payload: initialData.product,
  });
}

자유롭게 회고하기

이번 과제는 코드가 어떤 환경에서 실행되는지에 대해 많은 고민이 필요했던 작업이었습니다. 기존에는 당연하게 사용하던 코드들이 서버 환경에서는 바로 문제를 일으켰고, 이를 해결하는 과정에서 실행 시점과 환경을 구분하지 않으면 전체 흐름이 쉽게 깨질 수 있다는 점을 체감하게 되었습니다.

특히 서버에서 생성한 결과와 클라이언트에서 처음 사용하는 값이 조금이라도 어긋나면 문제가 발생하면서, 상태를 언제 생성하고 언제 초기화해야 하는지에 대해 이전보다 더 신경 쓰게 되었습니다. 이 경험을 통해 상태 관리가 단순히 값을 저장하는 문제가 아니라, 기준 시점을 명확히 하는 일이라는 점을 이해하게 되었습니다.

라우터와 상태 관리 방식 역시 자연스럽게 정리되었습니다. 서버와 클라이언트의 동작은 본질적으로 다를 수밖에 없다는 점을 인정하고, 구현은 분리하되 인터페이스를 맞추는 방식이 더 안정적이라는 것을 알게 되었습니다. 이를 통해 각각의 역할에 맞게 라우터를 분리하여 관리하는 구조에 대한 이해도 함께 높아졌습니다.

전체적으로 이번 과제를 통해 SSR과 SSG의 동작 방식에 대한 이해가 한층 깊어졌고, 앞으로 유사한 작업을 진행할 때 더 명확한 기준을 가지고 접근할 수 있을 것이라고 생각합니다.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@hansejun hansejun changed the title 한세준 과자 [6팀 한세준] Chapter 4-1 성능최적화: SSR, SSG, Infra Dec 15, 2025
hansejun and others added 28 commits December 16, 2025 21:46
ServerRouter receives URLs with base path already stripped by server.js,
so it should not include baseUrl in its route regex patterns. This fix
ensures proper route matching in server-side rendering.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Tests expect window.__INITIAL_DATA__ but code was using window.__INITIAL_STATE__.
Changed variable name in server.js, main.js, and static-site-generate.js.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Add title and description meta tags to index.html
- Generate page-specific meta tags in main-server.js
- Inject meta tags dynamically in server.js and static-site-generate.js
- Product pages now have dynamic titles: "Product Name - 쇼핑몰"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
문제점:
- SSG가 index.html을 생성할 때 placeholder를 교체하여, SSR에서 사용할 수 없었음
- 쿼리 파라미터가 있는 요청도 정적 index.html을 반환하여 필터링 미적용

해결 방법:
1. SSG에서 placeholder가 보존된 template.html을 별도 생성
2. SSR은 template.html을 템플릿으로 사용하도록 변경
3. server.js에 쿼리 파라미터 체크 미들웨어 추가
   - 쿼리 파라미터가 있으면 SSR로 처리
   - 쿼리가 없으면 정적 index.html 반환

변경 파일:
- server.js: 조건부 정적 파일 서빙 및 template.html 사용
- static-site-generate.js: template.html 생성 로직 추가
- src/main-server.js: 모듈 레벨 라우트 등록 및 Store 초기화
- src/lib/ServerRouter.js: 쿼리 객체 명시적 복사로 상태 격리
- src/router/withLifecycle.js: 서버 환경에서 watches 실행 방지

테스트 결과:
- e2e:basic 테스트 57개 모두 통과 (이전 3개 실패 → 0개 실패)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Create src/types/global.d.ts with window.__INITIAL_DATA__ interface
- Define types for product store, cart store, and route state
- Enable type safety for SSR state serialization/deserialization

This is critical for React 18 SSR implementation to prevent TypeScript
errors when accessing window.__INITIAL_DATA__ during hydration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Create ServerRouter class in lib package for server-side URL matching
- Implement router factory (createRouter.ts) with environment branching
- Export router instance that works in both server and client environments

Key features:
- ServerRouter: No window dependencies, pure URL matching
- Supports same API as client Router (addRoute, match, query, params)
- Environment check at module level (NOT in components)
- Prevents hydration mismatches

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
CRITICAL FIX: Add third parameter (getServerSnapshot) to useSyncExternalStore

This is the most important change for SSR implementation:
- Prevents hydration mismatch errors
- Required for React 18+ SSR compatibility
- Ensures server and client render identically

Without this fix:
- Hydration warnings in console
- Server/client rendering inconsistency
- E2E tests would fail

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Create server-side render function using renderToString
- Implement serverFetch utilities for mock data access
- Add route-based data prefetching (products, categories)
- Generate dynamic meta tags for SEO
- Serialize store state for client hydration

Key features:
- Store isolation per request (prevents memory leaks)
- ServerRouter integration for URL matching
- React component rendering to HTML string
- State serialization for window.__INITIAL_DATA__

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Express app setup with environment-based configuration
- Development: Vite middleware integration for HMR
- Production: Conditional static file serving with SSR fallback
- SSR handler with template loading (dev/prod branching)
- State injection via window.__INITIAL_DATA__ (XSS safe)
- Dynamic meta tag replacement for SEO
- Error handling with Vite stack trace fixing

Phase 3.2 완료 (server.js 구현)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Added default title and description meta tags
- Placeholders (<!--app-head--> and <!--app-html-->) already exist
- Meta tags will be dynamically replaced during SSR

Phase 3.3 완료 (HTML 템플릿 업데이트)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
CRITICAL SSR 수정사항:
1. Router hooks에 type assertion 추가 (Router<FunctionComponent>)
   - useRouterQuery, useRouterParams, useCurrentPage
   - 클라이언트 전용 hooks이므로 안전한 type cast

2. useRouter hook에 getServerSnapshot 추가 (packages/lib)
   - useSyncExternalStore의 세 번째 파라미터 필수

3. createStorage에 lazy initialization 적용
   - window.localStorage를 모듈 레벨 대신 함수 내부에서 접근
   - typeof window !== "undefined" 체크 추가

4. log.ts에 window 체크 추가
   - window.__spyCalls 초기화를 typeof window 가드로 보호

✅ SSR 동작 확인 완료: curl http://localhost:5176/ 성공!

Phase 3.4 완료 (SSR 동작 확인)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- Import hydrateRoot instead of createRoot
- Restore product store from window.__INITIAL_DATA__.product
- Restore cart store from window.__INITIAL_DATA__.cart
- Use hydrateRoot to make SSR HTML interactive

Phase 4.1 완료 (main.tsx Hydration 구현)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
- 홈페이지 정적 HTML 생성 (/, 기본 상태)
- 404 페이지 복사
- 전체 상품 상세 페이지 생성 (340개)
- SSR 템플릿(template.html) 별도 저장 (플레이스홀더 보존)
- 메타 태그 동적 주입 (title, description)
- State injection via window.__INITIAL_DATA__

Phase 5 완료 (SSG 구현)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
hansejun and others added 2 commits December 18, 2025 01:59
Added nodemon as devDependency to fix failing dev SSR E2E tests.
The dev:ssr script requires nodemon for auto-restarting the server
on file changes during development.

E2E test results: 57/57 passed (fixed 8 previously failing dev SSR tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Updated lockfile after adding nodemon devDependency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant