JiHoon-0330 님의 상세페이지[5팀 이지훈] Chapter 4-1 성능 최적화

과제 체크포인트

배포 링크

기본: https://jihoon-0330.github.io/front_6th_chapter4-1/vanilla/ 심화: https://jihoon-0330.github.io/front_6th_chapter4-1/react/

기본과제 (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 (상품 상세 페이지들)
  • 빌드 타임 페이지 생성
  • 파일 시스템 기반 배포

구현 과정 돌아보기

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

서버에서 렌더링 할 때 전역으로 사용중인 값을 그대로 사용하도록 코드를 작성해 메모리를 공유하는 문제가 있었다. 직접 브라우저로 접근해 테스트할 땐 1개의 요청만 존재하기 때문에 정상적으로 동작했지만, e2e 테스트를 진행할 땐 query 값이 다른 여러 페이지를 동시에 요청하는 상황에서 예상하지 못한 html 응답을 받는 문제가 있었다. 하나씩 실행하면 정상적으로 동작하고, 테스트를 한번에 실행하면 간헐적인 오류가 발생하는 상황에서 문제의 원인을 파악하는 것이 어려웠다. 실패하는 케이스의 HTML 응답 값을 반복적으로 살펴보니 다른 테스트의 영향을 받는다는 것을 추측할 수 있었고, 서버에서 렌더링 할 때는 메모리를 공유하는 전역 값을 사용하지 않도록 코드를 수정해 해결했다.

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

기존에 알고 있던 개념들이지만, 직접 구현하면서 이해되는 부분이 많았던 것 같다. 그중에서 브라우저에서 렌더링 하는 과정이 기억에 남는다. 처음에 SSR 을 구현할 땐 서버쪽 구현에만 집중하다 보니, 브라우저에서 렌더링은 어떻게 처리해야 하지 라는 고민이 있었다. 브라우저에서 처리되는 과정은 CSR 과 동일한 것인데 SSR 을 한다고 생각하니 시야가 좁아지고 어렵게 느껴진 것 같다. 단순히 정보를 아는것과 이해하는 것이 이런 상황에서 차이를 만든다고 느끼게 된 것 같다.

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

SSR 을 하는 경우 완성된 페이지를 보여주기 때문에 시각적으로 더 빠르게 느껴질 수 있다. 하지만 서버에서 렌더링을 하는 시간이 오래 걸린다면 클라이언트에서 로딩 표시는 보여주는 것이 더 유리할 것 같다. 그래서 SSR 을 할 땐 캐시를 적절히 활용해서 렌더링이 빠르게 될 수 있도록 최적화가 중요할 것 같다.

SSG 의 경우 미리 만들어 놓은 페이지를 응답하기 때문에 SSR 보다 응답속도에서 유리하다. 하지만 미리 생성된 파일을 응답하기 때문에 요청마다 동적인 데이터가 필요한 경우엔 적합하지 않을 수 있다. 이럴 땐 정적으로 보여줄 수 있는 일부 영역은 미리 생성하고, 동적인 영억은 CSR 로 처리할 수 있을 것 같다.

학습 갈무리

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

next.js 처럼 캐시 시간 설정과 리다이렉션 등 서버사이드 옵션을 Page 컴포넌트 파일에서 설정이 가능하도록 하면 좋을 것 같다. 정적인 파일들은 CDN 을 활용할 수 있는 구조를 만들면 서버 부하를 감소시킬 수 있고, 성능/비용 측면에서도 개선이 가능할 것 같다.

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

서버리스는 요청이서버 메모리를 사용하는 코드가 있다면 수정이 필요하다. 이번 과제에서는 운영 모드일 때 빌드된 index.html 파일을 서버가 시작할 때 한 번 가져오고 요청마다 사용을 하는데, 서버리스에서는 요청이 들어왔을 때 index.html 파일을 읽어서 사용하도록 변경할 필요가 있다. 각 서버리스 플랫폼마다 인터페이스가 다르기 때문에 플랫폼에 맞춰서 동작할 수 있도록 서버리스 - express 간의 추상화 계층이 필요하다.

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

api 응답이 느리다면 SSR 응답이 느릴 수 있기 때문에 api 응답 속도를 개선하는 작업이 필요할 수 있다. 렌더링하는 서버에서 정적인 파일도 관리한다면 성능에 병목이 생길 수 있다. 정적인 자산들은 CDN 을 활용하도록 개선한다. 렌더링 과정 자체에서 병목이 발생하는 지점이 있는지 확인한 후 렌더링 과정을 개선하는 작업이 필요할 수도 있을 것 같다. express 를 사용하는 것에서도 성능 저하가 발생할 수 있다고 하는데, 꼭 필요한게 아니라면 가벼운 http 서버를 사용할 수 있을 것 같다.

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

동시에 너무 많은 요청을 했을 때 서버에 부하가 걸리지 않는지 확인이 필요할 것 같다. 상품과 관련된 정보가 수정되는 시점을 알 수 있다면 정보가 변경 되었을 때만 새로 생성할 수 있도록 만들면 좋을 것 같다. 사람들이 자주 방문하지 않는 비인기 상품이 존재한다면 인기가 많은 상품 xx개만 SSG 를 사용하도록 고려할 수 있을 것 같다. 페이지가 너무 많은 경우엔 ISR 을 사용하는 것이 더 적합할 것 같다. 새로운 HTML 파일을 생성한 다음엔 CDN 에서 캐시를 사용하고 있다면 캐시를 무효화 해줘야 한다.

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

브라우저에서 렌더링이 완료되기 전 까지 클릭과 같은 인터렉션이 불가능 하기 때문에 웹사이트가 동작하지 않는다고 느껴질 수 있다. 링크 이동의 경우 자바스크립트 클릭 이벤트로 이동시키지 않고, a 태그를 사용한다면 하이드레이션을 기다리지 않고 이동할 수 있다. 이렇게 자바스크립트로 기능을 구현하는 것 보다 자바스크립트가 비활성화 된 상태에서도 동작할 수 있도록 웹 표준을 지켜 구현하면 개선이 가능하다. 서버에서 제공된 HTML 과 브라우저에서 렌더링한 HTML 이 다른 경우 깜빡임이나 레이아웃 쉬프트와 같은 불편함을 겪을 수 있다. 서버에서 렌더링하는 시점과, 브라우저에서 렌더링 하는 시점이 달라지는 값에 대해서는 스켈레톤 UI 를 사용해 개선이 가능할 것 같다.

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

SSR 을 도입한다고 하면 현재 서비스 상황에 맞게 서버리스를 사용할지 ec2 인스턴스를 사용할지 고려해야 할 것 같다. 트래픽에 따라 오토 스케일링이 가능한지, 24시간 모니터링이 가능한지와 같은 문제들이 있을 것 같다. 서버리스를 사용하는 경우 용량, 함수가 실행되는 시간 등 일반적으로 부족하지 않겠지만 제한이 걸리지는 않는지 확인할 필요가 있다. 콜드 스타트가 발생하더라도 괜찮은 서비스인지 고려해야 한다. 추가로 서버측 보안을 고려해야 하고, 서버에서 오류가 발생했을 때 어떤 방법으로 대응을 할 것 인지 고려해야 할 것 같다.

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

블랙박스가 적기 때문에 제어하기가 쉽고 불필요한 오버헤드가 적다고 생긱한다. 하지만 Next.js 처럼 프레임워크에서 제공하는 기능들이 필요하다면 직접 구현해야 한다. 현재 과제에서는 최초 진입하는 페이지만 SSR/SSG 가 적용이 되는데, Next.js 에서는 프리페치를 통해 다른 페이지에서 필요한 데이터들을 미리 불러오는 것으로 알고 있다. 이런 부분에서 SSR/SSG 를 활용하는 효율이 이미 많은 기능이 구현된 프레임워크 보다 부족할 수 있을 것 같다.

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

빌드 후 생성된 SSG 페이지를 CDN 으로 배포해서 사용하기

코드 품질 향상

자랑하고 싶은 구현

SSR 을 위한 고차함수를 만들어 일관성과 재사용성을 높였습니다. 렌더링에 필요한 초기 데이터, query, 값을 함수로 전달해 서버에서 메모리를 공유하지 않도록 했습니다.

// vanilla
export const HomePage = withIsomorphicLifecycle(
  {
    ssr: () => {
      return ... // 초기 렌더링에 필요한 데이터 가져오기
    },
    metadata: () => {
      return {
        title: "쇼핑몰 - 홈", // 메타데이터 설정
      };
    },
    initStore: ({ data }) => {
      // window.__INIT_DATA__ 설정
    },
    ({ data, query }) => {
      // 전역 스토어를 사용하지 않고 필요한 값을 함수로 전달
      // 렌더링
    });

// react
export const HomePage = withServer(
  {
    ssr: async ({ query }): Promise<ServerResponse> => {
      return ...
    },
    metadata: async () => {
      return {
        title: "쇼핑몰 - 홈",
      };
    },
  },
  () => {
    // 렌더링
  });

react 에서는 컨텍스트를 사용해 요청마다 필요한 데이터를 격리시켰습니다.

// 클라이언트
function main() {
  const router = new Router(routes, BASE_URL);
  router.start();

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const initData: any = (window as any).__INITIAL_DATA__;

  const renderApp = () => {
    return (
      <RouterContext value={router}>
        <ProductProvider productStore={createProductStore(initData)}>
          <App />
        </ProductProvider>
      </RouterContext>
    );
  };
  ...
// 서버
export const render = async (url: string, query: Record<string, string>) => {
  console.log({ url, query });

  const router = new Router(routes);
  router.start(url);
  router.query = query;

  const { ssr = fallback, metadata = fallback } = router.target as unknown as ServerOptions;
  const params = { query, params: router.params };

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const data: any = (await ssr(params)) ?? {};
  const { title = "" } = (await metadata(params)) ?? {};

  const html = renderToString(
    <RouterProvider router={router}>
      <ProductProvider productStore={createProductStore(data)}>
        <App />
      </ProductProvider>
    </RouterProvider>,
  );
  ...

개선하고 싶은 부분

심화 과제에서 any 타입을 사용한 부분들이 있는데, any 를 사용하지 않고 구현되도록 개선할 수 있다면 좋을 것 같습니다. ssr 하는 과정에서 에러가 발생했을 때 html 을 응답하고 있지 않은데, 이 부분도 신경써서 개선하면 좋았을 것 같습니다.

학습 연계

다음 학습 목표

  • SSG 결과물을 CDN 으로 배포해보기
  • 서버리스 환경에서 SSR 사용해 보기

실무 적용 계획

새로 입사하는 회사에서는 React 를 사용할 것 같은데, 빌드 결과물을 CDN 으로 최적화 해 관리해보기

리뷰 받고 싶은 내용

'자랑하고 싶은 구현' 에 있는 코드가 각 요청별로 데이터를 격리해 사용하기 알맞은 패턴이라고 생각하시나요? 만약 다른 방법이 있다면 어떤 방법이 있을지도 궁금합니다

과제 피드백

고생하셨습니다 지훈님! 회고도 정말 잘 작성해주셨네요. 구현도 고차 함수 형태로 제한해서 인터페이스를 제한해 일관되게 사용할 수 있도록 하고 메모리 공유 방지도 함께 둔것도 좋은 것 같아요. 결국 저라도 비슷한 방식으로 구현했을 것 같고, 팩토리 패턴같이 그냥 해당 인스턴스별로 상태를 전달받는 방식도 가능하지 않았을까 싶긴한데요. 크게 중요하지는 않을것 같아요.

고생하셨고 마지막 주차도 잘 마무리하시길 바랍니다~