yuhyeon99 님의 상세페이지[5팀 김유현] Chapter 4-1 성능 최적화

과제 체크포인트

배포 링크

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

구현 과정 돌아보기

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

Express SSR 서버와 프로덕션/개발 분기 처리 개발 모드에서는 Vite 미들웨어를 붙여야 하고, 프로덕션 모드에서는 sirvcompression으로 정적 자산을 서빙해야 했습니다. BASE 경로를 잘못 처리하면 정적 자산이 깨지거나 404가 발생하는 문제가 있었는데, process.env.BASE를 기준으로 경로를 일관되게 관리해 해결했습니다.

SSR 렌더링과 초기 데이터 주입 render 함수에서 반환한 initialData를 HTML에 주입할 때, 서버 직렬화 결과와 클라이언트 역직렬화 결과가 달라 Hydration 경고가 발생했습니다. JSON.stringify를 사용하고 window.__INITIAL_DATA__ 스크립트에 삽입하여 서버와 클라이언트 데이터 구조를 일치시켜 문제를 해결했습니다.

Router 파라미터 매칭 구현 ServerRouter에서 /product/:id/ 경로를 처리할 때 정규식 변환이 제대로 되지 않아 파라미터가 누락되는 문제가 있었습니다. 세그먼트를 ([^/]+)로 치환하고 캡처 결과를 params 객체에 매핑하는 로직을 추가하여 안정적으로 동작하도록 수정했습니다.

SSG 빌드 과정 최적화 고민 getProducts API를 통해 가져온 전체 상품 페이지를 /product/:id/ 형태로 SSG로 생성하는 과정에서 Promise.all로 처리하니 메모리 사용량이 급격히 늘었습니다. for 루프를 이용한 순차 처리로 안정성을 확보했고, 추후 증분 빌드와 배치 처리를 고려할 계획입니다.


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

  • Express와 Vite SSR 연동: 개발 모드에서는 viteDevServer.ssrLoadModule을 사용하고, 프로덕션 모드에서는 빌드된 main-server.js를 로드해 동일한 render 함수를 재사용할 수 있다는 점을 배웠습니다.
  • 스토리지 추상화 필요성: SSR 환경에서 localStorage를 직접 사용할 수 없으므로, 메모리 기반 스토리지를 제공하는 createStorage를 만들어 서버/클라이언트 양쪽에서 동일한 인터페이스로 접근 가능하게 했습니다.
  • 페이지 라이프사이클 관리: withLifecycle HOC를 통해 CSR 환경에서만 onMount, onUnmount, watches를 실행하도록 구성하여 불필요한 SSR 실행을 막을 수 있었습니다.
  • 정적 페이지 생성 흐름: ssg.js 스크립트에서 Vite SSR 모듈 로딩으로 동일한 렌더러를 활용해 /, /404, /product/:id/ 경로의 HTML을 파일 시스템에 저장하는 방식을 직접 구현했습니다.

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

  • 초기 로드 속도 개선: SSR 적용으로 CSR에서 API 요청을 기다리지 않고 즉시 HTML이 렌더링되어 FCP가 단축되었습니다.
  • 정적 파일 캐싱 활용: SSG로 생성된 HTML은 CDN 캐싱과 결합해 빠른 응답과 서버 부하 감소를 달성할 수 있습니다.
  • API 호출 중복 최소화: 서버에서 retrieveProductsretrieveProductDetails로 데이터를 미리 가져와 주입했기 때문에 클라이언트에서 동일한 데이터를 재요청하지 않아 네트워크 효율이 높아졌습니다.
  • 빌드 시 자원 사용 관리: 대량 상품 페이지를 한꺼번에 빌드하면 시간이 길어지고 메모리 점유율이 높아졌습니다. 순차 처리로 완화했으며, 향후 병렬 처리와 증분 빌드로 최적화할 수 있습니다.

학습 갈무리

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

  • 대규모 트래픽 대응을 위한 캐싱 계층과 로드 밸런싱을 추가해야 합니다.
  • CDN과 CI/CD 파이프라인을 연동해 배포 안정성을 강화할 수 있습니다.

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

  • 파일 시스템 접근(fs.readFileSync)을 사용할 수 없으므로 템플릿을 다른 방식으로 불러와야 합니다.
  • 서버리스 환경에서 Cold Start가 문제가 될 수 있어 초기화 비용을 최소화해야 합니다.
  • Edge 환경 특성상 API 호출과 캐싱 전략을 단순화해야 합니다.

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

  • SSR 시 매 요청마다 API 호출이 발생해 서버 부하가 커질 수 있음 → 캐싱 레이어 도입.
  • 모든 상품 페이지를 한 번에 SSG로 생성하면 빌드 시간이 길어짐 → 증분 빌드 도입.
  • 클라이언트 번들 크기가 커지면 초기 로드 속도가 늦어짐 → 코드 스플리팅 및 Tree Shaking 활용.

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

  • 병렬 처리와 배치 생성으로 빌드 시간을 최적화해야 합니다.
  • 메모리 사용량이 급격히 늘어날 수 있어 페칭과 렌더링을 스트리밍 방식으로 개선해야 합니다.
  • CDN 캐시 무효화 전략을 마련해야 합니다.

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

  • Hydration 중 사용자가 인터랙션을 못하는 시간이 발생함 → Skeleton UI와 Progressive Enhancement로 개선.
  • 초기 데이터가 일치하지 않으면 화면이 깜빡임 → 서버와 클라이언트 직렬화 구조를 통일해야 함.

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

  • SSR 오류 모니터링 및 로깅 체계를 도입해야 합니다.
  • 에러 발생 시 Fallback 페이지 제공 필요.
  • SEO 최적화와 보안(XSS, CSP 정책) 고려.

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

  • 장점: 동작 원리를 깊게 학습할 수 있고, 자유롭게 커스터마이징 가능.
  • 단점: 유지보수와 안정성 확보에 비용이 크며, 커뮤니티 지원이 부족.

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

  • getStaticPropsgetStaticPaths를 활용해 빌드 타임에 페이지를 생성합니다.
  • Vercel 같은 플랫폼을 사용하면 CDN 캐싱과 Edge 배포를 자동으로 지원받을 수 있습니다.

코드 품질 향상

자랑하고 싶은 구현

const tokenized = path.replace(/:\w+/g, (seg) => {
  names.push(seg.slice(1));
  return "([^/]+)";
});

정규식 기반으로 라우터를 구현해 /product/:id/ 같은 동적 라우트를 처리할 수 있도록 했습니다. 서버와 클라이언트 라우터를 동일한 방식으로 동작시켜 Hydration 불일치 문제를 줄였습니다.

개선하고 싶은 부분

  • 에러 처리를 단순히 try-catch로만 구성했습니다. HTTP 상태 코드에 따라 다른 Fallback 페이지를 제공하는 구조로 개선하고 싶습니다.
  • 대규모 상품 데이터 처리 시 Promise.all로 한꺼번에 처리하지 못하고 있어, 스트리밍이나 배치 로직을 도입할 필요가 있습니다.

리팩토링 계획

  • 라우터 로직을 함수형으로 단순화하고 테스트 가능한 구조로 리팩토링할 예정입니다.
  • 타입 안전성을 강화하기 위해 TypeScript 제네릭을 도입하고 싶습니다.
  • 스토리지 추상화(createStorage)와 라이프사이클 HOC(withLifecycle)를 별도 모듈로 분리하여 재사용성과 가독성을 높이고자 합니다.

학습 연계

다음 학습 목표

  • Serverless 환경에서 SSR 최적화 방법 학습.
  • 웹 컴포넌트와 SSR 호환성 연구.

실무 적용 계획

  • 회사 블로그를 SSG 기반으로 마이그레이션해 SEO 및 성능 개선.

리뷰 받고 싶은 내용

  • “관련 지식이 0”이라는 가정 하에, SSR/SSG를 이해하고 구현할 때 꼭 알아야 하는 최소 개념은 어떤 게 있을까요?
    • (예: 서버 렌더링이란? Hydration이란? 정적 빌드란?)

과제 피드백

고생하셨습니다 유현님! 아쉽게 테스트 두개가 모두 통과하지 못했고, 별도 배포링크가 함께 작성되어 있지 않아서 불합격 처리 했습니다.

“관련 지식이 0”이라는 가정 하에, SSR/SSG를 이해하고 구현할 때 꼭 알아야 하는 최소 개념은 어떤 게 있을까요? (예: 서버 렌더링이란? Hydration이란? 정적 빌드란?)

우리가 저 개념을 실제 활용할 때는 이미 구현되어있는 라이브러리를 선택하게 되는 경우가 많으니, 실제 구현적인 개념을 처음부터 깊게 적용하기 보다는 정리하신 것처럼 언제 사용하는게 적합한지와 발생할 수 있는 부가적인 운영 비용이 어떻게 되는지 정도 관점만 가지고 있어도 괜찮아요! 이때 기본적인 컨셉 용어, 말씀해주신 서버렌더링이나 하이드레이션 정적빌드 등에 대한 기본적인 내용도 확장해서 일단 알고 있는 정도로 공부를 하면 충분한 것 같습니다.

그리고 여유가 있다면 조금 더 깊게 들어가서 어떤 방식으로 구현이 되는지, 내부 컨셉에서 얻을 수 있는 아이디어가 뭐가 있는지 정도 공부해보면 충분하지 않을까 싶습니다. (주로 Next를 쓸텐데 각 Next에서 데이터를 페칭하는 방식들에 대해서도 공부를 한다면 기본적인 개념은 자연스럽게 생기지 않을까! 싶습니다)

고생하셨고 마지막주도 화이팅입니다!

최소 개념보다는 각각의 렌더링 방식에 대해