과제 체크포인트
배포 링크
vanilla: https://hanghae-plus.github.io/front_6th_chapter4-1/vanilla/ react: https://hanghae-plus.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. 서버-클라이언트 상태 동기화 문제
문제 상황:
- 서버에서 렌더링한 상품 데이터가 클라이언트에서 다시 로딩되면서 화면이 깜빡이는 현상
window.__INITIAL_DATA__가 제대로 전달되지 않아 클라이언트에서 빈 상태로 시작
해결 과정:
- 문제 진단: 브라우저 개발자 도구에서
window.__INITIAL_DATA__확인 →undefined발견 - 원인 분석: 서버에서 스크립트 주입 위치 문제 (
</head>태그 찾지 못함) - 해결책 적용:
// 서버에서 안전하게 스크립트 주입
const initialDataScript = Object.keys(initialData || {}).length > 0
? `<script>window.__INITIAL_DATA__ = ${JSON.stringify(initialData).replace(/</g, "\\u003c")};</script>`
: "";
const finalHtml = template
.replace("<!--app-html-->", html)
.replace("<!--app-head-->", head)
.replace("</head>", `${initialDataScript}</head>`); // 핵심 수정
2. Universal 코드 작성 - window 객체 참조 에러
문제 상황:
ReferenceError: window is not defined (서버 렌더링 시)
해결 과정:
- 에러 발생 지점 추적:
useLoadProductDetail훅에서 서버 실행 시 에러 - 환경 분기 로직 추가:
export const useLoadProductDetail = () => {
const productId = useRouterParams((params) => params.id);
useEffect(() => {
if (typeof window === "undefined") return; // 핵심 해결
if (!productId) return;
loadProductDetailForPage(productId);
}, [productId]);
};
- 검증: 서버와 클라이언트 모두에서 정상 동작 확인
3. Hydration 불일치 - React Hydration Warning
문제 상황:
Warning: Text content did not match. Server: "" Client: "상품명"
Warning: An error occurred during hydration. The server HTML was replaced with client content.
해결 과정:
- 원인 분석: 서버에서는 빈 상태로 렌더링, 클라이언트에서는 데이터가 있는 상태로 렌더링
- 서버 렌더링 강화:
// main-server.tsx - 서버에서 실제 데이터 로딩
const product = await getProduct(productId); // 실제 API 호출
initialData.currentProduct = product;
// 스토어에 데이터 주입
productStore.dispatch({
type: PRODUCT_ACTIONS.SET_INITIAL_DATA,
payload: initialData,
});
구현하면서 새롭게 알게 된 개념
-
서버-클라이언트 상태 동기화의 복잡성
- 서버에서 생성한 상태를 클라이언트에서 그대로 복원하는 것의 어려움
- 비동기 데이터 로딩과 동기식 렌더링 간의 타이밍 이슈
-
Hydration 불일치 문제와 해결법
- 서버 렌더링 HTML과 클라이언트 첫 렌더링이 다를 때 발생하는 문제
window객체 존재 여부 체크의 중요성
-
SSG와 SSR의 트레이드오프
- SSG: 빌드 시간 ↑, 런타임 성능 ↑, 동적 데이터 ↓
- SSR: 빌드 시간 ↓, 런타임 성능 ↓, 동적 데이터 ↑
-
Universal 코드 작성의 어려움
- 동일한 컴포넌트가 서버와 브라우저에서 모두 실행되어야 함
- DOM API, window 객체 등 브라우저 전용 API 사용 제한
성능 최적화 관점에서의 인사이트
- 프리페칭 전략: 상품 상세 + 관련 상품을 병렬로 로딩하여 사용자 경험 개선
- 번들 사이즈 최적화: 서버 전용 빌드와 클라이언트 빌드 분리
- 캐싱 활용: Express 압축 미들웨어로 응답 크기 최소화
학습 갈무리
Q1. 현재 구현한 SSR/SSG 아키텍처에서 확장성을 고려할 때 어떤 부분을 개선하시겠습니까?
- 컴포넌트 레이지 로딩: React.lazy()를 활용한 코드 스플리팅
- 마이크로 프론트엔드: 기능별 독립 배포 가능한 구조로 분리
- CDN 통합: 정적 자산을 CloudFront, CloudFlare 등으로 분산
- 캐싱 레이어: Redis를 활용한 렌더링 결과 캐싱
Q2. Express 서버 대신 다른 런타임(Cloudflare Workers, Vercel Edge Functions 등)을 사용한다면 어떤 점을 수정해야 할까요?
Vercel Edge Functions 적용 시:
fs모듈 → Vercel의 파일 시스템 API로 교체- Express 미들웨어 → Edge Runtime API로 변환
- 세션 스토리지 → 외부 데이터베이스 연동 필요
Cloudflare Workers 적용 시:
- Node.js 전용 모듈들 → Web API로 대체
- Cold start 최적화를 위한 번들 사이즈 최소화
- KV Storage를 활용한 캐싱 전략 도입
Q3. 현재 구현에서 성능 병목이 될 수 있는 지점은 어디이고, 어떻게 개선하시겠습니까?
병목 지점:
- 동기식 데이터 로딩: 상품 상세 + 관련 상품을 순차 처리
- 메모리 누수 위험: 스토어 상태가 서버에서 계속 누적될 수 있음
개선 방안:
- Promise.all()로 병렬 데이터 로딩 (이미 일부 적용됨)
- 요청별 스토어 인스턴스 격리
Q4. 1000개 이상의 상품 페이지를 SSG로 생성할 때 고려해야 할 사항은 무엇입니까?
- 증분 빌드: 변경된 상품만 재생성하는 시스템
- 병렬 처리: Worker Threads를 활용한 멀티코어 활용
- 압축 최적화: Gzip, Brotli 압축으로 파일 크기 최소화
- 빌드 캐시: 이전 빌드 결과 재사용으로 속도 향상
Q5. Hydration 과정에서 사용자가 느낄 수 있는 UX 이슈는 무엇이고, 어떻게 개선할 수 있을까요?
현재 UX 이슈:
- SSR 실패 시 빈 화면이 잠깐 나타날 수 있음
- 초기 데이터 로딩 중 스켈레톤 UI 부재
개선 방안:
- 점진적 Hydration: React 18 Suspense 활용
- 스켈레톤 UI: 로딩 상태 시각적 피드백
- Error Boundary: 에러 발생 시 사용자 친화적 메시지
- 로딩 인디케이터: 데이터 페칭 중 명확한 상태 표시
Q6. 이번 과제에서 학습한 내용을 실제 프로덕션 환경에 적용할 때 추가로 고려해야 할 사항은?
보안:
- HTTPS 강제, CSP 헤더 설정
- 입력값 검증 및 XSS 방지 강화
- 민감 정보 로그 필터링
모니터링:
- APM 도구 연동 (DataDog, New Relic)
- 에러 추적 시스템 (Sentry)
- 성능 모니터링 대시보드
배포:
- Blue-Green 배포 전략
- 롤백 시나리오 준비
- 헬스체크 엔드포인트 구현
Q7. Next.js 같은 프레임워크 대신 직접 구현한 SSR/SSG의 장단점은 무엇인가요?
직접 구현의 장점:
- 완전한 제어권: 모든 렌더링 로직 커스터마이징 가능
- 깊은 이해: SSR/SSG 원리를 정확히 이해하고 활용
- 유연한 최적화: 특정 요구사항에 맞는 세밀한 튜닝
- 가벼운 번들: 필요한 기능만 포함하여 오버헤드 최소화
직접 구현의 단점:
- 개발 시간: 다시 개발하는 시간 소요
- 안정성 부족: 검증되지 않은 코드로 인한 예상치 못한 버그
- 유지보수 부담: 프레임워크 업데이트, 보안 패치 등을 직접 관리
- 생태계 부족: 플러그인, 도구, 커뮤니티 지원 제한
Q8. Next.js 를 이용하여 SSG 방식으로 배포하려면 어떻게 해야 좋을까요?
// next.config.js
module.exports = {
output: 'export', // 정적 내보내기 활성화
trailingSlash: true, // 경로 호환성
images: {
unoptimized: true // 이미지 최적화 비활성화 (정적 배포 시)
}
}
// pages/product/[id].js - 동적 라우팅
export async function getStaticPaths() {
const products = await fetchAllProducts();
const paths = products.map(product => ({
params: { id: product.id.toString() }
}));
return {
paths,
fallback: false // 사전 생성된 페이지만 제공
};
}
export async function getStaticProps({ params }) {
const product = await fetchProduct(params.id);
const relatedProducts = await fetchRelatedProducts(params.id);
return {
props: { product, relatedProducts }
};
}
코드 품질 향상
자랑하고 싶은 구현
// SSR 실패 시 안전한 폴백
render = () => ({
html: '<div id="root"></div>', // 안전하게 비운 상태로 내려주기
head: "<title>쇼핑몰 - 홈</title>",
initialData: {},
});
2. XSS 방지 데이터 직렬화
// 보안을 고려한 JSON 이스케이프
const jsonString = JSON.stringify(data)
.replace(/</g, "\\u003c") // XSS 방지
.replace(/>/g, "\\u003e")
.replace(/&/g, "\\u0026");
개선하고 싶은 부분
현재 한계점
- 캐싱 전략 부족: Redis 같은 외부 캐시 없이 매번 데이터 재생성
- SEO 최적화 미완성: Open Graph, 구조화된 데이터 미적용
- 에러 모니터링 부재: Sentry 같은 에러 추적 시스템 없음
- 성능 모니터링 부족: 렌더링 시간, 메모리 사용량 추적 부재
리팩토링 계획
- 에러 경계(Error Boundary) 컴포넌트 추가
- 서버 로그 구조화 (Winston, Pino 등)
- TypeScript 타입 안정성 강화
리팩토링 계획
- 컴포넌트 레이지 로딩: React.lazy()를 활용한 코드 스플리팅
- 마이크로 프론트엔드: 기능별 독립 배포 가능한 구조로 분리
- CDN 통합: 정적 자산을 CloudFront, CloudFlare 등으로 분산
- 캐싱 레이어: Redis를 활용한 렌더링 결과 캐싱
학습 연계
다음 학습 목표
- 성능 최적화: Lighthouse 점수 90+ 달성
- 에러 처리: Error Boundary + 전역 에러 핸들링
- SEO 강화: 메타태그, 구조화된 데이터, 사이트맵
실무 적용 계획
- 랜딩페이지, 상품 상세페이지 SSR 적용 (SEO 중요도 기준)
- Core Web Vitals 추적 시스템 구축 및 Lighthouse CI 통합
- 데이터 프리페칭, 컴포넌트 레이지 로딩 등 성능 최적화 기법 적용
- 문서화
리뷰 받고 싶은 내용
- 초반에 admin 페이지로 next.js를 셋팅했는데, npm run dev를 하게 되면 react에 비해 핫 릴로딩이 너무 느려서 답답했는데, 이걸 개선시킬 수 없을까 궁금한데... 이건 셋팅의 문제인건지 아니면 전반적인 코드의 개선이 필요한건지 아니면 nextjs이기때문에 어쩔수 없는건지 궁금합니다.
과제 피드백
민지님 고생하셨습니다 ㅎㅎ 심화, 기본 과제 다 잘 마무리 해주셨네요 회고 잘 작성해주셨고 읽어보니 필요한 부분들에 대해서도 잘 정리해주신 것 같아요. 결국 이런 내용을 기반으로 설계단계에서 앱에 대해 적절한 렌더링 전략을 선택하는 중요한 내용이니 잘 기억해두시면 좋을 것 같습니다.
초반에 admin 페이지로 next.js를 셋팅했는데, npm run dev를 하게 되면 react에 비해 핫 릴로딩이 너무 느려서 답답했는데, 이걸 개선시킬 수 없을까 궁금한데... 이건 셋팅의 문제인건지 아니면 전반적인 코드의 개선이 필요한건지 아니면 nextjs이기때문에 어쩔수 없는건지 궁금합니다.
사실 이게 next.js의 매우 오래된 문제이긴한데요 ㅎㅎ (사실 지금이 과거에 비해 매우 빨라진겁니다) 내부에서 너무 많은 작업들을 수행해주고 있다 보니 어쩔 수 없는 부분인 것 같아요. 개발 서버에 있어서 몇몇 최적화를 제외하고 개발을 하시는 분들도 많이 있으신데 참고해서 필요 없는 부분은 하나씩 제외시키면서 하시면 빠르지 않을까 싶습니다! (실제로 이런 부분들로 인해서 + 빌드 시간 문제로 아예 Next.js를 걷어내시는 분들도 많이 있어요)
고생하셨고 마지막주차도 화이팅입니다!