Skip to content

Conversation

@jun17183
Copy link

@jun17183 jun17183 commented Aug 30, 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 (상품 상세 페이지들)
  • 빌드 타임 페이지 생성
  • 파일 시스템 기반 배포


구현 과정 돌아보기

이번 과제는 SSR에 대한 개념이 약해서 혼자 진행하긴 너무 어려웠다.
다른 분들의 코드를 참고하려 했지만 사실상 가져다 쓰고만 있었기에 과제 통과를 받기엔 스스로 양심이 찔렸다.
대신 코드를 분석하고 글로 정리하며 이번 기회에 SSR에 대한 개념을 확실히 잡기로 했다.

SSR이란?

  • SSR이란 Server Side Rendering의 약자로, 말 그대로 서버 측에서 렌더링을 한다는 것
  • 하지만 결국 HTTP는 텍스트 기반 통신이기에 DOM 요소나 JS와 같은 복잡한 데이터 구조 자체를 전송할 수는 없음
  • 그렇기에 사용자에게 전달할 DOM을 문자열로 변환하여 전달해야 함
app.use(/^(?!.*\/api).*$/, async (req, res) => {
  try {
    // ...
    res.status(200).set({ "Content-Type": "text/html" }).send(html);
  }
});

html 변환

  • 기존 CSR의 React를 생각해 보면 결국 사용자에게 전달되는 html은 빈 내용의 index.html이 전부다.
  • 이후 index.html을 읽으며 js를 실행하면서 화면을 그리게 된다.
<body class="bg-gray-50">
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>
  • js(jsx)에선 서버에 데이터를 요청하고(혹은 초기 데이터를 읽어) 이를 통해 DOM을 생성한다.
  • SSR의 경우 이 작업을 모두 진행 후에 완성된 DOM을 문자열 형태로 전달해 주어야 하는 것
// CSR
router.addRoute("/", HomePage);

// SSR
router.addRoute("/", () => {
  const {
    products,
    pagination: { total: totalCount },
  } = getProductsOnServer(router.query);
  const categories = getUniqueCategories();
  const results = { products, categories, totalCount };

  return {
    initialData: results,
    html: HomePage(results),
    head: "<title>쇼핑몰 - 홈</title>",
  };
});
  • 기존 CSR의 라우터와 비교해 보면 차이를 한 눈에 알 수 있는데, 기존 CSR 라우터는 컴포넌트만 리턴하는 반면 SSR의 경우 html을 그리기 위한 data, 이를 통해 먼저 그린 html, 그리고 head를 리턴한다.
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <script src="https://cdn.tailwindcss.com"></script>
  <!--app-head-->
</head>
<body class="bg-gray-50">
  <div id="root"><!--app-html--></div>
  <script type="module" src="/src/main.js"></script>
</body>
template = await fs.readFile("./index.html", "utf-8");
template = await vite.transformIndexHtml(url, template);

const rendered = await render(url, req.query);

// 클라이언트 하이드레이션용 초기 데이터 스크립트 생성
const initialDataScript = rendered.initialData
  ? `<script>window.__INITIAL_DATA__ = ${JSON.stringify(rendered.initialData)}</script>`
  : "";

// 렌더링 결과를 HTML 템플릿에 주입
const html = template
  .replace("<!--app-head-->", rendered.head ?? "")
  .replace("<!--app-html-->", rendered.html ?? "")
  .replace("</head>", `${initialDataScript}</head>`);
  • 우선 템플릿을 읽어 왔다면 앞서 라우터로부터 전달 받은 객체를 통해 html을 완성한다.

Hydration

  • Hydration은 서버에서 렌더링된 정적 HTML을 클라이언트에서 interactive한 웹 애플리케이션으로 되살리는 과정
  • 기존 CSR에서 빈 index.html에서 main.js를 읽는 것은 Hydration이 아니다.
  • 아래와 같은 형태처럼 이미 정적인 html이 그려진 후 버튼 같은 요소에 이벤트를 입히는 등 동적인 웹으로 변환하는 것이 Hydration
<!DOCTYPE html>
<html>
<body>
  <div id="root">
    <h1>안녕하세요</h1>
    <button>클릭하세요</button>  <!--    -->
    <p>카운터: 0</p>
  </div>
  <script>
    window.__INITIAL_DATA__ = {count: 0, user: {...}}
  </script>
  <script src="/bundle.js"></script>  <!-- 하이드레이션용 스크립트 -->
</body>
</html>

이번 과제에선 아래처럼 window.__INITIAL_DATA__에 초기 데이터를 넣어주고 페이지를 그린 후 (마운트 후) 해당 데이터를 통해 store를 초기화한다.

const initialDataScript = rendered.initialData
  ? `<script>window.__INITIAL_DATA__ = ${JSON.stringify(rendered.initialData)}</script>`
  : "";
export const HomePage = withLifecycle(
  {
    onMount: () => {
      if (typeof window === "undefined") {
        return;
      }
      if (window.__INITIAL_DATA__?.products?.length > 0) {
        const { products, categories, totalCount } = window.__INITIAL_DATA__;
        productStore.dispatch({
          type: PRODUCT_ACTIONS.SETUP,
          payload: {
            products,
            categories,
            totalCount,
            loading: false,
            status: "done",
          },
        });
        return;
      }
      loadProductsAndCategories();
    },
    watches: [
      () => {
        const { search, limit, sort, category1, category2 } = router.query;
        return [search, limit, sort, category1, category2];
      },
      () => loadProducts(true),
    ],
  },
  (props = {}) => {
    ...
  },
);

SSG

  • SSG(Static Site Generation)는 빌드 타임에 페이지들을 미리 렌더링하여 정적 html 파일로 생성하는 렌더링 방식
  • 위 SSR 방식처럼 html을 미리 완성하여 사용자에게 전달했다곤 하지만 여전히 다른 페이지로 이동 시 새로 페이지를 렌더링 해야 함
  • 이 새로운 페이지마저 미리 렌더링 하여 html 파일을 생성해 두는 것
async function generateStaticSite() {
  try {
    // HTML 템플릿 읽기
    const template = await fs.readFile("../../dist/vanilla/index.html", "utf-8");

    // 홈페이지 생성
    const homeResult = await generatePage("/", "../../dist/vanilla/index.html", template);

    // 주요 상품 상세 페이지들 생성
    const productIds = ["85067212996", "86940857379", "82094468339", "86188464619"];

    // 상품 페이지들을 병렬로 생성
    const productTasks = productIds.map((productId) => {
      const url = `/product/${productId}/`;
      const outputPath = `../../dist/vanilla/product/${productId}/index.html`;
      return generatePage(url, outputPath, template);
    });

    const productResults = await Promise.all(productTasks);
    ...
  }
}

Vite 미들웨어 사용 이유

if (!isProduction) {
  // 개발 환경: Vite 개발 서버 연동
  const { createServer } = await import("vite");
  vite = await createServer({
    server: { middlewareMode: true },
    appType: "custom",
    base,
  });
  app.use(vite.middlewares);
}
  1. ES 모듈 변환: Vite가 실시간으로 경로 해결 및 브라우저 호환 코드로 변환
  2. HMR (Hot Module Replacement): 코드 수정 → 즉시 반영 (서버 재시작 불필요)
  3. SSR 모듈 동적 로딩: 서버사이드에서 ES 모듈을 실시간으로 로드하고 의존성 해결
  4. 디버깅 지원: 압축된 코드 대신 원본 파일의 정확한 라인 에러 표시


가장 어려웠던 부분과 해결 과정

구현하면서 새롭게 알게 된 개념

성능 최적화 관점에서의 인사이트

학습 갈무리

Q1. 현재 구현한 SSR/SSG 아키텍처에서 확장성을 고려할 때 어떤 부분을 개선하시겠습니까?

Q2. Express 서버 대신 다른 런타임(Cloudflare Workers, Vercel Edge Functions 등)을 사용한다면 어떤 점을 수정해야 할까요?

Q3. 현재 구현에서 성능 병목이 될 수 있는 지점은 어디이고, 어떻게 개선하시겠습니까?

Q4. 1000개 이상의 상품 페이지를 SSG로 생성할 때 고려해야 할 사항은 무엇입니까?

Q5. Hydration 과정에서 사용자가 느낄 수 있는 UX 이슈는 무엇이고, 어떻게 개선할 수 있을까요?

Q6. 이번 과제에서 학습한 내용을 실제 프로덕션 환경에 적용할 때 추가로 고려해야 할 사항은?

Q7. Next.js 같은 프레임워크 대신 직접 구현한 SSR/SSG의 장단점은 무엇인가요?

Q8. Next.js 를 이용하여 SSG 방식으로 배포하려면 어떻게 해야 좋을까요?

코드 품질 향상

자랑하고 싶은 구현

개선하고 싶은 부분

리팩토링 계획

학습 연계

다음 학습 목표

실무 적용 계획

리뷰 받고 싶은 내용

과제 시작 전에도 SSR에 대한 대강의 흐름은 알고 있었으며, 위에 작성한 개념 정리처럼 코드도 모두 이해는 갑니다.
하지만 작성된 코드를 이해할 순 있어도 vite 미들웨어나 각 페이지의 onMount 처리 등 직접 작성하라고 하면 자신이 없는 부분들이 많습니다.
PR의 학습 갈무리에 있는 내용 또한 스스로 답변을 떠올려 보려고 해도 예시에 있는 주제는 생각조차 닿지 않습니다.
어쩌면 이번 주차는 저에게 아직 학습조차 힘든 레벨인가 싶은 생각까지 드는데요,
미시적으론 어떻게 SSR에 대해 공부를 하면 좋을까 라고 여쭙고 싶지만,
한편으론 지금처럼 공부해야 할 하나의 덩어리가 있을 때 이 덩어리가 가늠조차 되지 않는다면 어떻게 헤쳐나갈 수 있을지 여쭤보고 싶습니다.

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