[5팀 진재윤] Chapter 4-1 성능최적화: SSR, SSG, Infra #18
Open
jy0813 wants to merge 19 commits intohanghae-plus:mainfrom
Open
[5팀 진재윤] Chapter 4-1 성능최적화: SSR, SSG, Infra #18jy0813 wants to merge 19 commits intohanghae-plus:mainfrom
jy0813 wants to merge 19 commits intohanghae-plus:mainfrom
Conversation
…ontext() 함수routerHelper.js: getQuery(), getParams() SSR 호환 헬퍼serverRouter.js: 서버용 라우터 구현index.js: 새 모듈 re-export
…refetchData() 직접 반환server.js: render(url) 시그니처로 변경static-site-generate.js: SSG 빌드 호환성 수정동시성 문제 해결: 싱글톤 Store 대신 요청별 컨텍스트 사용
…DetailPage: 동일 패턴 적용SSR에서는 컨텍스트 데이터, CSR에서는 Store 데이터 사용
…e hooks 스킵router.js: SSR 환경 감지 통합render.js: 렌더링 로직 정리
…productService.js: 서비스 레이어 정리mocks: MSW 서버 설정 lib: 유틸리티 및 ServerRouter 추가
- ssrContext: isSSR(), getSSRData(), setSSRContext() 함수 추가 - ServerRouter: 서버 환경용 라우터 구현 - MSW server: Node.js 환경 mock 서버 설정 - handlers: SSR/CSR 환경 분기 처리
- main-server.tsx: renderToString 기반 SSR 렌더링 - ssr-data.ts: 홈페이지/상품상세 데이터 프리페칭 - productApi: SSR 환경 API 호출 지원 (절대 URL) - 관련 상품 SSR 로딩 추가
- productStore: window.__INITIAL_DATA__ hydration 지원 - useProductStore: SSR Context 데이터 직접 반환 - cartStorage: lazy initialization으로 SSR 호환 - main.tsx: hydrateRoot 조건부 실행
- router.ts: SSR 환경 감지 및 분기 처리 - useCurrentPage: SSR Context params 활용 - useRouterParams/Query: SSR 환경 fallback 처리 - App.tsx: SSR 환경 lifecycle hooks 스킵
- server.js: Express SSR 서버 (MSW 통합) - static-site-generate.js: 22개 정적 페이지 생성 - package.json: SSR/SSG 빌드 스크립트 추가 - preview:ssr tsx 런타임 사용
- log.ts: window.__spyCalls 조건부 초기화 - domUtils.ts: isNearBottom SSR guard 추가
- useRouter, useStore에 세 번째 인자(getServerSnapshot) 추가 - SSR hydration 시 'Missing getServerSnapshot' 에러 해결 - 서버/클라이언트 간 상태 일관성 보장
- HomePage: '쇼핑몰 - 홈' 타이틀 설정 - ProductDetailPage: 상품명 기반 타이틀 업데이트 - vanilla 버전과 동일한 UX 제공
- main.tsx: Store 로드 전 SSR 감지 로직을 동적 import로 분리 - productStore: hydration 디버그 로그 추가 - productUseCase: SSR 데이터 존재 시 fetch 스킵 로직 추가 - productUseCase: router → getRouter() 변경으로 null 체크 처리 - useProductFilter: 첫 렌더링 시 SSR 데이터 있으면 loadProducts 스킵
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
과제 체크포인트
배포 링크
vanilla
react
기본과제 (Vanilla SSR & SSG)
Express SSR 서버
<!--app-html-->,<!--app-head-->)서버 사이드 렌더링
클라이언트 Hydration
window.__INITIAL_DATA__스크립트 주입Static Site Generation
심화과제 (React SSR & SSG)
React SSR
renderToString서버 렌더링React Hydration
Static Site Generation
아하! 모먼트 (A-ha! Moment)
1. SSR에서 싱글톤 Store는 모든 사용자가 공유된다.
CSR에서 당연하게 쓰던 코드가 SSR에서 치명적인 버그를 일으켰습니다.
A 사용자가 "노트북"을 검색하고, B 사용자가 "마우스"를 검색하면 — Node.js는 싱글 스레드지만 비동기로 요청을 처리하기 때문에, A의 렌더링 시점에 B의 검색 결과가 Store에 덮어씌워져 있을 수 있습니다. 실제로 4개 테스트 실패를 고치려다가 14개 실패로 늘어난 것도 이 문제를 이해하지 못하고 Store에 dispatch하려 했기 때문이었습니다.
깨달음: 브라우저에서는 각 탭이 완전히 독립된 JS 컨텍스트지만, Node.js 서버에서는 모든 요청이 같은 메모리를 공유합니다. 해결책은 전역 Store 대신 요청별 Context로 데이터를 격리하는 것이었습니다. Next.js나 Remix 같은 프레임워크들이 복잡한 상태 격리 메커니즘을 갖추고 있는 이유를 이해하게 되었습니다.
2.
typeof window체크는 언제 하느냐가 핵심이다.window is not defined에러를 해결하려고 조건문을 추가했는데 여전히 에러가 났습니다.깨달음: 조건문은 실행 시점에 평가되지만,
new Router()는 모듈 로드 시점에 즉시 실행됩니다. 해결책은 Lazy Initialization 패턴이었습니다.모듈 최상위에서 인스턴스를 생성하지 않고, 실제로 필요한 시점에 생성하도록 변경하니 에러가 해결되었습니다.
3. Hydration은 새로 그리는 것 이 아니라 연결하는 것 이다.
SSR로 HTML을 보냈는데 버튼이 클릭되지 않아서 "클라이언트에서 다시 렌더링해야 하나?"라고 생각했습니다.
깨달음:
hydrateRoot()는 이미 있는 DOM을 재사용하면서 이벤트 핸들러만 연결합니다. 그래서 SSR HTML과 클라이언트 렌더링 결과가 정확히 일치해야 합니다.이번 프로젝트에서 겪은 불일치 사례들:
loading: false로 렌더링했는데, 클라이언트 Store 초기값은loading: true해결책은
window.__INITIAL_DATA__를 통해 SSR 데이터를 클라이언트로 전달하고, Store 초기화 시 이 데이터를 사용하여 동일한 상태에서 시작하도록 하는 것이었습니다.4. MSW는 브라우저용과 Node.js용이 완전히 다른 모듈이다.
Mock Service Worker를 사용하고 있었는데, SSR 서버에서
setupWorker is not a function에러가 발생했습니다.깨달음: MSW는 환경별로 완전히 다른 인터셉트 방식을 사용합니다.
msw/browsermsw/nodehandlers 배열은 공유할 수 있지만, setup 함수는 환경에 맞게 선택해야 합니다. 이 구분을 모르면 SSR 서버에서 API 모킹이 동작하지 않습니다.
자유롭게 회고하기
들어가며
이번 프로젝트에서 CSR 기반 쇼핑몰에 SSR과 SSG를 추가 구현하면서, 프론트엔드 개발자로서 "서버에서 JavaScript가 실행된다"는 것이 무엇을 의미하는지 깊이 이해하게 되었습니다. 단순히 Next.js나 Nuxt.js를 사용하면 해결되는 문제처럼 보이지만, 직접 구현해보니 프레임워크가 얼마나 많은 복잡성을 숨기고 있는지 체감할 수 있었습니다.
기술적 도전과 해결
isServer() vs isSSR() - 미묘하지만 중요한 구분
처음에는 "서버 환경이면 SSR 중인 거 아닌가?"라고 생각했습니다. 하지만 두 개념은 다릅니다.
isServer()isSSR()예를 들어 SSG 빌드 스크립트를 실행할 때:
isServer()→true(Node.js 환경)isSSR()→ 페이지를 렌더링할 때만true, 빌드 스크립트 자체는false이 구분이 있어야 빌드 시점과 렌더링 시점의 로직을 정확히 분리할 수 있습니다.
프레임워크를 넘어선 공통 패턴
Vanilla JS SSR을 먼저 구현하고 React SSR을 구현했는데, 같은 문제가 반복되면서 패턴이 보이기 시작했습니다.
SSR 구현의 공통 패턴:
isServer,isBrowser,isSSR)window.__INITIAL_DATA__)프레임워크가 달라도 SSR의 근본적인 도전은 동일합니다. 이 패턴들을 이해하고 있으면 Next.js나 Remix 같은 프레임워크를 사용할 때도 내부 동작을 더 잘 이해할 수 있습니다.
실패에서 배운 교훈
4개 → 14개 테스트 실패 사건
URL 인코딩 관련 4개 테스트가 실패하는 상황에서, "Store에 dispatch하면 되겠지"라고 생각하고 코드를 수정했습니다. 결과는 14개 테스트 실패. 동시성 문제를 더 악화시킨 것이었습니다.
교훈: 문제가 있을 때 "빠른 해결책"보다 "원래 어떻게 동작했는지" 먼저 분석해야 합니다. 처음에 작성하였던 코드(
main-server-origin.js)를 다시 읽고 나서야 Store를 사용하지 않는 이유를 이해했습니다.withLifecycle deps 버그
Vanilla JS에서 useEffect와 유사한 의존성 감지 로직을 구현했는데, 첫 마운트 시 deps를 빈 배열로만 초기화하는 실수를 했습니다.
결과적으로 검색어가 변경되어도 상품 목록이 업데이트되지 않았습니다.
lifecycle.deps[0]이undefined이기 때문에 비교 함수가 항상 "변경 없음"을 반환했습니다.교훈: React의 useEffect도 내부적으로 이전 deps를 저장해두고 비교합니다. 직접 구현할 때는 이런 "당연한 동작"도 명시적으로 구현해야 합니다.
처음부터 다시 해본다면
1. SSR-First 설계
CSR로 먼저 만들고 SSR을 추가하는 방식은 많은 리팩토링이 필요했습니다. 다음에는 처음부터 SSR을 고려하여 설계하겠습니다.
2. 환경별 테스트
E2E 테스트가 많은 버그를 잡아줬지만, SSR 전용 단위 테스트가 있었다면 더 빨리 발견했을 것입니다.
3. 문서화 습관
이번처럼 에러 → 원인 → 해결 과정을 기록해두면 같은 실수를 반복하지 않습니다. 특히 "왜 이렇게 했는지"를 남겨두면 나중에 코드를 수정할 때 실수를 방지할 수 있습니다.
마무리
SSR 구현은 결국 같은 코드를 다른 환경에서 실행하는 도전이었습니다.
브라우저에서는 당연히 있는 것들이 서버에서는 없고, 서버에서는 당연한 것들이 브라우저에서는 고려할 필요가 없었습니다. 이 간극을 메우는 과정에서 프론트엔드와 백엔드의 경계에 대해 깊이 생각하게 되었습니다.
Next.js 같은 프레임워크를 사용하면 이 모든 복잡성을 추상화해주지만, 직접 구현해본 경험은 프레임워크의 동작 원리를 이해하고 문제를 디버깅하는 데 큰 도움이 됩니다. 앞으로 SSR 관련 이슈가 발생하면, 이번에 배운 패턴들을 떠올리며 더 빠르게 해결할 수 있을 것입니다.
리뷰 받고 싶은 내용
static-site-generate.js에서 SSG 빌드 시 모든 상품 페이지를 순차적으로 생성하고 있습니다. 현재는 약 20개 상품으로 테스트 중인데, 상품이 1000개 이상으로 늘어났을 때 빌드 시간과 메모리 사용량이 걱정됩니다.Promise.all+ chunking)으로 개선 가능할지ssrContext.ts에서 전역 변수let ssrContext를 사용하여 요청별 상태를 관리하고 있습니다.현재는 try-finally로 렌더링 후 컨텍스트를 클리어하고 있지만, Node.js의 비동기 특성상 동시 요청이 들어오면 컨텍스트가 덮어씌워질 위험이 있지 않은지 검토 부탁드립니다.
예를 들어 A 요청의 렌더링 중에 B 요청이 setSSRContext()를 호출하면, A의 컴포넌트가 B의 데이터를 읽게 되는 것 아닌지 우려되고, 회고에서 "싱글톤 Store는 모든 사용자가 공유된다"를 핵심 깨달음으로 다뤘는데, 현재 SSR Context 구현도 동일한 패턴의 위험이 있는 것 같습니다. 요청별로 Context를 격리하는 더 안전한 방식이 있다면 조언 부탁드립니다.