과제 체크포인트
배포 링크
- 기본: https://chan9yu.github.io/front_6th_chapter4-1/vanilla/
- 심화: https://chan9yu.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 (상품 상세 페이지들)
- 빌드 타임 페이지 생성
- 파일 시스템 기반 배포
구현 과정 돌아보기
가장 어려웠던 부분과 해결 과정
서버 요청 간 전역 상태 공유 문제와 메모리 격리 구현
가장 어려웠던 부분은 서버사이드 렌더링에서 요청별 메모리 격리를 구현하는 것이었습니다. E2E 테스트에서 동시에 여러 페이지를 요청할 때 다른 요청의 데이터가 섞이는 현상이 발생했습니다.
문제 원인은 서버에서 전역 상태를 공유하면서 각 요청의 데이터가 오염되는 것이었습니다. 해결 과정에서 단일 요청은 정상적으로 동작하지만 동시 요청 시 간헐적으로 실패하는 패턴을 발견했고, 이를 통해 메모리 공유 문제임을 파악할 수 있었습니다.
해결책으로 요청별 격리된 데이터 전달 시스템을 구축했습니다. Vanilla에서는 withUniversal를 통해 각 요청마다 독립적인 데이터를 전달하고, React에서는 메모리 스토리지와 Context API를 활용한 요청별 상태 격리를 구현했습니다.
구현하면서 새롭게 알게 된 개념
라우터 팩토리 패턴과 환경별 분기의 중요성
기존에는 라우터를 단순히 URL 매칭 도구로만 생각했지만, Universal JavaScript 환경에서는 서버와 클라이언트에서 서로 다른 라우터 인스턴스가 필요함을 알았습니다
useSyncExternalStore의 서버사이드 동작 원리
React 18의 useSyncExternalStore에서 getServerSnapshot 옵션의 중요성을 알게되었습니다. 서버는 구독 메커니즘이 없기 때문에 정적 스냅샷을 반환해야 합니다.
// 서버사이드 안정성을 위한 getServerSnapshot 구현
const useStore = (selector) => {
return useSyncExternalStore(
store.subscribe,
() => selector(store.getState()),
() => selector(store.getState()), // getServerSnapshot 추가
);
};
SSRService 클래스를 통한 체계적인 서버 렌더링
서버사이드 렌더링 로직을 단순한 함수가 아닌 클래스 기반으로 구조화하여 확장성과 유지보수성을 높였습니다.
성능 최적화 관점에서의 인사이트
MSW 서버 통합을 통한 개발 환경 최적화
개발 환경에서 백엔드 API 없이도 풀스택 개발이 가능하도록 MSW를 서버사이드에서도 통합했습니다. 이를 통해 클라이언트와 서버에서 동일한 모킹 데이터를 사용하여 하이드레이션 불일치를 방지했습니다.
학습 갈무리
Q1. 현재 구현한 SSR/SSG 아키텍처에서 확장성을 고려할 때 어떤 부분을 개선하시겠습니까?
현재 각 요청마다 새로운 라우터와 스토어 인스턴스를 생성하는 방식이 메모리 효율성 면에서 개선이 필요합니다. Redis를 활용한 렌더링 결과 캐싱과 페이지별 설정 관리 시스템을 도입하고 싶습니다. 또한 SSRService 클래스를 더 모듈화하여 기능별로 독립적인 서비스로 분리하면 마이크로서비스 아키텍처로 확장할 때 유리할 것 같습니다.
Q2. Express 서버 대신 다른 런타임(Cloudflare Workers, Vercel Edge Functions 등)을 사용한다면 어떤 점을 수정해야 할까요?
가장 큰 변경점은 파일 시스템 접근 방식입니다. 현재 서버 시작 시 index.html을 읽어두는 방식을 요청별 읽기로 변경해야 하고, Express 미들웨어를 Web API 기반으로 재작성해야 합니다. MSW 서버도 Node.js 환경에 의존하고 있어서 Edge Runtime 호환 모킹 방식으로 교체하거나 실제 API로 전환이 필요합니다.
Q3. 현재 구현에서 성능 병목이 될 수 있는 지점은 어디이고, 어떻게 개선하시겠습니까?
SSR 시 각 요청마다 전체 렌더링을 수행하는 부분이 가장 큰 병목입니다. 페이지별 캐싱 전략을 도입하고, 현재 순차 처리되는 SSG 빌드를 병렬 처리로 개선하면 성능이 크게 향상될 것입니다. 또한 MSW 오버헤드를 줄이기 위해 프로덕션에서는 직접 API 호출로 전환하는 것도 중요합니다.
Q4. 1000개 이상의 상품 페이지를 SSG로 생성할 때 고려해야 할 사항은 무엇입니까?
현재 SSGBuilder가 순차 처리 방식이라 빌드 시간이 오래 걸릴 것입니다. Promise.all을 활용한 병렬 처리와 배치 단위 처리로 메모리 사용량을 제어해야 합니다. 또한 모든 페이지를 한 번에 생성하기보다는 인기도 기반 우선순위를 두고, 나머지는 ISR로 처리하는 하이브리드 전략이 효율적일 것 같습니다.
Q5. Hydration 과정에서 사용자가 느낄 수 있는 UX 이슈는 무엇이고, 어떻게 개선할 수 있을까요?
가장 큰 문제는 JavaScript 로딩 완료 전까지 인터랙션이 불가능한 점입니다. 기본적인 네비게이션은 <a> 태그를 활용해 하이드레이션 없이도 동작하도록 하고, 서버와 클라이언트 렌더링 차이로 인한 Layout Shift를 방지하기 위해 스켈레톤 UI와 일관된 플레이스홀더를 사용하는 것이 중요합니다.
Q6. 이번 과제에서 학습한 내용을 실제 프로덕션 환경에 적용할 때 추가로 고려해야 할 사항은?
서버 모니터링과 에러 추적 시스템이 필수라고 생각합니다. SSR 실패 시 CSR로 자동 전환하는 Fallback 로직과 보안 강화(XSS 방지, CSP 설정)가 중요할 거 같습니다
Q7. Next.js 같은 프레임워크 대신 직접 구현한 SSR/SSG의 장단점은 무엇인가요?
직접 구현의 가장 큰 장점은 완전한 제어권과 깊은 이해입니다. SSR/SSG의 내부 동작을 정확히 파악할 수 있어서 특정 요구사항에 맞는 세밀한 최적화가 가능합니다. 하지만 개발 시간과 유지보수 부담이 크고, Next.js의 풍부한 생태계와 검증된 최적화 기능들을 포기해야 하는 단점이 있습니다.
Q8. Next.js 를 이용하여 SSG 방식으로 배포하려면 어떻게 해야 좋을까요?
next.config.js에서 output: 'export'를 설정하고, 동적 라우트는 getStaticPaths로 경로를 미리 정의해야 합니다. getStaticProps로 빌드 타임 데이터 페칭을 처리하고, 이미지 최적화 등 일부 기능은 정적 배포를 위해 비활성화해야 합니다. Vercel이나 GitHub Pages 같은 플랫폼에서 자동으로 SSG를 인식해 배포해줍니다.
코드 품질 향상
자랑하고 싶은 구현
라우터 팩토리를 통한 환경별 인스턴스 관리
서버와 클라이언트 환경에서 서로 다른 기능을 가진 라우터를 팩토리 패턴으로 해결했습니다
// 환경별 라우터 팩토리
function createRouter() {
const RouterClass = isServer() ? ServerRouter : SPARouter;
return new RouterClass<FunctionComponent>({}, BASE_URL);
}
SSRService 클래스를 통한 체계적인 서버 렌더링
서버 렌더링 로직을 클래스 기반으로 구조화하여 확장성과 재사용성을 높였습니다
export class SSRService {
constructor(routes) {
this.routes = routes;
this.router = createRouter(routes, "", { initRoutes: false });
}
async render(url, query = {}) {
try {
// 라우터 초기화
this.router.start(url, query);
// 데이터 프리페칭
const data = await this.prefetchData(this.router.target, query);
// HTML 렌더링
const html = this.renderPage(this.router.target, { data, query });
return {
html,
head: this.generateHead(),
data,
};
} catch (error) {
return this.handleError(error);
}
}
}
개선하고 싶은 부분
타입 안전성 강화가 가장 시급합니다. React 패키지에서 any 타입 사용을 제네릭으로 대체하여 서버-클라이언트 데이터 전달 과정의 타입 안전성을 향상시키고 싶습니다.
SSR 과정에서 발생하는 에러에 대한 적절한 Fallback HTML 응답 및 에러 복구 로직 추가가 필요합니다.
현재 SSGBuilder가 순차 처리 방식인 점도 개선하고 싶습니다. 병렬 처리로 개선하여 대용량 페이지 생성 시 성능을 향상시키고, 서버와 클라이언트 라우터 간 인터페이스 차이를 최소화하여 Universal 코드 작성을 더 최적화 해보고 싶습니다
리팩토링 계획
의존성 주입 패턴 도입을 통해 테스트 용이성을 높이고 싶습니다. SSRService, 라우터, 스토어 등을 의존성 주입 구조로 개선하고, 현재 하나의 파일에 있는 여러 책임을 단일 책임 원칙에 따라 분리하면 좋을 거 같습니다
서버와 클라이언트에서 동일한 인터페이스로 사용할 수 있는 Universal 훅 라이브러리 구축과 하드코딩된 라우트 처리 대신 설정 파일 기반의 동적 라우팅 시스템 구축도 고려해본다면 좋을 거라 생각됩니다
학습 연계
다음 학습 목표
과제에서는 SSR과 SSG를 직접구현하면서 많은 지식을 얻을 수 있었는데 ISR 방식도 따로 구현해보고 싶습니다.
실무 적용 계획
사실 지금 실무에서는 CSR만 사용하고 있기때문에 당장 적용 계획은 없습니다
리뷰 받고 싶은 내용
과제 피드백
찬규님 고생하셨습니다 ㅋㅋㅋㅋ 분명 채점을 하려고 켰는데 조금 남았다고 조금만 기다려 달라는 글을 보고 기다렸는데... 결국 해결이 잘 안된거같군요................ 우선 심화는 테스트 코드가 통과하지 않아 이 부분에 대한 부분은 불합격했습니다 ㅠㅠ 정리해주신 회고 내용보면 더 아깝군요. 필요한 부분들이나 서버 내에서 렌더링을 하는 과정에 있어 필요한 부분들에 대해서도 명확하게 잘 학습해주시고 정리해주신것 같아요. 개선에 대한 지점도 명확하게 인지하고 계신것 같구요 ㅎㅎ
리뷰 받고 싶으신 내용은 따로 없으셔서 여기서 마무리 하도록 하겠습니다. 마지막 주차도 화이팅하세요!