Open
Conversation
- Express + Vite SSR 파이프라인 구축 - 서버 렌더링, 하이드레이션, SSG 완료 - import 경로 문제 해결 및 서버 호환성 개선
- 상품 목록, 상세, 카테고리 API 구현 - 서버 환경에서 절대 URL 사용하도록 수정 - 상품 상세 페이지 렌더링 로직 추가 및 관련 상품 로드 기능 구현 - 라우터에서 서버 환경에 맞춘 경로 처리 및 이벤트 리스너 등록 조건 추가
- 빌드 스크립트에 'build:without-ssg' 추가 - 미리보기 스크립트의 출력 디렉토리 수정 - Router.js에 디버깅 로그 추가하여 라우팅 과정 추적 용이
- ServerRouter 클래스 추가로 서버 사이드 라우팅 자동화 - ApiRouter 클래스 추가로 API 라우트 자동화 - 페이지 컴포넌트에 SSR 메서드 추가 - 중앙 집중식 라우트 설정으로 확장성 향상 - 수동 URL 분기 제거하고 자동화된 라우터 사용
- SSR에서 클라이언트와 동일한 정렬 기준을 사용하도록 수정 - 쿼리 객체에 기본 정렬 기준 추가
- MSW 서버를 시작하여 API 요청을 모킹하도록 수정 - Vite 서버를 생성하여 SSR 모듈을 로드하도록 변경 - 페이지 렌더링 및 저장 프로세스 단계 업데이트 - Vite 및 MSW 서버 정리 로직 추가
- 개발 환경에서 MSW 서버를 시작하도록 추가 - SSR 메서드에서 로딩 상태를 항상 false로 설정하여 완전한 데이터 반환 - API 라우트에서 필터링 및 정렬 로직 통합 - 홈 및 상품 상세 페이지의 SSR 메서드 개선
- 빌드된 main-server.js 파일을 동적으로 찾도록 수정 - SSR 메서드에서 로딩 상태를 항상 false로 설정하여 완전한 데이터 반환 - 홈 및 상품 상세 페이지에서 SSR 데이터가 있을 경우 불필요한 로드 방지 - 클라이언트 하이드레이션 시 SSR 데이터 사용 개선
- index.html에서 스타일시트 링크에 self-closing 태그 추가 - SSR 메서드에서 초기 데이터 스크립트를 HTML 템플릿에 통합하여 코드 정리 - 홈 및 상품 상세 페이지의 SSR 메서드에서 불필요한 상태 필드 제거
- 홈 페이지에서 SSR 데이터와 클라이언트 쿼리 통합 - 서버에서 URL 쿼리 파라미터를 req.query로 변경하여 일관성 향상
- 홈 페이지와 상품 상세 페이지에서 클라이언트 초기 데이터 로드 로직 수정 - 상품 목록 로드 시 응답 구조 변경에 따른 상태 업데이트 방식 개선 - 불필요한 상태 필드 제거 및 에러 처리 로직 정리
- createStorage 함수에서 localStorage가 없는 경우에 대한 처리 추가 - cartReducer에서 스토리지 로드 실패 시 초기 상태 사용하도록 수정 - 스토리지 데이터 구조 검증 로직 추가
- 서버 시작 로직을 async 함수로 변경하여 비동기 처리 개선 - API 라우트에서 SSR 제외 로직 추가 - 제품 API 호출 시 서버 환경에 따른 baseUrl 동적 생성 - 상태 관리 개선을 위해 loading 상태를 status에 기반하여 업데이트 - 카테고리 설정 함수에서 매개변수 구조 변경
- 빌드 스크립트에서 클라이언트 및 서버 출력 디렉토리 수정 - SSR 환경에서 HTML 템플릿 경로 및 API 라우팅 개선 - 정적 사이트 생성 프로세스에서 템플릿 경로 수정 - 클라이언트 MSW 비활성화 로직 추가 및 API 핸들러 수정 - 상태 관리 및 쿼리 처리 로직 개선
- 상품 상세 페이지 테스트를 SSR에서 SSE로 변경하여 렌더링 검증 - 서버 시작 시 URL 처리 로직 개선, 개발 환경에서 base URL을 적절히 처리하도록 수정 - 서버 라우터에서 경로 정규화 로직 개선, baseUrl 제거 처리 추가
- 서버 시작 시 URL에서 baseUrl을 제거하여 경로 정규화 로직을 개선 - 라우터에서 경로 정규화 시 baseUrl 제거 처리 추가 - 불필요한 API 라우터 삭제
- popstate 이벤트 리스너를 항상 등록하도록 수정하여 서버 환경 의존성 제거 - query 및 #findRoute 메서드에서 window 객체 사용을 간소화하여 코드 일관성 향상 - push 메서드에서 불필요한 서버 환경 체크 제거 및 URL 처리 로직 개선
- 서버 렌더링 시 요청마다 새로운 서버 라우터 인스턴스를 생성하여 상태 격리 구현 - 클라이언트에서만 라우트 등록하도록 조건 추가 - 서버 환경에 따라 적절한 라우터 인스턴스 생성 로직 추가 - URL 처리 로직을 개선하여 서버와 클라이언트 간의 일관성 향상
- Express 서버 SSR 지원 (path-to-regexp 에러 해결) - 바닐라 runtime 패턴 적용 (createStorage, Router SSR 체크) - 페이지별 SSR 메서드 분리 (ssr-methods.ts) - ServerRouter 구현 및 라우트 등록 - 클라이언트 하이드레이션 로직 구현 - MSW 서버 사이드 통합 - 동적 메타데이터 생성 지원 - ESLint 에러 수정
- React SSR 서버 구현 (server.js) - main-server.tsx에서 서버사이드 렌더링 로직 구현 - SSR 데이터 하이드레이션 및 전역 저장 방식으로 컴포넌트 간 데이터 전달 개선 - ProductList, SearchBar, HomePage에서 SSR 데이터 우선 사용하여 로딩 상태 제거 - 스켈레톤 UI 및 카테고리 로딩 메시지 불필요한 표시 방지 - TypeScript 타입 안정성 개선 (any → unknown/specific types) 주요 개선사항: - SSR 시 초기 데이터가 있을 때 클라이언트 재요청 방지 - 하이드레이션 과정에서 데이터 손실 문제 해결 - App.tsx에서 전역 SSR 데이터를 props로 컴포넌트에 전달
- main-server.tsx에서 라우트에 따라 적절한 컴포넌트 렌더링 로직 추가 - SearchBar 컴포넌트에 SSR 쿼리 지원 추가, 클라이언트 상태와 동기화 - useProductFilter 훅에서 SSR 쿼리 정보 처리 로직 개선 - HomePage에서 SearchBar에 SSR 쿼리 전달하여 초기 검색 상태 유지
- package.json에서 build 관련 스크립트를 npm에서 pnpm으로 변경 - static-site-generate.js에서 MSW 서버 및 Vite 서버 통합 로직 개선 - 서버 렌더링 시 페이지 목록 생성 및 HTML 템플릿 처리 로직 추가 - API 라우트 및 정적 리소스 경로 수정 로직 추가 - App.tsx에서 SSR 쿼리 지원 추가
- App.tsx에서 SSR 데이터 전달 방식을 개선하여 props 전개 사용 - main.tsx에서 전역 SSR 데이터 대신 요청별 상태 관리 구현 - 클라이언트 하이드레이션 로직에서 요청별 상태 저장 및 정리 기능 추가 - 메모리 누수 방지를 위한 cleanup 함수 추가
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 SSG: https://ckdwns9121.github.io/front_6th_chapter4-1/vanilla/
react SSG: https://ckdwns9121.github.io/front_6th_chapter4-1/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
구현 과정 돌아보기
가장 어려웠던 부분과 해결 과정
vite로 실행하는것과 node로 실행하는 것의 차이
vite와 node의 실행환경 차이때문에 import문에서 하나하나 확장자를 붙여줘야하는 문제가 생겼다. 그래서 지훈님께서 vite-ssr-vanilla 템플릿을 써보라고 공유해주셨고 이 템플릿으로 개발 모드시에는 Vite의 미들웨어로 번들·핫리로드 지원, 배포 시에는 compression(gzip 압축) + sirv(정적 파일 서비스)로 분기해서 처리할 수 있었다.
구현하면서 새롭게 알게 된 개념
SSR과 SSG를 구현할 때 하이드레이션 동작 원리
하이드레이션은 단순히 서버가 보낸 HTML을 재사용하는 것이 아니라, 서버가 렌더링한 정적 HTML에 클라이언트 측의 JavaScript 로직(이벤트 리스너, 상태 관리)을 연결하여 동적인 웹 페이지로 만드는 과정임을 이해했다. 이걸 코드로 직접 구현해보면서 어떻게 하이드레이션이 이루어지는지 경험해볼 수 있었다.
graph TD A[서버: API 데이터 수집] --> B[renderToString으로 HTML 생성] B --> C[HTML에 window.__INITIAL_DATA__ 주입] C --> D[클라이언트로 전송] D --> E[main.tsx에서 데이터 읽기] E --> F[전역 상태에 저장] F --> G[스토어에 하이드레이션] G --> H[App.tsx에서 props로 전달] H --> I[컴포넌트에서 사용] J[window.__INITIAL_DATA__] --> E K[직접 접근] --> I나는 이런 플로우로 구현을 했다.
getServerSnapShop
getServerSnapshot 함수는서버 사이드 렌더링(SSR) 환경에서 사용된다. 서버는 클라이언트처럼 subscribe 해서 상태 변화를 감지할 수 없으니, 정적인 상태 스냅샷을 반환해야 한다. 즉, 서버 렌더링 시점에서만 사용되는 getSnapshot 대체용 함수임을 알 수 있었다.
renderToString와 renderToPipeableStream
renderToString은 동기적 방식의 렌더링인 반면 renderToPipeableStream은 비동기 스트리밍 방식으로 서버는 HTML 파일을 청크 단위로 쪼개서 클라이언트에 보내는것이다. 이번 과제에서는 renderToPipeableStream를 써보진 못했지만 다음에 기회가 된다면 꼭 공부해서 써보고싶다.
성능 최적화 관점에서의 인사이트
SSR은 사용자가 페이지를 요청할 때마다 서버가 렌더링을 시작하는 방식이다. 이 방식은 항상 최신 데이터를 보여줄 수 있다는 장점이 있지만, 성능과 관련된 두 가지 주요 트레이드오프가 존재한다고 생각한다.
학습 갈무리
Q1. 현재 구현한 SSR/SSG 아키텍처에서 확장성을 고려할 때 어떤 부분을 개선하시겠습니까?
아키텍쳐에 대한 확장성을 고민하지 못해서 아직 정확하게 어떤부분을 개선해야할 지 잘 떠오르지 않는다. 지금 회고를 쓰면서 드는 생각은 현재는 Express 서버 하나에서 렌더링과 API를 모두 처리하고 있는데, 트래픽이 늘어나면 렌더링 서버와 API 서버를 분리하는 게 좋을 것 같다. 그리고 여러 서버 인스턴스 간 세션 공유를 위해 Redis 같은 외부 스토어를 도입하면 좋지 않을까?
Q2. Express 서버 대신 다른 런타임(Cloudflare Workers, Vercel Edge Functions 등)을 사용한다면 어떤 점을 수정해야 할까요?
Cloudflare Workers, Vercel Edge Functions가 정확하게 뭔지 몰라서 찾아봤는데 엣지 컴퓨팅을 제공하는 서비스라고 배웠다. 즉, 사용자의 요청을 중앙 서버까지 멀리 보내지 않고, 사용자와 가까운 지역의 데이터 센터(=에지 네트워크)에서 바로 실행시켜서 빠른 응답을 주는 방식이라고 한다.
만약 이 SSR 서버를 Cloudflare Workers나 Vercel Edge Functions 같은 Edge 런타임으로 이전한다면, 몇 가지 수정이 필요해보인다.
하지만 어떤점을 수정해야할지 명확하게 잘 모르겠다.
Q3. 현재 구현에서 성능 병목이 될 수 있는 지점은 어디이고, 어떻게 개선하시겠습니까?
아까 위에서 언급한 성능 최적화 관점에서의 인사이트에서의 내용과도 비슷한 맥락인데 SSR의 성능 최적화는 TTFB와 서버 부하를 줄이는 데 초점을 맞춰야 할 것 같은다. 이를 위해 renderToPipeableStream을 활용한 스트리밍 SSR을 도입하거나, 데이터베이스 쿼리를 최적화하고 캐싱 전략을 적용하는 것이 중요할 것 같다.
그리고 실제로 이런 캐싱 전략을 한번 도입해보려고 했는데 시간 관계상 잘 안됐다.
아마 이런식으로 클라이언트 단 캐시매니저를 구현해보려고 했는데 실제로는 Redis나 Memcached 같은 외부 캐시를 써야겠지만, 개발 환경에서는 이런 간단한 메모리 캐시도 효과적일 것 같다.
Q4. 1000개 이상의 상품 페이지를 SSG로 생성할 때 고려해야 할 사항은 무엇입니까?
현재는 순차적으로 처리하고 있어서 분명 1000개정도 페이지면 엄청 오래걸릴 것 같다. Promiss.all을 활용해서 병렬 처리를 한다던지 배치처리로 개선해야할 것 같다.
또한 한번에 많은 페이지를 메모리에 올려버리면 그 부분에서도 에러가 발생할 수 있을 것 같은데 증분 빌드라던지 변경된 페이지만 재 생성하는 시스템이 필요할 것 같다.
Q5. Hydration 과정에서 사용자가 느낄 수 있는 UX 이슈는 무엇이고, 어떻게 개선할 수 있을까요?
인터렉션이 차단되는 시간 문제가 생길 것 같은데 자바스크립트가 비활성화 되어도 기본 기능은 동작하게 만들고, 하이드레이션이 실패하면 CSR 모드로 자동 전환하는 fallback 로직을 추가해서 대응하면 좋을 것 같다.
혹은 가능하다면 사용자의 이벤트를 캡쳐해서 어떤 큐나 스택에 저장해놓고 하이드레이션이 완료 되면 실행하는 방식도 되지 않을까.??
Q6. 이번 과제에서 학습한 내용을 실제 프로덕션 환경에 적용할 때 추가로 고려해야 할 사항은?
서버 모니터링 기능을 추가해보고싶다. FE 개발자는 단순히 화면만 구현하는게 아닌 인프라나 서버도 중요하다고 언급해주셨다. 실제 서비스에서 서버 렌더링 성능, 로깅 시스템과 같은 지표를 측정할 수 있는 시스템을 구축해보고싶다.
Q7. Next.js 같은 프레임워크 대신 직접 구현한 SSR/SSG의 장단점은 무엇인가요?
학습 효과가 엄청났다. 특히 SSR/SSG의 내부 동작을 깊이 이해할 수 있었고, 이제 Next.js 문서를 읽으면 "아, 이 부분은 내가 구현했던 그 부분이구나" 하고 이해할 수 있을 것 같다는 자신감이 생긴다..!.!
단점은 아마 개발생산성, 유지보수 측면이지 않을까 싶다. 괜히 사람들이 Nextjs나 Nuxt를 쓰는게 아니구나 싶었다. 이번에 기업 면접을 봤었는데 그 회사의 기술 스택에서도 Nextjs를 쓰고있었다. 처음엔 Nextjs의 이점을 잘 살릴 수 없을 것 같은 서비스(?)여서 정말 궁금한 마음에 Nextjs를 왜 쓰고 계시냐고 물어봤었다. 돌아온 답변은 SSR, SEO와 같은 이점이 아니여도 개발자가 React에서 사용할 수 없는 편한 API들을 제공해준다고 말씀해주셨다. 잘 만들어진 도구를 잘 활용해서 쓰는법도 중요한법인것 같다.
but.!! 장인은 도구를 탓하지 않는다는 정신을 이번주차에서 크게 배울 수 잇었다. 만약 Next랑 Nuxt없었으면 어떡할래 .?.? -> 내가만든다!!! 라는 마인드셋을 장착할 수 있었다.
코드 품질 향상
자랑하고 싶은 구현
이 구조를 강제함으로써, 페이지마다 SSR/메타데이터 로직을 일관되게 구현할 수 있게 했다.
homePageSSR에서는 API 호출을 묶어서 처리하고, 에러 발생 시에도 최소한의 데이터 구조를 반환하도록 했다. 또한 homePageMetadata에서는 검색 쿼리 유무에 따라 동적으로 메타데이터를 생성하게끔 구현했다.
serverRouteMatches 배열을 만들어 각 라우트별 SSR/Metadata 함수를 명시적으로 매핑했다.
prefetchData와 generateMetadata 함수는 모든 라우트에서 공통적으로 동작할 수 있도록 추상화했다.
개선하고 싶은 부분
지금 특정 React 컴포넌트에서는 불필요하게 초기상태를 props로 받는 로직을 추가했는데 이 부분을 서버 상태만 관리할 수 있는 중앙 훅으로 분리해서 참조할 수 있게 한다던지 ContextAPI를 활용해서 초기 서버상태를 주입하는 형태로 개선해볼 수 있을 것 같다.
리팩토링 계획
항해 과정 끝나고 시간내서 꼭 리팩토링 해보겠습니다.
학습 연계
다음 학습 목표
ISR은 어떤식으로 구현하면 좋을지 학습해보기
실무 적용 계획
항해 과정이 끝나면 SSG를 활용한 개인블로그를 만들어봐야겠다.
리뷰 받고 싶은 내용
Q1. 서버-클라이언트 데이터 전달 과정에서 타입 안전성을 어떻게 강화할 수 있을까요?
현재 구현에서 서버가 클라이언트로 전달하는 초기 상태 데이터(window.INITIAL_DATA)의 타입을 any나 unknown으로 지정했습니다. 이로 인해 다음과 같은 문제가 발생할 수 있다고 생각합니다.
이런 부분을 어떻게 개선할 수 있을까요?
Q2. Express 서버 대신 다른 런타임(Cloudflare Workers, Vercel Edge Functions 등)을 사용한다면 어떤 점을 수정해야 할까요?
위에 PR의 질문 내용과도 같은데 제가 엣지 컴퓨팅에 대해선 지식이 없어서 구체적인 답변을 생각하지 못했습니다. 혹시 코치님이라면 이 질문에 대해서 어떤 의견이신지 궁금합니다.