과제 체크포인트
배포 링크
기본: https://yunwoo-yu.github.io/front_6th_chapter4-1/vanilla/ 심화: https://yunwoo-yu.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 (상품 상세 페이지들)
- 빌드 타임 페이지 생성
- 파일 시스템 기반 배포
구현 과정 돌아보기
가장 어려웠던 부분과 해결 과정
vite를 통한 epxress를 이용한 SSR렌더링 설정은 문서가 있어 수월하게 진행했지만 그 외 라우터, Store 동기화 같은 부분까지 모두 어려웠던 것 같습니다.
Rotuer는 기존 Router를 활용해 브라우저 환경에만 존재하는 값들을 지우고 인자로 받아 사용하도록 만들었습니다.
export const render = async (url, query) => {
try {
// 1. 라우터 시작
router.start(url, query);
...etc
}
start(url = "/", query = {}) {
this.#currentQuery = query;
this.#route = this.#findRoute(url);
}
router.start를 render쪽에서 url과 query를 받아 넣어 router내 필요한 값들을 저장했습니다. render 전에 router를 설정하고 router에서 path, query등 prefetch에 필요한 값들을 적용시켰습니다.
// SSR 데이터를 클라이언트 스토어에 hydrate
function hydrateFromSSRData() {
if (typeof window === "undefined" || !window.__INITIAL_DATA__) {
return;
}
try {
const initialData = window.__INITIAL_DATA__;
const currentPath = window.location.pathname;
// 홈페이지 hydration
if (currentPath === "/" && initialData.products) {
productStore.dispatch({
type: PRODUCT_ACTIONS.SETUP,
payload: {
products: initialData.products || [],
totalCount: initialData.totalCount || 0,
categories: initialData.categories || {},
currentProduct: null,
relatedProducts: [],
loading: false,
error: null,
status: "done",
},
});
}
// 상품 상세 페이지 hydration
else if (currentPath.includes("/product/") && initialData.product) {
productStore.dispatch({
type: PRODUCT_ACTIONS.SETUP,
payload: {
products: [],
totalCount: 0,
categories: {},
currentProduct: initialData.product,
relatedProducts: initialData.relatedProducts || [],
loading: false,
error: null,
status: "done",
},
});
}
// hydration 완료 플래그
window.__HYDRATED__ = true;
} catch (error) {
console.error("💥 SSR 데이터 hydration 실패:", error);
}
}
hydration는 main.js에서 initRender 전에 해당 함수로 Store를 동기화해주고 mount 시 데이터를 불러와 초기화 되는 부분은 window.HYDRATED 변수를 이용해 분기처리해 hydration이 진행됐을 경우 실행되지 않도록 했습니다.
구현하면서 새롭게 알게 된 개념
- react에서 ServerCompoent를 지원하는데 어떻게 쓰지라고 생각했었는데 이용하려면 별도의 express서버가 필요하다는 걸 알았습니다
- Hydration의 데이터는 window객체를 통해 들어간다는 걸 알았습니다.
성능 최적화 관점에서의 인사이트
사실 빨리 통과해야 한다는 압박감에 여러 요소들을 충분히 검토하지 못했습니다. 하지만 데이터 변화가 없는 정적 콘텐츠로만 구성된 페이지의 경우, SSG(Static Site Generation)를 활용하면 빌드 시점에 미리 생성되어 런타임에는 단순한 파일 서빙만으로 처리가 가능하므로 성능상 이점이 클 것으로 판단됩니다. (서버 부하도 감소)
또 CDN 적용 시 CDN은 파일 시스템 기반으로 캐싱하기에 효율적이라고 생각이 들었습니다.
학습 갈무리
Q1. 현재 구현한 SSR/SSG 아키텍처에서 확장성을 고려할 때 어떤 부분을 개선하시겠습니까?
지금 페이지 마다 onMount 시 window === "undefined" 를 통해 초기 데이터 fetching 여부를 결정하는데 해당 부분을 모든 페이지에서 반복하지 않도록 onMount 함수를 개선해 볼 것 같습니다.
Q2. Express 서버 대신 다른 런타임(Cloudflare Workers, Vercel Edge Functions 등)을 사용한다면 어떤 점을 수정해야 할까요?
잘 모르겠습니다ㅎㅎ.. 추측으로는 데이터 관련된 prefetching 들을 해당 런타임에서 내려주는걸로 해야하지 않을까 생각합니다.
Q3. 현재 구현에서 성능 병목이 될 수 있는 지점은 어디이고, 어떻게 개선하시겠습니까?
음.. 잘 모르겠습니다.....
Q4. 1000개 이상의 상품 페이지를 SSG로 생성할 때 고려해야 할 사항은 무엇입니까?
SSG가 적합하지 않을 것 이라고 생각이 들긴하는데.. 만약 반드시 SSG로 해야한다면 번들러쪽에서 빌드할 때 변화가 있는것만 재생성하도록 contenthash 같은것들을 잘 써야할 것 같습니다.
Q5. Hydration 과정에서 사용자가 느낄 수 있는 UX 이슈는 무엇이고, 어떻게 개선할 수 있을까요?
Hydration이 완료되기 전 자바스크립트는 다운되지 않아 화면은 보여도 이벤트는 사용할 수 없는 문제가 있습니다. 개선 방법은 잘모르겠습니다ㅎㅎ..
Q6. 이번 과제에서 학습한 내용을 실제 프로덕션 환경에 적용할 때 추가로 고려해야 할 사항은?
window 객체로 넘긴 데이터들이 그대로 나와 만약 유저정보같은 페이지라면 문제가 될 것 같습니다. (보안)
Q7. Next.js 같은 프레임워크 대신 직접 구현한 SSR/SSG의 장단점은 무엇인가요?
장점: 번들이 작아진다 단점: 너무 미흡해 여러 케이스에 대해 대응하려면 또 직접 다 구현해야한다.
Q8. Next.js 를 이용하여 SSG 방식으로 배포하려면 어떻게 해야 좋을까요?
getStaticProps, getStaticPaths 함수를 이용해 필요한 데이터,Path를 미리 입력해 빌드시 SSG로 생성해주고 vercel을 통해 배포하는것이 자동으로 SSG를 인식해 좋을 것 같습니다.
개선하고 싶은 부분
페이지 마다 반복적인 window 체크, url에 따른 prefetch등 반복적으로 작성해야하는 부분들을 하드코딩이 아니게 작성해보고 싶습니다. 예를들면 페이지에 어떤 함수를 쓰면 해당 함수가 페이지에서 SSR에서 실행되는 함수로 인식해서 prefetch, store업데이트들을 할 수 있다던가 같은?
현재는 페이지가 추가될때마다 반복 문구가 작성되는 느낌이라 유지보수에 좋지 않을 것 같습니다.
다음 학습 목표
실제로 사용 가능할 정도로 업그레이드 하려면 어떤것들을 해야할지 궁금해 졌습니다!
실무 적용 계획
아직 실무에 직접적으로 적용해볼만한 곳은 없는 것 같습니다!
리뷰 받고 싶은 내용
- 테스트가 간헐적으로 로컬에서 실패하는데 이유가 궁금합니다!
- 현재 구조에서는 페이지가 추가될 때 마다 onMount에서 별도의 처리가 필요하고 main-server쪽 render 함수에도 if 같은 조건문이 계속 추가되는 구조인데 이게 아니라 동적(?)으로 하드코딩이 아닌 일관성있는 코드를 짜려면 어떤 구조로 가져갔어야할까요?
- 심화파트에서 useProductStore처럼 전역 스토어를 통했기에 express 서버에서 접근이 가능했는데요, 만약 useState를 사용하고 useEffect로 useState를 업데이트하는 코드였다면 SSR시에 컴포넌트의 데이터를 어떻게 전달할 수 있을까요?
과제 피드백
안녕하세요 윤우님! 9주차 과제 잘 진행해주셨네요 ㅎㅎ 고생하셨습니다.
테스트가 간헐적으로 로컬에서 실패하는데 이유가 궁금합니다!
일단 한 번에 너무 많은 서버를 띄워서 그럴 수도 있고, 제일 문제가 될 수 있는 부분은 동시성에 대한 처리입니다. store와 router를 ssr을 할 때 매번 재생성 해줘야 오류가 없는데, 이걸 재생성하지 않고 전역에 생성된걸 사용한다면 request1에서 만든 데이터를 request2에서 사용하는 경우가 생길 수 있어요! router도 마찬가지입니다.
request1에서 정의된 주소의 query가 request2에서 그대로 쓰일 수 있는거죠. 그러다보면 자연스럽게 오류로 이어질 수 있답니다!
현재 구조에서는 페이지가 추가될 때 마다 onMount에서 별도의 처리가 필요하고 main-server쪽 render 함수에도 if 같은 조건문이 계속 추가되는 구조인데 이게 아니라 동적(?)으로 하드코딩이 아닌 일관성있는 코드를 짜려면 어떤 구조로 가져갔어야할까요?
nextjs의 getServerSideProps 처럼 페이지별로 필요한 데이터를 가져오는 함수를 만들어서 사용할 수 있답니다 ㅎㅎ 과제 솔루션을 참고해보시면 좋을 것 같아요!
심화파트에서 useProductStore처럼 전역 스토어를 통했기에 express 서버에서 접근이 가능했는데요, 만약 useState를 사용하고 useEffect로 useState를 업데이트하는 코드였다면 SSR시에 컴포넌트의 데이터를 어떻게 전달할 수 있을까요?
렌더링 사이클 바깥에서 값을 생성한 다음에, props를 통해 전달하면 되겠죠!?
const App = (props) => {
const [initData, setInitData] = useState(() =. props.initData);
return (
// ... 렌더링 로직 수행 ...
)
}
const initData = { products: [{ ... }, { ... }], totalCount: 2 }
const render = () => {
const <App initData={initData} />
}
이런 느낌입니다!