nemobim 님의 상세페이지[2팀 정도은] Chapter 4-1 성능 최적화 🧦🧦🧦🧦🧦

과제 체크포인트

배포 링크

바닐라: https://nemobim.github.io/front_6th_chapter4-1/vanilla/ 리액트: https://nemobim.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. HTML을 만든다.
  2. 서버에서 해당 경로에 접속시 기존에 데이터를 가져와서 같이 넣어주고 HTML을 뿌린다
  3. CSR/SSR을 사용하는 부분을 조건부 처리해서 서버 환경에서도 돌아가도록 만든다.

라고 처음에 생각했으나 결국 연결고리를 짓는 부분이 부족한지 how 에서 막혔습니다.

왜 SSR이 안 되지?

어찌저찌 가이드를 보고 따라 가봤을때는 어떤식으로 동작하는지 콘솔도 찍어보고 테스트 해봤습니다. 그런데 코드를 구현해도 SSR이 아주 잠깐 나왔다가 사라지고 CSR 코드가 나오더라구요,,

서버: <h1>🛍️ 쇼핑몰</h1>
↓ (JavaScript 로드 후)
클라이언트: <div>기존 쇼핑몰</div>

처음에는 버그인줄 알았으나 코드를 미구현해서 그랬고, 하이드레이션 과정을 빼먹었다는 걸 알게 되었습니다.

1. SSR ≠ 단순 HTML 생성 잘못된 이해: "서버에서 HTML만 만들면 SSR 끝" 올바른 이해: "서버 HTML + 클라이언트 Hydration = 완전한 SSR" 2. Hydration의 진짜 의미 잘못된 이해: "클라이언트에서 다시 렌더링하기" 올바른 이해: "서버 HTML을 살려두고 JavaScript 기능만 붙이기"

어쩔때는 성공하고 어쩔때는 실패하는 e2e

싱글톤 라우터로 인한 동시성 문제의 심각성을 깨달았습니다. 라우터를 하나의 인스턴스로 만들어놨더니 여러 요청이 동시에 들어올 때 서로 다른 요청의 URL이 뒤섞이는 문제가 발생했습니다...클라이언트에서는 한 사용자당 하나의 라우터로 충분하지만 서버에서는 각 요청마다 별도의 라우터 인스턴스를 생성해야 상태가 섞이지 않는다는 걸 배웠습니다.

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

  • SSR과 SSG의 차이점

    • SSR은 요청할 때마다 서버에서 실시간으로 HTML을 생성
    • SSG는 빌드 타임에 미리 모든 페이지를 정적 파일로 만들어두는 방식
  • Universal JavaScript 패턴

    • 같은 코드가 서버와 클라이언트에서 다르게 동작해야 한다.
    • typeof window === "undefined" 체크가 코드 곳곳에 필요함
    • localStorage나 DOM API 같은 브라우저 전용 기능들을 서버에서 대체 필요
  • ** useSyncExternalStore의 getServerSnapshot**

    • React 18에서 서버-클라이언트 상태 동기화의 핵심
    • 세번째 인자로 서버 환경에서의 초기 상태를 제공해야 Hydration 불일치를 방지

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

SSG로 생성된 정적 파일들을 서버 처리 과정 없이 바로 HTML 파일을 전송할 수 있어서 응답 시간이 빠르다! SSR로 초기 데이터를 미리 받으니까 사용자 경험에 좋다!

학습 갈무리

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

  • 요청별 상태 격리 시스템 구축
    • 현재 싱글톤 라우터로 인한 동시성 문제를 해결하기 위해 요청별로 독립적인 컨텍스트를 만드는 시스템이 필요
    • 각 요청마다 격리된 라우터, 스토어 인스턴스를 생성하도록 개선

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

모르겠어요...AI 돌렸습니다.

  • 파일 시스템 접근 제거: 현재 템플릿 파일을 fs.readFile로 읽어오는데, 엣지 환경에서는 파일 시스템이 없으므로 템플릿을 환경변수나 KV 스토리지에 저장하거나, 번들에 인라인으로 포함시켜야 합니다.
  • Cold Start 최적화: 번들 크기를 최소화하기 위해 dynamic import를 적극 활용하고, 자주 사용되는 코드만 메인 번들에 포함시키겠습니다. 또한 워커 인스턴스 간 상태 공유가 불가능하므로 모든 상태를 요청 스코프로 관리해야 합니다.
  • Web API 호환성: Node.js 전용 모듈들(fs, path 등)을 Web API 기반으로 대체해야 합니다. Buffer 대신 Uint8Array, require 대신 import를 사용하고, 서버 환경 감지도 typeof process !== 'undefined' 방식으로 변경해야 합니다.

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

  • renderToString CPU 병목:
    • 현재 동기적 렌더링으로 인해 CPU 사용량이 높은데 Worker Thread를 활용한 병렬 렌더링이나 렌더링 결과 캐싱을 도입 -자주 요청되는 페이지는 Redis에 렌더링 결과를 저장하여 CPU 부하를 줄이기
  • 메모리 누수 방지:
    • 싱글톤 패턴으로 인한 메모리 누적 문제를 해결하기 위해 요청 완료 후 명시적으로 인스턴스를 정리하는 가비지 컬렉션 트리거를 추가
    • 대용량 데이터 처리 시 스트리밍 방식을 도입하여 메모리 사용량을 제어
  • 정규식 라우터 최적화

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

  • 메모리 사용량 관리
    • 한 번에 1000개를 다 만들려고 하면 컴퓨터 메모리가 부족하다.
    • 100개씩 나누어서 만들고, 100개 만들 때마다 메모리를 비워주는 방식으로 진행
    • 순차 처리 방식을 Worker Thread 기반 병렬 배치 처리로 개선
  • 증분 빌드 시스템
    • 변경된 상품만 재생성하는 시스템을 구축
    • 상품 데이터의 해시값을 저장해두고 변경 감지 시에만 해당 페이지를 재빌드하여 전체 빌드 시간을 대폭 단축
  • CDN 캐시 무효화 전략
    • 상품별로 태그를 부여하여 특정 상품 변경 시 해당 페이지만 선별적으로 캐시를 무효화

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

  • 인터랙션 차단 문제
    • 전체 앱이 Hydration 완료될 때까지 모든 버튼이 비활성화
    • React 18의 Selective Hydration을 도입하여 우선순위가 높은 컴포넌트(장바구니 버튼, 검색 등)부터 순차적으로 활성화 필요
  • 로딩 상태 시각화
    • Hydration 진행 상태를 사용자가 알 수 있도록 스켈레톤 UI와 프로그레시브 로딩 인디케이터를 추가

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

  • 얼마나 빠른지, 에러가 얼마나 나는지 계속 지켜보기.. 모니터링
  • window.__INITIAL_DATA__에 사용자 입력 데이터가 포함될 때 XSS 공격을 방지하기 위한 HTML 이스케이핑과 데이터 sanitization이 필수
  • Content Security Policy(CSP) 설정으로 외부 스크립트 실행을 제한하고 서버 에러 시 스택 트레이스나 민감한 정보가 클라이언트에 노출되지 않도록 에러 응답을 필터링

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

장점 - 내 마음대로 할 수 있음: 원하는 대로 정확히 만들 수 있습니다. 그리고 문제 생겼을 때 어디가 잘못됐는지 알 수 있어요.

단점 - 너무 힘들고 시간 많이 걸림: 개발 및 유지보수 비용이 매우 높습니다. Next.js가 자동으로 처리해주는 코드 스플리팅, 이미지 최적화, 자동 Static 최적화 등을 모두 직접 구현해야 하고 새로운 React 버전이나 웹 표준 변화에 대응하는 부담도 큼...

결론: 학습 목적이나 매우 특수한 요구사항이 있는 경우가 아니라면 Next.js 같은 검증된 프레임워크를 사용하는 것이 좋다. 다만 이번 구현 경험을 통해 Next.js를 사용할 때도 내부 동작을 이해하고 더 효과적으로 활용할 수 있게 된거같다(?)

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

  • 인기 상품만 미리 만들고 나머지는 사용자가 처음 방문할 때 만들도록 설정
  • 나머지는 fallback: 'blocking'으로 설정하여 사용자가 처음 방문할 때 만들도록 한다.
  • ISR(Incremental Static Regeneration) 활용
    • 옵션을 설정하여 페이지를 주기적으로 업데이트할 수 있다. 상품 정보는 하루에 한 번, 가격 정보는 1시간마다 갱신

코드 품질 향상

자랑하고 싶은 구현

솔직히 자랑할 만한 코드는 없었습니다... 과제 완성에 급급해서 "일단 돌아가게만 만들자"는 마음으로 구현했습니다..

개선하고 싶은 부분

  • 싱글톤 패턴 라우터의 동시성 문제: -가장 큰 문제였던 싱글톤 라우터를 요청별 인스턴스로 변경

    • 현재는 여러 요청이 들어올 때 라우터 상태가 뒤섞여서 A 사용자가 B 사용자의 페이지를 보는 심각한 버그가 발생합니다.
  • any 타입 남발:

    • 시간에 쫓겨서 타입 정의를 제대로 하지 못하고 any로 때려박은 부분들이 많다
  • 중복된 Mock 코드:

  • 기존 API와 동일한 인터페이스를 유지하기 위해 serverMock을 따로 만들었는데 코드 중복이 심하고 유지보수가 어려움

  • 추상화를 통해 공통 로직 뽑아내기

리팩토링 계획

  • 과제 하느라 촉박해서 타입을 any 로 때려박은 부분이있음
  • mock을 사용하기 위해 기존코드와 동일한 serverMock을 따로 만듦..

학습 연계

다음 학습 목표

있는거나 잘하겠습니다..!

실무 적용 계획

CSR며이며 데이터가 잘 바뀌지 않는 프로젝트를 찾아 SSR로 처리해보자..1

리뷰 받고 싶은 내용

  • 서버와 클라이언트에서 같은 코드를 실행하면서 환경별 분기 처리가 너무 많아졌는데, 더 깔끔하게 추상화할 수 있는 패턴이 있는지 궁금합니다.
  • 학습연계로 뭘하면 좋을까요

과제 피드백

안녕하세요 도은님!! 우겨곡절 끝에 결국 해내셨군요 ㅎㅎ 고생하셨습니다!!

싱글톤 라우터로 인한 동시성 문제의 심각성을 깨달았습니다. 라우터를 하나의 인스턴스로 만들어놨더니 여러 요청이 동시에 들어올 때 서로 다른 요청의 URL이 뒤섞이는 문제가 발생했습니다...클라이언트에서는 한 사용자당 하나의 라우터로 충분하지만 서버에서는 각 요청마다 별도의 라우터 인스턴스를 생성해야 상태가 섞이지 않는다는 걸 배웠습니다.

맞아요.. 이게 과제여서 다행이라고 생각합니다 ㅎㅎ 실제 서비스에서 동일한 문제가 발생한다면 정말 심각한 문제가 될 수 있어요 ㅠㅠ 다른 사용자의 개인정보를 갑자기 렌더링 한다거나, 의도치않은 계정 탈취를 한다거나...? 이런 류의 문제가 발생할 수 있답니다.

서버와 클라이언트에서 같은 코드를 실행하면서 환경별 분기 처리가 너무 많아졌는데, 더 깔끔하게 추상화할 수 있는 패턴이 있는지 궁금합니다.

이 부분은 과제 솔루션을 참고해보시면 좋을 것 같아요! 환경별 분기처리는 아마 window와 연관된 부분 (router, storage 등) 일텐데, 별도의 구현체를 만들어서 사용하면 될 것 같아요. 보통 "전략 패턴" 이라고 하는데요, 인터페이스는 동일하고 각 인터페이스에 대한 구현체가 여러개 있어서 바꿔낄 수 있도록 하는거죠!

학습연계로 뭘하면 좋을까요

NextJS 만들어보기 시도해보는거 추천드립니다 ㅋㅋ 아니면 저의 항해 블로그에 기여해본다거나!? SEO 관련 이슈가 추가되어있는 상태랍니다 ㅎㅎ