과제 체크포인트
배포 링크
기본과제 (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 (상품 상세 페이지들)
- 빌드 타임 페이지 생성
- 파일 시스템 기반 배포
회고
왜 매주차 과제가 저에게는 고비일까요. 이번 과제도 겨우겨우 턱걸이로 완성했습니다. 처음 과제 주제를 맞이했을 때는 재밌을 것 같다고 생각했고, SSR 서버를 직접 구현해본다니 잘 만들어보고 싶었습니다..! 그러나 많은 허들 앞에서 무너져 어떻게든 패스만 받기 위해 주먹구구식으로 구현했습니다...
이번 회고는 과제를 진행하기에 급급하여 부족했던 학습 내용을 채우는 것을 위주로 회고를 작성해보려고 합니다!
구현하면서 새롭게 알게 된 개념
1. 클라이언트 코드를 서버 Node 환경에서 실행하면 왜 안되나?
서버를 구현하며 처음에는 html을 페이지 요청 경로에 맞는 페이지 컴포넌트를 실행하여 반환하게 하면 어떻게될까하여 아래 코드처럼 해봤습니다.
const html = HomePage();
이 코드 때문에 아래 에러가 발생했습니다.
Error [ERR_UNSUPPORTED_DIR_IMPORT]: Directory import '/Users/angielee/Desktop/hanghae/front_6th_chapter4-1/packages/vanilla/src/components' is not supported resolving ES modules imported from /Users/angielee/Desktop/hanghae/front_6th_chapter4-1/packages/vanilla/src/pages/HomePage.js
단순히 생각해보자면 HomePage 컴포넌트가 템플릿 리터럴의 html 코드를 반환하고 있으니까 페이지 컴포넌트를 실행하면 html 코드를 만들 수 있는 것 아닌가? 싶지만..이 얘기는 페이지 컴포넌트가 실행되는 CSR 환경과 동일한 클라이언트 사이드, 즉 브라우저 환경일 때만 예상한대로 동작합니다.
하지만 서버에는 dom도 없고 window도 없기 때문에 기존에 CSR로 동작하던 앱을 그대로 서버 환경에서 실행하려고 하면 여러 문제가 발생합니다.
첫째, HomePage 컴포넌트 내부에서 사용하는 router.query나 document.getElementById 같은 브라우저 전용 API들이 Node.js 환경에서는 존재하지 않아 에러가 발생합니다. 둘째, ES 모듈 시스템에서 import { ProductList, SearchBar } from "../components"처럼 디렉토리를 직접 import하는 방식이 Node.js에서는 지원되지 않습니다. Node.js는 정확한 파일 경로와 확장자를 요구하기 때문에 모듈 해석 오류가 발생합니다. 셋째, withLifecycle로 감싸진 라이프사이클 함수들이 DOM 이벤트나 브라우저 API에 의존하고 있어 서버 환경에서는 정상 동작하지 않습니다.
1) 브라우저 API 시뮬레이션 환경 구축
// packages/vanilla/src/utils/setupJsDom.js
export async function setupServerJsdom() {
const dom = new JSDOM(`<!DOCTYPE html>...`);
// 브라우저 전역 객체들을 Node.js에 주입
globalThis.window = dom.window;
globalThis.document = dom.window.document;
globalThis.localStorage = dom.window.localStorage;
globalThis.Element = dom.window.Element;
// ... 기타 브라우저 API들
}
Node.js 환경에서 존재하지 않는 DOM을 모킹하기 위해 이렇게 JSDOM을 사용해서 서버에 가짜 브라우저 환경을 만들어 document, window 등의 API를 사용할 수 있게 합니다. jsdom 라이브러리를 사용하여 가상의 HTML 문서와 DOM 트리를 메모리에 생성하고, 브라우저에서만 사용 가능한 DOM API들을 Node.js 환경에서도 호출할 수 있도록 시뮬레이션합니다.
이를 통해 서버 사이드 렌더링 과정에서 컴포넌트들이 document.getElementById(), localStorage, sessionStorage 같은 브라우저 전용 API를 사용하더라도 에러 없이 실행될 수 있습니다.
2) 환경별 코드 분기 처리
// utils/envUtils.js
export const isServer = import.meta.env.SSR;
// HomePage.js에서 환경별 분기
const { search: searchQuery, limit, sort, category1, category2 } =
isServer ? query : router.query; // 서버에서는 파라미터로 받은 query 사용
const products = serverData?.products || productState.products; // 서버 데이터 우선
기존 코드대로 router.query를 사용하면 에러가 나기 때문에, 서버 환경에서는 브라우저 라우터 대신 서버에서 전달받은 파라미터로 전달받아 사용하도록 분기 처리했습니다.
3) 모듈 경로 문제 해결
디렉토리 import 방식이 Node.js에서 지원되지 않기 때문에
// HomePage.js 내부 - Node.js에서 문제가 되는 부분
import { ProductList, SearchBar } from "../components"; // ❌ 디렉토리 import
import { router, withLifecycle } from "../router"; // ❌ 디렉토리 import
이렇게 HomePage.js 내부의 import문에서 에러가 발생합니다.
// 직접 import 대신 Vite를 통한 모듈 로드
const { render } = await vite.ssrLoadModule("/src/main-server.js");
그래서 이렇게 Vite에서 지원하는 ssrLoadModule 메서드를 사용하여 모듈을 import하면 Vite가 자동으로 모듈 경로를 해석하여 변환해주어 import문에서 발생하던 에러를 해결할 수 있습니다.
ssrLoadModule은 단순히 모듈을 로드하는 함수가 아니라, 브라우저용 코드를 서버 환경에서 실행 가능하게 만드는 핵심 도구입니다.
직접 import를 시도하면 모듈 해석, 환경 호환성, 의존성 관리 등 수많은 문제가 발생하지만, ssrLoadModule을 사용하면 Vite가 이 모든 복잡성을 자동으로 해결해주어 개발자는 SSR 구현에만 집중할 수 있게 됩니다.
2. 서버 사이드에서는 상대 경로 URL을 파싱할 수 없다?
API 요청 경로를 파싱할 수 없다는 에러가 발생했습니다.
Prefetch error: TypeError: Failed to parse URL from /api/products?page=1&limit=20&sort=price_asc
기존에 CSR 환경에서 동작하던 Mock 서버의 API 라우트가 상대 경로로 작성되어있는데, 이 경로에 대한 해석이 SSR 환경에서는 예상과 다르게 해석되기 때문에 파싱할 수 없다는 에러가 발생한 것입니다.
에러가 발생한 정확한 시점
- 사용자가 브라우저에서 홈페이지 접속 (http://localhost:5174/)
- Express 서버가 SSR 렌더링 시작
- HomePage 컴포넌트의 prefetch 함수 실행
- getProducts() 호출로 상품 데이터 미리 가져오기 시도
- fetch('/api/products?...')에서 상대 경로 파싱 실패
- Node.js 환경에서 '/api/products'를 절대 URL로 변환할 수 없어 에러 발생
=== CSR 환경 시뮬레이션 (브라우저) ===
현재 페이지: http://localhost:5173/
상대 경로: /api/products
브라우저 해석 결과: http://localhost:5173/api/products
=== SSR 환경 (Node.js 서버) ===
현재 환경: Node.js 서버
상대 경로: /api/products
Node.js 해석 실패: Invalid URL
CSR 환경(브라우저)에서의 상대 경로 해석 과정
현재 브라우저에서 실행되는 컨텍스트가 아래와 같을 때,
// 브라우저에서 실행되는 상황
console.log(window.location.href); // "http://localhost:5173/"
console.log(window.location.origin); // "http://localhost:5173"
console.log(window.location.host); // "localhost:5173"
fetch를 호출 시 브라우저는 아래와 같은 상대 경로를
// CSR에서 API 호출
fetch('/api/products?page=1&limit=20&sort=price_asc');
현재 브라우저 환경의 origin과 상대경로를 더하여 최종 해석된 URL을 사용합니다.
- 현재 origin: http://localhost:5173/
- 상대 경로: /api/products?page=1&limit=20&sort=price_asc
- 결합 결과: http://localhost:5173/api/products?page=1&limit=20&sort=price_asc
// 브라우저의 내부 동작 (의사코드)
const resolveURL = (relativePath) => {
const baseURL = window.location.origin; // "http://localhost:5173"
return new URL(relativePath, baseURL); // 자동으로 결합
};
console.log(resolveURL('/api/products?page=1&limit=20&sort=price_asc'));
// → URL {
// href: "http://localhost:5173/api/products?page=1&limit=20&sort=price_asc",
// pathname: "/api/products",
// search: "?page=1&limit=20&sort=price_asc"
// }
SSR 환경(서버)에서의 상대 경로 해석 과정
서버 환경에서의 실행 컨텍스트가 아래와 같고, 웹 컨텍스트에 대한 정보가 전혀 없습니다!
// Node.js 서버에서의 상황
console.log(process.cwd()); // "/Users/.../front_6th_chapter4-1"
console.log(__dirname); // "/Users/.../packages/vanilla"
console.log(typeof window); // "undefined"
그렇기 때문에 fetch를 실행할 때, fetch는 상대 경로를 해석하기 위해 브라우저 경로의 origin을 필요로 하는데, origin에 대한 정보가 없으므로 Base URL 없이 상대 경로만 제공됩니다.
// 브라우저의 내부 동작 (의사코드)
const resolveURL = (relativePath) => {
const baseURL = window.location.origin; // "http://localhost:5173"
return new URL(relativePath, baseURL); // Base URL 없이 상대 경로만 제공
};
console.log(resolveURL('/api/products?page=1&limit=20&sort=price_asc'));
// "Invalid URL"
// '/api/products'는 상대 경로이므로 base URL 없이는 파싱 불가
그러므로! 서버 환경에서 상대 경로가 정상적으로 해석되려면 개발자가 상대 경로의 기준점을 명시적으로 제공해야합니다.
// packages/vanilla/src/api/productApi.js
const getApiBaseUrl = () => {
if (typeof window === "undefined") {
// SSR 환경: 명시적으로 Base URL 제공
return "http://localhost:5174";
} else {
// CSR 환경: 브라우저가 자동 해석하도록 빈 문자열
return "";
}
};
// 사용
export async function getProduct(productId) {
const baseUrl = await getApiBaseUrl();
const response = await fetch(`${baseUrl}/api/products/${productId}`);
가장 어려웠던 부분과 해결 과정
홈 화면을 요청했는데, 왜 상세 페이지가 보이지?
정말 이상한 현상이 발생했습니다. 로컬에서 SSR을 개발 서버에서 실행하면 아무 문제가 없는데, 테스트만 돌리면 SSR에서 에러가 났습니다. 심지어 에러는 상세 페이지에 대한 테스트에서 html이 홈 화면 html을 받거나, 홈 화면에서 상세 페이지에 대한 html을 받고 있었습니다.
상태 공유(State Sharing) 문제
테스트 환경에서 여러 테스트 케이스가 동시에 또는 순차적으로 실행될 때 서버 사이드 렌더링 과정에서 전역 상태나 모듈 상태가 공유되면서 발생하는 현상입니다.
개발 서버에서는 각 요청이 독립적으로 처리되고 브라우저에서 페이지 간 이동 시 완전히 새로운 컨텍스트가 생성되지만, 테스트 환경에서는 Node.js 프로세스 내에서 여러 SSR 렌더링이 빠르게 연속으로 실행됩니다. 이때 라우터 상태, 스토어 상태, 또는 전역 변수들이 이전 테스트의 실행 결과를 그대로 유지하고 있어서 다음 테스트에 영향을 미치게 됩니다.
에러가 발생했을 때 코드를 보면 ServerRouter를 다음과 같이 싱글톤 패턴으로 전역 객체로 구현하여 사용했습니다.
// packages/vanilla/src/lib/ServerRouter.js
// 모듈 레벨에서 서버 라우터 인스턴스를 생성하여 단일 인스턴스를 공유하여 사용
class ServerRouter {
// ...ServerRouter 구현체
}
export const serverRouter = new ServerRouter()
serverRouter를 모듈 레벨에서 싱글톤으로 생성하면, Node.js 프로세스가 살아있는 동안 동일한 인스턴스가 계속 재사용됩니다.
상세 페이지에 대한 테스트에서 상품 상세 페이지 /product/12345/를 렌더링하면 싱글톤 serverRouter의 내부 상태가 #pathname = "/product/12345/", #route = ProductDetailPage, #query = {} 등으로 설정됩니다.이어서 홈 페이지에 대한 테스트에서 홈 페이지 /를 렌더링할 때 새로운 URL로 start() 메서드를 호출하지만, 이전 상태가 완전히 정리되지 않거나 다른 전역 상태와 얽혀서 예상치 못한 결과가 나타납니다.
특히 라우터뿐만 아니라 productStore, cartStore 같은 다른 전역 객체들도 마찬가지로 이전 테스트의 데이터를 유지하고 있기 때문에, 홈 페이지에서 이전에 로드된 상품 상세 정보가 표시되거나 반대로 상세 페이지에서 홈 페이지의 상품 목록이 나타나는 현상이 발생합니다.
서버 요청마다 새로운 ServerRouter 인스턴스를 생성
이 문제를 해결하기 위해서는 싱글톤 패턴을 포기하고 각 SSR 렌더링마다 새로운 ServerRouter 인스턴스를 생성하도록 했습니다.
// packages/vanilla/src/main-server.js
export async function render(url) {
const serverRouter = new ServerRouter();
serverRouter.addRoute("/", HomePage);
serverRouter.addRoute("/product/:id/", ProductDetailPage);
serverRouter.addRoute("/404", NotFoundPage);
serverRouter.start(url);
// ..중략
const html = await serverRouter.target(data, query);
return { html, head, initialData: data };
}
이렇게 하면 각 렌더링 요청마다 완전히 독립적인 라우터 컨텍스트가 생성되어 이전 요청의 상태가 다음 요청에 영향을 주지 않습니다.
결국 이 문제는 싱글톤 패턴의 전형적인 부작용으로, 서버 사이드 렌더링과 같이 동일한 코드가 여러 번 실행되는 환경에서는 상태 격리가 매우 중요하다는 것을 보여주는 사례입니다. 각 렌더링 요청이 완전히 독립적인 컨텍스트를 가져야 하므로, 전역 상태보다는 함수 스코프 내에서 생성되는 지역 상태나 의존성 주입 패턴을 사용하는 것이 더 좋다는 것을 배웠습니다!
성능 최적화 관점에서의 인사이트
학습 갈무리
Q1. 현재 구현한 SSR/SSG 아키텍처에서 확장성을 고려할 때 어떤 부분을 개선하시겠습니까?
캐싱 시스템 도입이 가장 중요한 개선점입니다. 현재는 매 요청마다 서버에서 렌더링하는데, 자주 변경되지 않는 페이지들은 메모리나 Redis에 캐싱하여 응답 속도를 크게 개선할 수 있습니다. 또한 컴포넌트 단위 캐싱을 도입하여 페이지 전체가 아닌 개별 컴포넌트별로 캐시 전략을 다르게 적용할 수 있을 것 같습니다.
Q2. Express 서버 대신 다른 런타임(Cloudflare Workers, Vercel Edge Functions 등)을 사용한다면 어떤 점을 수정해야 할까요?
Cloudflare Workers는 더욱 제약이 심해서 현재의 폴리필 방식으로는 작동하지 않습니다. setupJsDom.js에서 globalThis.window를 전역에 추가하는 대신 Worker 환경에 맞는 별도의 추상화 레이어를 구축해야 합니다.
Vercel Edge Functions의 경우 현재 Node.js 22 기반의 fetch API를 그대로 활용할 수 있지만, 파일 시스템 접근이 제한되므로 static-site-generate.js의 fs.writeFileSync 부분을 클라우드 스토리지 API로 대체해야 합니다. Cold Start 문제는 모든 서버리스 환경의 공통 이슈로, 번들 크기를 최소화하고 동적 import를 활용해 필요한 코드만 로드하도록 개선해야 합니다.
Q3. 현재 구현에서 성능 병목이 될 수 있는 지점은 어디이고, 어떻게 개선하시겠습니까?
첫 번째 병목점은 SSR 렌더링 과정입니다. 현재 main-server.js에서 매 요청마다 vite.ssrLoadModule을 호출하고 전체 컴포넌트 트리를 렌더링합니다. 이를 개선하기 위해 청크 단위 로딩과 컴포넌트별 lazy loading을 도입할 수 있습니다.
두 번째는 데이터 프리페칭 단계입니다. HomePage.prefetch에서 Promise.all([getProducts(query), getCategories()])로 병렬 처리하고 있지만, 데이터 의존성이 없는 경우 더 세밀한 최적화가 가능합니다. 또한 현재 MSW를 통한 Mock 응답도 실제 프로덕션에서는 GraphQL DataLoader 패턴이나 배치 쿼리로 N+1 문제를 해결해야 합니다.
세 번째는 하이드레이션 과정입니다. 현재 hydrateStore에서 전체 초기 데이터를 한 번에 로드하는데, 이를 점진적 하이드레이션으로 개선하여 사용자가 상호작용하는 부분부터 우선 활성화할 수 있습니다.
Q4. 1000개 이상의 상품 페이지를 SSG로 생성할 때 고려해야 할 사항은 무엇입니까?
현재 static-site-generate.js는 순차적으로 페이지를 생성하므로 병렬 처리가 필수입니다. for (const page of pages) 루프를 Promise.all이나 pMap으로 개선하되, 메모리 사용량을 고려해 동시 실행 수를 제한해야 합니다.
증분 빌드 시스템도 중요합니다. 모든 상품을 매번 재생성하는 대신, 변경된 상품만 감지하여 해당 페이지만 다시 생성하는 로직이 필요합니다. 이를 위해 해시 기반 변경 감지나 타임스탬프 비교를 활용할 수 있습니다.
또한 현재는 getProducts()로 모든 상품을 메모리에 로드하는데, 1000개 이상에서는 스트리밍 방식으로 페이지 단위로 데이터를 가져와 메모리 사용량을 제한해야 합니다. CDN 캐시 무효화 전략도 중요한데, 상품별로 독립적인 캐시 키를 설정하여 특정 상품 변경 시 전체 캐시를 무효화하지 않도록 해야 합니다.
Q5. Hydration 과정에서 사용자가 느낄 수 있는 UX 이슈는 무엇이고, 어떻게 개선할 수 있을까요?
현재 hydrateStore에서 delete window.__INITIAL_DATA__로 초기 데이터를 제거하는데, 이 과정에서 Flash of Unstyled Content (FOUC) 문제가 발생할 수 있습니다. 서버에서 렌더링된 HTML과 클라이언트에서 하이드레이션된 결과가 일시적으로 다를 수 있기 때문입니다.
인터랙션 차단 시간도 중요한 이슈입니다. 현재는 main.tsx에서 hydrateStore() 완료 후에야 React 앱이 마운트되므로, 큰 초기 데이터에서는 사용자가 오랫동안 클릭할 수 없는 상태가 됩니다. 이를 Progressive Enhancement로 개선하여 기본 HTML부터 상호작용 가능하게 하고, JavaScript가 로드되면서 점진적으로 기능을 활성화해야 합니다.
Skeleton UI나 Loading Spinner 같은 로딩 상태 표시도 현재는 구현되어 있지 않습니다. productStore의 loading 상태를 활용하여 하이드레이션 진행 상황을 사용자에게 명확히 보여주어야 합니다.
Q6. 이번 과제에서 학습한 내용을 실제 프로덕션 환경에 적용할 때 추가로 고려해야 할 사항은?
보안 측면에서 현재 window.__INITIAL_DATA__에 모든 데이터를 노출하는 방식은 민감한 정보 유출 위험이 있습니다. 사용자별 데이터는 별도 API 호출로 분리하고, CSP(Content Security Policy) 설정으로 XSS 공격을 방어해야 합니다.
Q7. Next.js 같은 프레임워크 대신 직접 구현한 SSR/SSG의 장단점은 무엇인가요?
장점으로는 완전한 제어권을 들 수 있습니다. 현재 구현한 ServerRouter나 createStore 같은 라이브러리는 프로젝트 요구사항에 정확히 맞춰져 있고, 불필요한 기능이 없어 번들 크기가 작습니다. 또한 학습 효과가 뛰어나 SSR/SSG의 내부 동작 원리를 완전히 이해할 수 있습니다.
단점으로는 유지보수 비용이 큽니다. Next.js는 수많은 엣지 케이스와 브라우저 호환성 문제를 이미 해결했지만, 직접 구현한 코드는 이런 문제들을 하나씩 마주치며 해결해야 합니다. 생태계 지원도 부족해서 이미지 최적화, 국제화, 성능 모니터링 같은 기능을 모두 직접 구현해야 합니다.
커뮤니티와 문서화 측면에서도 Next.js는 풍부한 자료와 플러그인을 제공하지만, 직접 구현한 시스템은 팀 내에서만 알고 있는 "블랙박스"가 될 위험이 있습니다.
Q8. Next.js 를 이용하여 SSG 방식으로 배포하려면 어떻게 해야 좋을까요?
Next.js로 마이그레이션한다면 현재의 static-site-generate.js 로직을 getStaticPaths와 getStaticProps로 변환해야 합니다. 상품 목록은 getStaticPaths에서 생성하고, 각 상품 데이터는 getStaticProps에서 프리페치합니다.
현재 프로젝트 구조를 Next.js에 맞게 변환하면:
- packages/vanilla/src/pages/ → pages/ 디렉토리 구조
- ServerRouter → Next.js의 내장 라우터로 대체
- productStore → SWR이나 React Query로 상태 관리 개선
- setupJsDom → 불필요 (Next.js가 자동 처리)
배포 전략으로는 Vercel이나 Netlify에서 next export로 정적 파일을 생성하거나, **ISR(Incremental Static Regeneration)**을 활용하여 상품 변경 시 해당 페이지만 재생성하는 방식을 권장합니다. 현재 MSW 기반의 Mock API는 실제 API 서버나 Headless CMS로 대체하여 데이터 소스를 명확히 해야 합니다.
학습 연계
다음 학습 목표
"Edge Computing과 CDN 기반 SSR 최적화" - 현재 Node.js 서버 기반의 SSR을 Cloudflare Workers나 Vercel Edge Functions 같은 Edge 환경으로 이전했을 때의 성능 향상과 구현 방식 차이점을 학습하고 싶습니다. 특히 setupJsDom.js에서 구현한 브라우저 폴리필 방식이 Edge 환경에서는 어떻게 대체되어야 하는지 깊이 알고 싶습니다.
리뷰 받고 싶은 내용
과제 피드백
수고했습니다. 이번 과제는 SSR과 SSG를 직접 구현해보면서 렌더링 전략의 차이점과 성능 최적화 방법을 체험하는데 목적이 있었습니다.
"왜 매주차 과제가 저에게는 고비일까요. 이번 과제도 겨우겨우 턱걸이로 완성했습니다." ㅋㅋ 그만큼 가파른 성장을 하고 있다는 거겠죠! 완성을 했다는것 자체가 아주 중요합니다. 한번 하고 나면 두번은 쉬울 거에요.
"...하지만 서버에는 dom도 없고 window도 없기 때문에 기존에 CSR로 동작하던 앱을 그대로 서버 환경에서 실행하려고 하면 여러 문제가 발생합니다." 맞아요! 같은 코드를 사용하지만 Node와 Browser가 달라지는 부분을 추상화하는 것도 SSR의 가장 큰 미션이죠. JSDOM을 활용한 브라우저 API 시뮬레이션 환경 구축이나 ssrLoadModule을 통한 모듈 경로 문제 해결 등 실질적인 해결책들을 직접 구현해보면서 SSR의 복잡성을 체험하게 되었으리라 생각합니다.
상대 경로 URL 파싱 문제도 흥미로운 케이스였어요. CSR과 SSR 환경에서 같은 /api/products 경로가 어떻게 다르게 해석되는지 명확하게 분석했고, getApiBaseUrl() 함수로 환경별 분기 처리한 해결책도 현실적입니다.
특히 테스트 환경에서 발생한 상태 공유 문제 분석이 깊이 있었어요. "홈 화면을 요청했는데 왜 상세 페이지가 보이지?"라는 의문에서 시작해서 싱글톤 패턴의 부작용까지 찾아낸 과정이 훌륭했습니다. 각 SSR 렌더링마다 새로운 Router 인스턴스를 생성하는 해결책도 맞고요.
이번 과제를 통해 단순히 기능을 구현하는 것을 넘어서 왜 이런 문제들이 발생하는지, 어떻게 해결해야 하는지까지 깊이 탐구해보신 경험이 앞으로 성능 최적화할 때 큰 자산이 될 거예요. 수고하셨습니다. 다음 과제도 화이팅입니다!