과제 체크포인트
배포 링크
https://yangs1s.github.io/front_6th_chapter4-1/react/ https://yangs1s.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 불일치 방지
- 클라이언트 상태 복원
Static Site Generation
- 동적 라우트 SSG (상품 상세 페이지들)
- 빌드 타임 페이지 생성
- 파일 시스템 기반 배포
구현 과정 돌아보기
가장 어려웠던 부분과 해결 과정
플레이스홀더 없음/이미 채워짐 플레이스홀더 없음/이미 채워짐
### 해결 과정
outputPath 경로 문제로 착각하고 경로 수정에만 집중했고, 초기 렌더링 검증 테스트도 함께 실패하기 시작, 여러 경로 할당하는 방식을 하드코딩으로도 해보고, 철자가 틀렸는지, 에디터에서 경로 복사해오기 해서도 해보고 여러모로 해도 안됐음.
### 해결
AI를 이용, 템플릿 파일 자체에 문제가 있다는 것을 깨달음, 템플릿 경로를 dist안에 있는 index.html로 파일로 가져옴
```jsx
// ✅ 해결: 원본 템플릿 파일 사용
const template = fs.readFileSync("./index.html", "utf-8");
원본 파일로 써야하는 이유를 AI를 통해서 찾아봄
SSG 템플릿의 조건
- 플레이스홀더 필수: , 등이 있어야 함
- 미처리 상태: 빌드 과정을 거치지 않은 원본 템플릿이어야 함
- 동적 치환 가능: SSR 결과물로 플레이스홀더를 교체할 수 있어야 함
빌드된 파일을 템플릿으로 쓰면 안 되는 이유
- 이미 특정 페이지(보통 홈페이지) 내용으로 채워져 있음
- 플레이스홀더가 실제 내용으로 대체된 상태
- 새로운 페이지 내용을 주입할 여지가 없음
SSR 검색·필터링 테스트
문제
SSR 검색·필터 테스트에서 #search-input 값이 ''로 SSR되어 toHaveValue("젤리")가 타임아웃으로 실패했습니다.
원인 파악 과정
- 처음엔 왜 빈 문자열인지 정확히 못 잡았고, “서버에 쿼리를 저장한다”는 관점 자체를 놓침 → AI 도움 요청.
- Router.js에서 전역 라우터에는 SSR 요청별 쿼리를 보존하지 않음(브라우저 의존) 이걸 보존할수있게 저장해야한다는걸 파악.
해결:
- useRouterQuery를 컨텍스트 라우터(RouterProvider) 사용으로 변경.
- Router.ts에 세터 추가후 서버에서 쿼리를 보존 할수있게 수정.
컨텍스트 사용
요청 전용 라우터를 컨텍스트로 만들었습니다. 요청 전용 라우터를 컴포넌트에 안전하게 전달하기 위해서 사용해야해서 컨텍스트를 이용했습니다.
구현하면서 새롭게 알게 된 개념
하이드레이션
서버사이드 렌더링은 웹 페이지를 서버에서 미리 렌더링하여 클라이언트에게 HTML을 전달하는 방식이다. 이때 클라이언트 측에서 자바스크립트를 통해 추가적인 기능(예: 이벤트 처리, 상태 관리 등)을 활성화하는 과정을 하이드레이션이라고 한다.
왜 하이드레이션이 필요한가?
- 빠른 초기 로드:
- 서버사이드 렌더링을 통해 브라우저는 즉시 HTML을 표시할 수 있어 초기 로드 속도가 빠르다.
- 사용자는 화면이 빠르게 표시되는 경험을 할 수 있다.
- 동적 상호작용 가능:
- 서버가 렌더링한 HTML만으로는 버튼 클릭, 폼 제출 등 동적 상호작용이 불가능하다.
- 하이드레이션 과정에서 자바스크립트가 로드되고 이벤트 리스너가 부착되어 동적 기능이 활성화된다.
- SEO:
- 서버사이드 렌더링된 HTML은 검색 엔진 크롤러가 쉽게 읽을 수 있어 SEO에 유리하다.
하이드레이션의 동작 방식
- 서버사이드 렌더링 (SSR):
- 서버에서 미리 렌더링된 HTML이 클라이언트에게 전달됨.
- 이 HTML은 이미 완성된 구조를 가지고 있으나, 자바스크립트 로직은 아직 적용되지 않은 상태임.
- 하이드레이션:
- 클라이언트 측에서 자바스크립트가 로드됨.
- 자바스크립트는 기존의 HTML과 연결되어 이벤트 리스너, 상태 관리, 기타 동적 기능을 활성화함.
- 이 과정을 통해 서버에서 받은 정적인 HTML이 클라이언트 측에서 동적인 웹 애플리케이션으로 변환됨.
구현을 통해서 SSR에서는 아무런 움직임을 줄수 없다는점? CSR 스크립트를 통한 기능을 활성화 시킬수 있다는점을 알았고, 하이드레이션 과정에서 클라이언트와 서버의 구조가 일치해야한다는걸 알게되었어요
서버 상태관리 초기화를 하는 이유
얻는 이점
1. 사용자 경험 (UX) 개선
❌ Before: 상품목록 → 로딩스피너 → 상품목록 (깜빡임) ✅ After: 상품목록 → 상품목록 (매끄러움)
2. 성능 개선
- 중복 API 호출 방지: 서버에서 이미 가져온 데이터를 재사용
- 네트워크 트래픽 감소: 불필요한 요청 제거
- 빠른 인터랙션: 데이터가 이미 있어서 즉시 반응
3. SSR의 이점 유지
- SEO: 서버에서 렌더링된 HTML로 검색엔진 최적화
- 초기 로딩 속도: JavaScript 없어도 콘텐츠 표시
- Progressive Enhancement: JS 실패해도 기본 기능 동작
4. 일관성 보장
처음 구현시 어색해서 csr인건지 아님 ssr인건지 잘 몰랐고, 화면 깜빡임과, 중복으로 api가 호출되고 이래서 찾아보다가 이해했습니다.
성능 최적화 관점에서의 인사이트
사실 어떤 관점으로 생각해야는지 잘 모르겠습니다. https://medium.com/wantedjobs/%EC%9B%B9-%EC%84%B1%EB%8A%A5-%EC%B5%9C%EC%A0%81%ED%99%94-ssr-cache-%EC%A0%81%EC%9A%A9%EA%B8%B0-bf022e3a1a72
성능 최적화 관점에서의 인사이트
비즈니스적 중요성 - 매출과 직결되는 문제
원티드 사례를 보면서 깨달은 것은 성능 최적화가 단순한 기술적 개선이 아니라는 점입니다. 속도가 매출과 사용자 유지에 직접적인 영향을 미치는 비즈니스 핵심 요소라는 걸 알게 됐습니다.
근거와 동기가 있다보니 성능 최적화의 중요성을 제대로 인식하게 되었고, 단순히 "기술적으로 멋있어서" 하는 작업이 아니라 사용자 유지와 매출에 직결되는 중요한 업무라는 걸 생각해 봤습니다.
기술적 관점에서의 최적화 경험
1. 컨텍스트/라우터 최적화
과제를 진행하면서 라우터 성능 개선에 집중했습니다:
- **컨텍스트 라우터 + useSyncExternalStore(selector)**로 필요한 조각만 구독해 리렌더를 최소화
- Provider의 value(라우터 인스턴스)를 재생성하지 않도록 안정화해서 불필요한 컨텍스트 전파 억제
- 라우터 상태 변경 시 불변 머지·얕은 비교로 실제로 변경된 구독자만 갱신되도록 최적화
2. SSR/하이드레이션 최적화
하이드레이션 과정에서 발생하는 성능 문제들을 해결했습니다:
- SSR 데이터로 스토어 선초기화해서 클라이언트에서 불필요한 재페치 제거
- getServerSnapshot을 정확히 제공해서 하이드레이션 미스매치와 불필요한 리렌더 방지
- 서버에서 라우트 고정(push(url)) → 쿼리 주입 순서로 결정된 상태만 렌더링되도록 보장
실제 체감한 최적화 효과
처음에는 "그냥 동작하면 되는 거 아닌가?"라고 생각했는데, 실제로 최적화를 적용해보니 확실한 차이가 있었습니다:
- 깜빡임 현상 제거: 서버-클라이언트 상태 동기화로 화면 전환이 매끄러워짐
- 중복 API 호출 방지: 초기 데이터 활용으로 네트워크 요청 감소
- 인터랙션 지연 개선: 점진적 하이드레이션으로 사용자가 더 빨리 페이지를 사용할 수 있게 됨
결론
성능 최적화라는게 비즈니스적으로나 기술적으로 두개 다 의미가 있고 중요하다는점이였고, 저도 아직 백수지만 회사에 들어갈때는 이러한 경험과 마인드셋으로 앞으로 사용자 중심의 성능 최적화를 지속해야겠다고 생각합니다.
학습 갈무리
SSR/SSG 과제 Q&A 정리
Q1. 현재 구현한 SSR/SSG 아키텍처에서 확장성을 고려할 때 어떤 부분을 개선하시겠습니까?
- 상태 격리 및 동시성 현재: 전역 productStore 공유로 요청 간 상태 오염 위험 개선: 요청별 스토어 인스턴스 생성, DI(Dependency Injection) 패턴 도입
- 라우터 확장성 현재: 하드코딩된 라우트 등록, 단순 패턴 매칭 개선: 라우트 설정 외부화(JSON/YAML) 중첩 라우트, 레이아웃 라우트 지원 라우트 가드(인증, 권한) 체인 구조
(추가 고려사항: 캐싱 전략, 마이크로서비스 분리, CDN 활용, 로드 밸런싱 등을 생각해볼 수 있음)
Q2. Express 서버 대신 다른 런타임(Cloudflare Workers, Vercel Edge Functions 등)을 사용한다면 어떤 점을 수정해야 할까요?
런타임 API 전환
- Node 전용 API 제거: express, fs, path, process, stream 등 사용 금지
- Web 표준으로 대체: fetch 핸들러, Request/Response, URL/URLSearchParams
SSR 엔트리 적응
- Node 스트리밍(
renderToPipeableStream) 대신 Web Streams 지원(renderToReadableStream) 고려 - 현재
render(url, query)는 그대로 재사용 가능하되, 로더/템플릿 주입은 Web API로 처리
템플릿/정적 자원
- 파일 시스템 읽기 금지 → 템플릿은:
- 빌드 결과에서 import(번들에 포함)하거나
- 정적 호스팅된 index.html을 fetch로 가져오거나
- KV/R2(Cloudflare) 등 스토리지에서 로드
- 정적 파일은 플랫폼이 서빙하므로 서버 코드에서 sirv/compression 제거
라우팅/URL 처리
Request.url로 pathname/search 파싱해render(pathname, query)호출- 베이스 경로(BASE_URL)가 플랫폼 경로와 일치하도록 설정(서브패스 배포 시 주의)
MSW/모킹
- 에지 런타임에 Node MSW 서버 사용 불가
- 모킹이 필요하면:
- 테스트 환경에 한해 브라우저/Service Worker 모킹만 사용
- 서버 런타임에서는 실제 fetch 또는 런타임별 모킹 전략으로 분리
Q3. 현재 구현에서 성능 병목이 될 수 있는 지점은 어디이고, 어떻게 개선하시겠습니까?
전역 상태 오염 문제
문제 상황:
main-server.tsx에서 모든 SSR 요청이 하나의 전역productStore인스턴스를 공유
productStore.dispatch({
type: PRODUCT_ACTIONS.SETUP,
payload: initialData, // 요청마다 다른 데이터인데 같은 스토어에 저장
});
- 모든 요청이 공유되어서 엉뚱한 데이터가 연결될 때가 있었음
해결 방향:
- 요청별 스토어 인스턴스 생성하거나, 컨텍스트로 격리된 상태 제공
- SSR 렌더 시점에만 임시 상태를 사용하고 전역 스토어 건드리지 않기
Q4. 1000개 이상의 상품 페이지를 SSG로 생성할 때 고려해야 할 사항은 무엇입니까?
규모가 커지면 빌드 시간의 급격한 증가, 메모리 부족, 전역 상태 충돌 문제가 발생합니다. 여러 페이지를 동시에 생성하는 과정에서 전역 상태가 섞여서 엉뚱한 내용이 저장될 수도 있습니다.
핵심 해결책:
- 각 페이지 생성 시마다 독립적인 상태를 만들어야 합니다
- 전역 스토어나 라우터를 공유하면 여러 페이지의 데이터가 섞일 수 있구요
- 페이지별로 새로운 상태 인스턴스를 생성하고, 생성 완료 후에는 정리하는 것이 중요한 것 같아요
Q5. Hydration 과정에서 사용자가 느낄 수 있는 UX 이슈는 무엇이고, 어떻게 개선할 수 있을까요?
주요 UX 이슈들
1. 상호작용 지연
- 페이지는 보이는데 버튼 클릭이 안 됨
- 하이드레이션 완료까지 1-3초 기다려야 함
2. 깜빡임과 화면 변화
- 서버: "로그인" 버튼 → 클라이언트: "로그아웃" 버튼으로 갑자기 변경
- 장바구니 개수가 0개 → 3개로 갑자기 바뀜
- 구현 시에도 매번 있었던 변화
3. 중복 로딩
- 콘텐츠 표시 → 로딩 스피너 → 다시 콘텐츠 (깜빡임)
- 사용자: "뭐지? 왜 또 로딩?"
4. 입력 데이터 손실
- 검색어 입력 중 → 하이드레이션으로 값 초기화/포커스 잃음
개선 방법
-
상태 일치: 서버와 클라이언트 초기 상태를 같게 만들기
- 서버와 클라이언트 초기 상태가 달라서 하이드레이션 시 화면이 바뀜
-
우선순위: 중요한 UI부터 하이드레이션하기
- 모든 걸 한번에 하이드레이션하면 인터랙션 가능 시점 늦어짐
-
피드백: 사용자에게 현재 상태 명확히 알려주기
- 페이지는 보이는데 클릭이 안 되면 안 됨
-
데이터 보호: 사용자 입력 손실 방지하기
Q6. 이번 과제에서 학습한 내용을 실제 프로덕션 환경에 적용할 때 추가로 고려해야 할 사항은?
에러 핸들링 및 Fallback 전략
- SSR 실패 시 CSR로 자동 전환: 서버 문제 시에도 앱이 동작하게 안전망
- API 타임아웃 처리: 외부 API가 느리면 SSR도 느려짐 → 적절한 제한시간 설정
- 부분 실패 허용: 관련상품 로드 실패해도 메인 상품은 보여주기
Q7. Next.js 같은 프레임워크 대신 직접 구현한 SSR/SSG의 장단점은 무엇인가요?
직접 만들어보니까
장점:
1. 학습 효과와 깊은 이해
- 직접 구현하면서 SSR/SSG의 데이터 흐름(라우팅→쿼리 주입→스토어→하이드레이션), 요청 단위 상태 격리, 브라우저 의존성 제거 등 핵심 개념을 깊게 이해하게 됨
- 문제 원인 파악은 쉬워짐
2. 커스터마이징 자유도
- 직접 만들다 보니까 라우터나 스토어 등 커스텀 자유도가 좀 높음
단점:
유지보수 비용과 안정성
- 직접 유지해야 하므로 사고 범위가 넓고 회귀 리스크가 큼
- 코드를 정리해야 하거나 억지스러운 면이 있는 코드도 존재해서, 유지보수가 필요합니다
Q8. Next.js를 이용하여 SSG 방식으로 배포하려면 어떻게 해야 좋을까요?
Next.js SSG 배포 방법
1. 설정 변경
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export', // 정적 파일로 내보내기
trailingSlash: true,
images: {
unoptimized: true // 정적 배포 시 이미지 최적화 비활성화
}
}
2. 페이지별 정적 생성
// pages/products/[id].js
export async function getStaticPaths() {
// 빌드 시점에 생성할 경로들 정의
const products = await getAllProducts();
return {
paths: products.map(product => ({
params: { id: product.id }
})),
fallback: false // 정의되지 않은 경로는 404
};
}
export async function getStaticProps({ params }) {
// 각 페이지의 데이터 미리 로드
const product = await getProduct(params.id);
return {
props: { product }
};
}
3. 빌드 및 배포
npm run build # 정적 파일 생성 (out 폴더)
# Netlify, Vercel, GitHub Pages 등에 out 폴더 배포
주의사항:
- 서버 전용 기능(API Routes, getServerSideProps) 사용 불가
- 동적 라우팅은 미리 경로를 정의해야 함
- 클라이언트 사이드 라우팅은 여전히 동작
코드 품질 향상
개선하고 싶은 부분
- 리액트로 ssr 구현할때, any타입을 몇몇 군데 사용 했는데 그냥 안쓰고 다시 만들어 보고 싶습니다.
- 일단 시간이 부족해 리액트를 너무 AI의존도를 너무 높여서 과제를 했는데, 흐름 파악이 좀 어려웠습니다.
- 리액트에서 에러나 이슈가 발생시에 어떤 부분이 문제였는지? 찾아내는게 좀 어려워서 나중에 한번 전체적으로 구현을 다시 해보고 싶습니다.
학습 연계
다음 학습 목표
- 다시 한번 구현 해보기. 나의 함량을 높여서.
아직은 백수고 취업하고 있어서 실무에 연계는 어떻게 해볼지 생각은 안해봤습니다.
리뷰 받고 싶은 내용
현재 SSR에서 전역 productStore를 모든 요청이 공유하고 있는데, 동시 요청 시 상태 오염 가능성이 있었던거 같았습니다.
static-site-generate.js에서 Promise.all로 상품 상세 페이지 병렬로 생성했었는데.
ssg 테스트를 할때, 기대했던 상세페이지가 안나오고 엉뚱한 상세페이지가 나오더라고요.
export const render = async (url: string, query: Record<string, string>) => {
// ...
productStore.dispatch({ // ← 모든 요청이 같은 전역 인스턴스
type: PRODUCT_ACTIONS.SETUP,
payload: initialData,
});
const html = renderToString(<App router={router} />);
}
해결 방법으로 요청별 스토어 인스턴스 생성 vs Context Provider 격리 중 어느 쪽이 좀 효율적인지 궁금합니다.
과제 피드백
성진님 고생하셨어요~ 훌륭하게 과제에 대해 잘 정리해주신 것 같네요.
성능 최적화 관점에서 인사이트에 어떤 관점으로 생각하면 잘 모르겠다고 남겨주신것에 있어서 제 개인적인 생각을 조금만 붙여보면 결국 남겨주신 블로그처럼 저희는 최대한 사용자들이 쓰기 좋은 UX로 빠른 시간내에 서빙하는게 최종 목표인 사람들일거거든요. 그 과정에서 트레이드 오프도 발생할거고 이 성능적인 부분이 실제 제품 관점에서 알게 모르게 큰 영향을 미치기도 할거구요. 라이트 하우스 같은것들을 활용하다 보면 각 지표들이 왜 측정되는 지, 이게 안좋으면 어떤 영향을 주는지 나타내게 되는데 이런 부분들도 함께 봐보면 좋을것 같아요 ㅎㅎ
해결 방법으로 요청별 스토어 인스턴스 생성 vs Context Provider 격리 중 어느 쪽이 좀 효율적인지 궁금합니다.
완전하게 격리된다는 관점에서 크게 차이는 없을 것 같긴한데.. 제가 개인적으로 생각하기에는 기존 로직을 그대로 사용할 수 있다는 관점에서 요청별로 분리하는게 구현관점이나 추후 관리 관점에서도 훨씬 편하지 않을까 싶긴하네요! 둘 다 해봐도 좋은 방식일 것 같습니다.
고생하셨고 마지막 주차도 화이팅입니다!