realstone2 님의 상세페이지[3팀 여진석] Chapter 4-1 성능 최적화

과제 체크포인트

배포 링크

기본과제 https://realstone2.github.io/front_6th_chapter4-1/vanilla/

기본과제 (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 불일치 방지
  • 클라이언트 상태 복원

React Static Site Generation

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

구현 과정 돌아보기

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

SSR Router SSR 라우터 분기 방식을 단순히 아래처럼 만들고 router를 서버나 클라이언트에서 사용하려고 했더니 문제가 발생하였다.

const router = ssr ? new ssrRouter() : new clientRouter();

하나씩 실행할 때는 문제가 없었는데, e2e테스트를 동시에 돌리는 경우에 문제가 발생하였다. 고민하다보니까 서버에서 router 인스턴스를 전역으로 한개를 사용하면서 query, route의 값이 계속 바뀌는게 문제였다.

과거에도 SSR에서 store를 사용했다가 문제를 겪었던 기억이 경험이 있었는데, router도 마찬가지라는 것을 간과했다.

기존에 router를 import해서 가져오던 방식을 nextJS가 route값을 params로 받는 방식이 떠올라서 route를 전달받는 방식으로 수정하였다.

ssr html 만드는 과정 위에서 route전달 하는 방식으로 수정한다고 했는데, 이러다보니까 기존 page 인터페이스형식도 변경이 필요하다고 느껴졌다. 기존에는 route를 등록할 때 getSSRData를 받아오고, store를 설정해주는 등의 작업을 거쳤는데, 이것도 문제였다.

express에서 주소를 요청받으면 그 때 서버데이터를 새로 생성하고 store에 넣어주도록 수정하였다. 해당 방식을 좀 일관성있는 인터페이스를 구성하고자 아래 인터페이스를 구성하였다.

renderPage 함수로 생성할 때 마다 새롭게 데이터가 셋팅되도록 수정하였다.


export const withSSR = ({ render, onMount, getSSRData, renderHead }) => {
  return {
    renderPage: async (router) => {
      const serverData = await getSSRData?.(router);
      onMount?.(serverData);
      const html = render(router);
      return {
        html,
        serverData,
        head: renderHead?.(serverData),
      };
    },
  };
};


export const HomePage = import.meta.env.SSR
  ? withSSR({
      renderHead: () => "<title>쇼핑몰 - 홈</title>",
      render: renderPage,
      getSSRData: async (router) => {
        const response = await Promise.all([getProducts(router.query), getCategories()]);
        console.log("🚀 ~ response:", response);
        const [
          {
            products,
            pagination: { total },
          },
          categories,
        ] = response;

        return {
          products,
          categories,
          totalCount: total,
        };
      },
      onMount: ({ products, categories, totalCount }) => {
        productStore.dispatch({
          type: PRODUCT_ACTIONS.SETUP,
          payload: {
            products,
            categories,
            totalCount: totalCount,
            loading: false,
            status: "done",
          },
        });
      },
    })
  : withLifecycle(
    ...
  );

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

SSR과 client에서의 hydration nextjs를 page라우터를 사용할 때 서버에서 한번 랜더링 클라이언트에서 한번 랜더링 된다라는 개념이 왜그런지 잘 이해가 안되었는데, 이번에 직접구성하다보니 이해가 되었다. 결국 같은 DomTree를 구성하게 되고, client에서는 event등록 css등의 hydration 과정을 거치게 된다.

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

학습 갈무리

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

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

람다를 사용해서 server를 구성한다면 express처럼 항상 서버가 띄워져있는 상황보다 서버비용을 줄일 수 있다.

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

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

상품이 많을수록 빌드가 너무 오랜 시간이 걸리는 것이 문제다.

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

hydration이 되기전까지는 이벤트가 발생하지 않는 문제가 있다.

로딩 UI를 노출시키거나, event를 등록하는 js파일을 가장 우선순위로 호출하면 어느정도는 해결되지 않을까싶다.

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

SSR의 경우는 서버 모니터링 필요. SSG의 경우는 얼마나 자주 빌드를 해줄 것인지 정책을 정해야한다.

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

학습적으로 원하는 기능을 필요에 따라 넣을 수 있다. 불필요하게 지원해주는 nextjs기능을 사용하지 않아도 된다. vercel에 종속적이지 않고 사용하는 클라우드에 접목시키기 쉽다.

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

코드 품질 향상

자랑하고 싶은 구현

page 컴포넌트 선언하는 부분이 깔끔한 인터페이스라고 생각되었습니다.


export const withSSR = ({ render, onMount, getSSRData, renderHead }) => {
  return {
    renderPage: async (router) => {
      const serverData = await getSSRData?.(router);
      onMount?.(serverData);
      const html = render(router);
      return {
        html,
        serverData,
        head: renderHead?.(serverData),
      };
    },
  };
};


export const HomePage = import.meta.env.SSR
  ? withSSR({
      renderHead: () => "<title>쇼핑몰 - 홈</title>",
      render: renderPage,
      getSSRData: async (router) => {
        const response = await Promise.all([getProducts(router.query), getCategories()]);
        console.log("🚀 ~ response:", response);
        const [
          {
            products,
            pagination: { total },
          },
          categories,
        ] = response;

        return {
          products,
          categories,
          totalCount: total,
        };
      },
      onMount: ({ products, categories, totalCount }) => {
        productStore.dispatch({
          type: PRODUCT_ACTIONS.SETUP,
          payload: {
            products,
            categories,
            totalCount: totalCount,
            loading: false,
            status: "done",
          },
        });
      },
    })
  : withLifecycle(
    ...
  );

개선하고 싶은 부분

캐싱전략, 서버컴포넌트 방식 도입 등을 도입시켜보고 싶습니다.

지금은 SSG를 직접 빌드해야지만 작동하지만 캐싱시간을 정해놓는 인터페이스를 구현하고 싶습ㄴ디ㅏ.

리팩토링 계획

학습 연계

다음 학습 목표

실무 적용 계획

개인 블로그를 만들 때 해당 내용을 생각하면서 구성할 수 있을 것 같습니다.

리뷰 받고 싶은 내용

  1. SSR로 한번 페이지를 전달받고나서 client에서 event로 navigation이 될 때 이동이 SSR template 내용이 반영되지 않다보니 head title과 serverData가 보고있는 페이지와 일치하지 않습니다. 페이지 이동시에 매번 내용을 넣어주면서 교체시켜줘야되나도 생각해봤지만, 이게 맞나? 싶은 것 같습니다. 이 문제를 어떤식으로 해결할 수 있을까요?

과제 피드백

수고하셨습니다~ 진석님

Q. SSR로 한번 페이지를 전달받고나서 client에서 event로 navigation이 될 때 이동이 SSR template 내용이 반영되지 않다보니 head title과 serverData가 보고있는 페이지와 일치하지 않습니다.
페이지 이동시에 매번 내용을 넣어주면서 교체시켜줘야되나도 생각해봤지만, 이게 맞나? 싶은 것 같습니다.
이 문제를 어떤식으로 해결할 수 있을까요?

A. SSR 렌더링 된 이후로는 SPA와 동일하게 동작하는 것이 기본적이기 때문에 title 은 동적으로 업데이트하는 것이 맞는 것 같아요~ nextjs도 동일하게 구현하는 것으로 알고있습니다.