jeongmingi123 님의 상세페이지[2팀 정민기] Chapter 4-1 성능 최적화

과제 체크포인트

배포 링크

기본 : https://jeongmingi123.github.io/front_6th_chapter4-1/vanilla/ 심화 : https://jeongmingi123.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 (상품 상세 페이지들)
  • 빌드 타임 페이지 생성
  • 파일 시스템 기반 배포

구현 과정 돌아보기

가장 어려웠던 부분과 해결 과정

// productUseCase.ts
export const loadProductsAndCategories = async () => {
  router.query = { current: undefined }; // 항상 첫 페이지로 초기화
  productStore.dispatch({
    type: PRODUCT_ACTIONS.SETUP,
    payload: {
      ...initialProductState,
      loading: true,
      status: "pending",
    },
  });

심화과제인 react 구현중에, router.query = { current: undefined }; 로 인해 렌더링 이슈가 발생하였고, 해당 코드를 지우면서 timeout이 계속 발생하여 react 심화과제 CSR 렌더링 이슈가 발생하여 CSR react 테스트 코드가 다 실패하였습니다.

router.query = { current: undefined };를 제거하면 라우터의 초기 상태가 빈 객체 {}가 되고, API 호출 시 current 파라미터가 전달되지 않아 서버와 동일한 기본값(1)이 사용됨. SSR과 CSR의 결과가 일치하게 되어 하이드레이션 이슈가 해결되었음. (이걸로 한 6시간쓴듯..)

구현하면서 새롭게 알게 된 개념

성능 최적화 관점에서의 인사이트

사실 과제 이해하는데 오래걸려서 .. 성능 최적화까지는 하지 못했음

학습 갈무리

Q1. 현재 구현한 SSR/SSG 아키텍처에서 확장성을 고려할 때 어떤 부분을 개선하시겠습니까?

현재 아키텍처의 한계점

  1. 단일 서버 의존성: Express 서버 하나에 모든 SSR 로직이 집중되어 있어 수평 확장이 어려움
  2. 메모리 기반 상태 관리: 서버 재시작 시 모든 상태가 초기화됨
  3. 동기식 렌더링: 모든 페이지가 순차적으로 렌더링되어 병목 발생
  4. 캐싱 전략 부재: 동일한 요청에 대한 캐싱 메커니즘 없음

개선 방안

  1. 마이크로서비스 아키텍처 도입

    • API 서버와 렌더링 서버 분리
    • 각 서비스별 독립적인 스케일링
    • 서비스 간 통신을 위한 gRPC 또는 HTTP/2 활용
  2. 캐싱 계층 구축

    // Redis 기반 페이지 캐싱
    const cacheKey = `page:${url}:${JSON.stringify(query)}`;
    const cached = await redis.get(cacheKey);
    if (cached) return JSON.parse(cached);
    
    const rendered = await render(url, query);
    await redis.setex(cacheKey, 300, JSON.stringify(rendered)); // 5분 캐시
    
  3. 비동기 렌더링 및 스트리밍

    // React 18의 Suspense와 스트리밍 활용
    const stream = renderToPipeableStream(React.createElement(App), {
      onShellReady() {
        res.setHeader('Content-Type', 'text/html');
        stream.pipe(res);
      }
    });
    
  4. CDN 통합

    • 정적 자산과 SSG 페이지를 CDN에 배포
    • Edge Computing을 통한 지역별 최적화

Q2. Express 서버 대신 다른 런타임(Cloudflare Workers, Vercel Edge Functions 등)을 사용한다면 어떤 점을 수정해야 할까요?

Cloudflare Workers로 마이그레이션 시 고려사항

  1. 런타임 제약사항

    // 현재: Node.js API 사용
    import fs from "node:fs/promises";
    
    // Workers: KV Storage 사용
    const data = await env.PRODUCTS_KV.get("products");
    
  2. Cold Start 최적화

    // 번들 크기 최소화
    import { renderToString } from "react-dom/server";
    // 필요한 부분만 import하여 번들 크기 줄이기
    
    // 웜업 전략
    export default {
      async fetch(request, env, ctx) {
        // 핫 패스 최적화
        if (request.url.includes('/api/health')) {
          return new Response('OK');
        }
        // ... 렌더링 로직
      }
    };
    
  3. 메모리 제한 대응

    // 대용량 데이터 처리 시 스트리밍 활용
    const stream = new ReadableStream({
      start(controller) {
        // 청크 단위로 데이터 전송
        controller.enqueue(chunk);
      }
    });
    

Vercel Edge Functions 활용

  1. Edge Runtime 최적화

    export const config = {
      runtime: 'edge',
      regions: ['iad1', 'sfo1'] // 지역별 배포
    };
    
    export default async function handler(req) {
      // Edge에서 실행되는 최적화된 렌더링
      const html = await renderToStaticMarkup(Component);
      return new Response(html, {
        headers: { 'Content-Type': 'text/html' }
      });
    }
    
  2. ISR (Incremental Static Regeneration) 활용

    // 빌드 타임 + 런타임 재생성
    export async function getStaticProps() {
      return {
        props: { data },
        revalidate: 60 // 60초마다 재생성
      };
    }
    

Q3. 현재 구현에서 성능 병목이 될 수 있는 지점은 어디이고, 어떻게 개선하시겠습니까?

주요 병목 지점

  1. 서버 렌더링 시 CPU 집약적 작업

    // 현재: 모든 상품을 메모리에서 필터링
    const filteredProducts = filterProducts(items, query);
    
    // 개선: 데이터베이스 쿼리 최적화
    const products = await db.products
      .where('title', 'like', `%${search}%`)
      .orderBy('price', sort === 'price_asc' ? 'asc' : 'desc')
      .limit(limit)
      .offset(offset);
    
  2. 메모리 누수 위험

    // 현재: 전역 변수로 데이터 보관
    let template;
    let render;
    
    // 개선: WeakMap 활용 및 메모리 관리
    const renderCache = new WeakMap();
    const templateCache = new Map();
    
    // 주기적 캐시 정리
    setInterval(() => {
      if (templateCache.size > 100) {
        templateCache.clear();
      }
    }, 60000);
    
  3. 번들 크기 최적화

    // 현재: 전체 라이브러리 import
    import { renderToString } from "react-dom/server";
    
    // 개선: Tree Shaking 및 코드 스플리팅
    const { renderToString } = await import("react-dom/server");
    
    // Vite 설정 최적화
    export default defineConfig({
      build: {
        rollupOptions: {
          output: {
            manualChunks: {
              vendor: ['react', 'react-dom'],
              utils: ['lodash', 'date-fns']
            }
          }
        }
      }
    });
    

성능 모니터링 도입

// 성능 메트릭 수집
const performanceObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`${entry.name}: ${entry.duration}ms`);
  }
});

performanceObserver.observe({ entryTypes: ['measure'] });

Q4. 1000개 이상의 상품 페이지를 SSG로 생성할 때 고려해야 할 사항은 무엇입니까?

빌드 시간 최적화

  1. 병렬 처리 구현

    // 현재: 순차 처리
    for (const productId of productIds) {
      await generateProductPage(productId);
    }
    
    // 개선: 병렬 처리 (배치 단위)
    const BATCH_SIZE = 10;
    const batches = chunk(productIds, BATCH_SIZE);
    
    for (const batch of batches) {
      await Promise.all(
        batch.map(productId => generateProductPage(productId))
      );
    }
    
  2. 증분 빌드 구현

    // 변경된 상품만 재빌드
    const lastBuildTime = await getLastBuildTime();
    const changedProducts = await getChangedProducts(lastBuildTime);
    
    if (changedProducts.length > 0) {
      await generateProductPages(changedProducts);
    }
    
  3. 메모리 사용량 관리

    // 스트리밍 방식으로 파일 생성
    const writeStream = fs.createWriteStream(outputPath);
    
    for (const product of products) {
      const html = await renderProduct(product);
      writeStream.write(html);
      
      // 메모리 압박 시 가비지 컬렉션 유도
      if (process.memoryUsage().heapUsed > 500 * 1024 * 1024) {
        global.gc && global.gc();
      }
    }
    

CDN 캐시 무효화 전략

// 상품 업데이트 시 관련 페이지 무효화
async function invalidateProductCache(productId) {
  const paths = [
    `/product/${productId}/`,
    `/`, // 홈페이지도 무효화 (관련 상품 목록)
  ];
  
  await Promise.all(
    paths.map(path => cdn.purge(path))
  );
}

부분 재빌드 구현

// 상품별 의존성 그래프 관리
const dependencyGraph = {
  'product-1': ['home', 'category-electronics'],
  'product-2': ['home', 'category-clothing']
};

async function rebuildAffectedPages(changedProductId) {
  const affectedPages = dependencyGraph[changedProductId] || [];
  await Promise.all(
    affectedPages.map(page => rebuildPage(page))
  );
}

Q5. Hydration 과정에서 사용자가 느낄 수 있는 UX 이슈는 무엇이고, 어떻게 개선할 수 있을까요?

주요 UX 이슈

  1. 인터랙션 차단 시간

    // 현재: 전체 앱이 하이드레이션 완료까지 대기
    
    // 개선: Progressive Enhancement
    const ProgressiveApp = () => {
      const [isHydrated, setIsHydrated] = useState(false);
      
      useEffect(() => {
        setIsHydrated(true);
      }, []);
      
      return (
        <div>
          {/* 서버 렌더링된 콘텐츠는 즉시 표시 */}
          <StaticContent />
          {isHydrated && <InteractiveContent />}
        </div>
      );
    };
    
  2. 레이아웃 시프트 (CLS) 방지

    // Skeleton UI로 공간 확보
    const ProductCardSkeleton = () => (
      <div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
        <div className="aspect-square bg-gray-200 animate-pulse" />
        <div className="p-3">
          <div className="h-4 bg-gray-200 rounded animate-pulse mb-2" />
          <div className="h-3 bg-gray-200 rounded animate-pulse mb-2" />
          <div className="h-6 bg-gray-200 rounded animate-pulse" />
        </div>
      </div>
    );
    
  3. 로딩 상태 표시

    // 하이드레이션 진행 상태 표시
    const HydrationProgress = () => {
      const [progress, setProgress] = useState(0);
      
      useEffect(() => {
        const interval = setInterval(() => {
          setProgress(prev => Math.min(prev + 10, 90));
        }, 100);
        
        return () => clearInterval(interval);
      }, []);
      
      return (
        <div className="fixed top-0 left-0 w-full h-1 bg-gray-200">
          <div 
            className="h-full bg-blue-600 transition-all duration-300"
            style={{ width: `${progress}%` }}
          />
        </div>
      );
    };
    

Islands Architecture 적용

// 필요한 부분만 하이드레이션
const ProductCard = ({ product, isInteractive = false }) => {
  if (!isInteractive) {
    // 정적 렌더링
    return <StaticProductCard product={product} />;
  }
  
  // 인터랙티브 렌더링
  return <InteractiveProductCard product={product} />;
};

// 클라이언트에서 필요한 카드만 하이드레이션
document.addEventListener('DOMContentLoaded', () => {
  const visibleCards = document.querySelectorAll('.product-card[data-in-viewport="true"]');
  visibleCards.forEach(card => {
    hydrateCard(card);
  });
});

Q6. 이번 과제에서 학습한 내용을 실제 프로덕션 환경에 적용할 때 추가로 고려해야 할 사항은?

모니터링 및 로깅 체계

  1. 성능 모니터링

    // Core Web Vitals 측정
    import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
    
    getCLS(console.log);
    getFID(console.log);
    getFCP(console.log);
    getLCP(console.log);
    getTTFB(console.log);
    
    // 서버 성능 모니터링
    const serverMetrics = {
      renderTime: Date.now() - startTime,
      memoryUsage: process.memoryUsage(),
      cpuUsage: process.cpuUsage()
    };
    
    await metricsCollector.record('ssr_performance', serverMetrics);
    
  2. 에러 핸들링 및 Fallback 전략

    // Circuit Breaker 패턴
    class RenderService {
      constructor() {
        this.failureCount = 0;
        this.lastFailureTime = 0;
        this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
      }
      
      async render(url, query) {
        if (this.state === 'OPEN') {
          if (Date.now() - this.lastFailureTime > 60000) {
            this.state = 'HALF_OPEN';
          } else {
            return this.getFallbackResponse();
          }
        }
        
        try {
          const result = await this.doRender(url, query);
          this.onSuccess();
          return result;
        } catch (error) {
          this.onFailure();
          return this.getFallbackResponse();
        }
      }
    }
    
  3. A/B 테스트 적용

    // 기능 플래그 기반 A/B 테스트
    const FeatureFlags = {
      NEW_PRODUCT_LAYOUT: 'new_product_layout',
      ENHANCED_SEARCH: 'enhanced_search'
    };
    
    const renderWithFeatureFlags = async (url, query, userId) => {
      const flags = await featureFlagService.getFlags(userId);
      
      if (flags[FeatureFlags.NEW_PRODUCT_LAYOUT]) {
        return renderWithNewLayout(url, query);
      }
      
      return renderWithLegacyLayout(url, query);
    };
    

보안 고려사항

  1. XSS 방지

    // 서버 렌더링 시 XSS 방지
    import { escape } from 'html-escaper';
    
    const safeRender = (content) => {
      return escape(content);
    };
    
  2. CSP (Content Security Policy) 설정

    // Express에서 CSP 헤더 설정
    app.use((req, res, next) => {
      res.setHeader('Content-Security-Policy', 
        "default-src 'self'; " +
        "script-src 'self' 'unsafe-inline'; " +
        "style-src 'self' 'unsafe-inline'; " +
        "img-src 'self' data: https:;"
      );
      next();
    });
    

Q7. Next.js 같은 프레임워크 대신 직접 구현한 SSR/SSG의 장단점은 무엇인가요?

직접 구현의 장점

  1. 학습 효과와 깊은 이해

    • SSR/SSG의 내부 동작 원리를 직접 경험
    • 성능 최적화 포인트를 정확히 파악
    • 문제 발생 시 근본 원인 추적 가능
  2. 커스터마이징 자유도

    // 특수한 요구사항에 맞는 최적화 가능
    const customRender = async (url, query) => {
      // 특정 페이지는 다른 렌더링 전략 적용
      if (url.includes('/admin')) {
        return renderWithAdminLayout(url, query);
      }
      
      // 일반 페이지는 표준 렌더링
      return renderWithStandardLayout(url, query);
    };
    
  3. 번들 크기 최적화

    • 필요한 기능만 구현하여 번들 크기 최소화
    • 프레임워크의 불필요한 기능 제거 가능

직접 구현의 단점

  1. 유지보수 비용

    • 버그 수정 및 보안 패치를 직접 관리해야 함
    • 새로운 기능 추가 시 프레임워크 대비 개발 시간 증가
  2. 생태계와 커뮤니티 지원 부족

    • 플러그인, 미들웨어 등이 제한적
    • 문제 해결 시 커뮤니티 지원 부족
  3. 안정성과 검증 부족

    • 다양한 엣지 케이스에 대한 검증 필요
    • 프로덕션 환경에서의 안정성 확보 어려움

하이브리드 접근법

// Next.js의 장점을 활용하면서 커스터마이징
// next.config.js
module.exports = {
  experimental: {
    // 커스텀 렌더링 로직 추가
    customServer: true,
  },
  
  // 필요한 기능만 선택적 사용
  webpack: (config) => {
    // 커스텀 웹팩 설정
    return config;
  }
};

Q8. Next.js를 이용하여 SSG 방식으로 배포하려면 어떻게 해야 좋을까요?

Next.js SSG 최적화 설정

  1. getStaticProps와 getStaticPaths 활용

    // pages/products/[id].js
    export async function getStaticPaths() {
      // 모든 상품 ID 가져오기
      const products = await fetchAllProducts();
      const paths = products.map(product => ({
        params: { id: product.id }
      }));
      
      return {
        paths,
        fallback: 'blocking' // 새로운 상품은 런타임에 생성
      };
    }
    
    export async function getStaticProps({ params }) {
      const product = await fetchProduct(params.id);
      
      return {
        props: { product },
        revalidate: 3600 // 1시간마다 재생성
      };
    }
    
  2. ISR (Incremental Static Regeneration) 활용

    // pages/index.js
    export async function getStaticProps() {
      const products = await fetchProducts();
      
      return {
        props: { products },
        revalidate: 60 // 60초마다 재생성
      };
    }
    
  3. 빌드 최적화

    // next.config.js
    module.exports = {
      // 이미지 최적화
      images: {
        domains: ['example.com'],
        formats: ['image/webp', 'image/avif']
      },
      
      // 번들 분석
      webpack: (config, { isServer }) => {
        if (!isServer) {
          config.resolve.fallback = {
            ...config.resolve.fallback,
            fs: false
          };
        }
        return config;
      },
      
      // 압축
      compress: true,
      
      // 실험적 기능
      experimental: {
        optimizeCss: true,
        optimizePackageImports: ['react-icons']
      }
    };
    

배포 전략

  1. Vercel 배포

    # vercel.json
    {
      "builds": [
        {
          "src": "package.json",
          "use": "@vercel/next"
        }
      ],
      "functions": {
        "pages/api/**/*.js": {
          "maxDuration": 30
        }
      }
    }
    
  2. 다른 플랫폼 배포

    # Dockerfile
    FROM node:18-alpine AS deps
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci --only=production
    
    FROM node:18-alpine AS builder
    WORKDIR /app
    COPY . .
    COPY --from=deps /app/node_modules ./node_modules
    RUN npm run build
    
    FROM node:18-alpine AS runner
    WORKDIR /app
    ENV NODE_ENV production
    COPY --from=builder /app/public ./public
    COPY --from=builder /app/.next/standalone ./
    COPY --from=builder /app/.next/static ./.next/static
    
    EXPOSE 3000
    CMD ["node", "server.js"]
    
  3. CDN 설정

    // next.config.js
    module.exports = {
      assetPrefix: process.env.NODE_ENV === 'production' 
        ? 'https://cdn.example.com' 
        : '',
      
      // 정적 파일 캐싱
      async headers() {
        return [
          {
            source: '/static/:path*',
            headers: [
              {
                key: 'Cache-Control',
                value: 'public, max-age=31536000, immutable'
              }
            ]
          }
        ];
      }
    };
    

성능 모니터링

// pages/_app.js
import { Analytics } from '@vercel/analytics/react';

export default function App({ Component, pageProps }) {
  return (
    <>
      <Component {...pageProps} />
      <Analytics />
    </>
  );
}

// 성능 측정
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';

function sendToAnalytics(metric) {
  // Vercel Analytics 또는 다른 분석 도구로 전송
  analytics.track('web-vital', metric);
}

getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);

코드 품질 향상

자랑하고 싶은 구현

없음

개선하고 싶은 부분

이번에 과제를 하면서, 에러가 어떻게 발생하는지 제대로 처리를 못해서 에러 처리를 제대로 못한거같습니다. 로깅을 추가하고, 에러 페이지 컴포넌트 및 에러 타입에 따른 응답에 대한 내용도 더 추가할 것 같습니다. 또한 환경 설정함수나 유틸 함수도 적절하게 분리를 못해서 코드가 난잡해지고 중복 현상도 많이 발생했던거 같아 다음과 같이 바꿀 것 같습니다.

  1. 구조적 분리 환경 설정 분리: config/ 폴더에 환경별 설정 파일 분리 미들웨어 분리: SSR 렌더링 로직을 별도 미들웨어 파일로 분리 유틸리티 함수 분리: URL 정규화, 에러 처리 등을 별도 모듈로 분리

  2. 에러 처리 개선 에러 타입별 처리: 404, 500 등 에러 타입에 따른 다른 응답 에러 로깅: 구조화된 로깅 시스템 추가 도입 에러 페이지: 에러 페이지 컴포넌트 추가

리팩토링 계획

ServerRouter.ts 부분을 함수형으로 리팩토링을 진행할 것 같습니다.

/**
 * 서버사이드 라우터
 */
import { BaseRouter } from "./BaseRouter.js";

export class ServerRouter extends BaseRouter {
  #currentUrl = "/";
  #origin = "http://localhost";
  #query = {};

  constructor(baseUrl = "") {
    super(baseUrl);
  }

  get query() {
    return this.#query;
  }

  set query(newQuery) {
    // 서버사이드에서는 Express에서 이미 파싱된 쿼리 객체를 직접 저장
    this.#query = newQuery || {};
  }

  getCurrentUrl() {
    return this.#currentUrl;
  }

  getOrigin() {
    return this.#origin;
  }

  /**
   * 서버 URL 설정
   * @param {string} url - 요청 URL
   * @param {string} [origin] - 서버 origin (선택적)
   */
  setUrl(url, origin = "http://localhost") {
    this.#currentUrl = url;
    this.#origin = origin;
    this.updateRoute(this.getCurrentUrl());
  }

  /**
   * 서버사이드에서는 네비게이션 불가
   */
  push() {
    throw new Error("Navigation is not supported in server-side routing");
  }

  /**
   * 라우터 시작
   */
  start() {
    this.updateRoute(this.getCurrentUrl());
  }
}

개인적으로 클래스도 좋아하지만, typescript는 함수형으로 많이 만들기에 다음과 같이 변경할 것 같습니다.

const createServerRouter = (baseUrl = "") => {
  // 상태 (클로저로 캡슐화)
  let currentUrl = "/";
  let origin = "http://localhost";
  let query = {};
  
  const baseRouter = createBaseRouter(baseUrl);
  
  return {
    // getter/setter 함수들
    getQuery: () => query,
    setQuery: (newQuery) => { query = newQuery || {}; },
    
    getCurrentUrl: () => currentUrl,
    getOrigin: () => origin,
    setUrl: (url, originParam) => { /* 구현 */ },
    push: () => { throw new Error("..."); },
    start: () => { /* 구현 */ }
  };
};

학습 연계

다음 학습 목표

Google Lighthouse와 같은 것을 사용하여, 사용자가 페이지를 실제로 보고 상호작용까지의 과정을 한번 측정해보고싶습니다. 이런 것을 사용했을 때 얼마나 사용자가 성능 체감이 되는지 한번 체크를 해보고싶습니다.

실무 적용 계획

회사에서 이전에 만든 인수인계 사이트를 SSR, SSG를 적용하여 마이그레이션 하여 만들어 보고 싶습니다. SSG와 SSR을 적절히 사용하여, 이 보다 웹 속도처리부분에서 팀원들에게 향상된 부분을 보여주고 싶음.

리뷰 받고 싶은 내용

// productUseCase.ts
export const loadProductsAndCategories = async () => {
  router.query = { current: undefined }; // 항상 첫 페이지로 초기화
  productStore.dispatch({
    type: PRODUCT_ACTIONS.SETUP,
    payload: {
      ...initialProductState,
      loading: true,
      status: "pending",
    },
  });

심화과제 react 구현중에 처음에는 다 CSR 관련 테스트코드가 통과했으나, SSR 및 SSG 구현하고 나니까 CSR (port 5175번) dev 테스트 코드만 전체 실패되었습니다.

그리하여, loadProductsAndCategories 함수에서 router.query = { current: undefined }; 로 인해 테스트 코드가 계속 렌더링 이슈가 발생했다고 생각했습니다.

이 후 router.query = { current: undefined }를 제거하였고 다음과 같이 생각했습니다.

  1. 라우터의 초기 상태가 빈 객체 {}가 됨.
  2. API 호출 시 current 파라미터가 전달되지 않아 서버와 동일한 기본값이 사용 됨
  3. SSR과 CSR의 결과가 일치하게 되어 하이드레이션 이슈가 해결됨.

이는 초기 상태의 일관성을 똑같이 만들어서 서버와 클라이언트 간의 렌더링 결과를 동일하게 만든다고 생각하였는데 이게 맞을까요??

과제 피드백

안녕하세요 민기님! 9주차 과제 잘 진행해주셨네요 ㅎㅎ 고생하셨습니다!!

이 부분의 경우 CSR 테스트이기 때문에 SSR과는 전혀 관련이 없어요..! 에초에 실행하는것도 vite 개발서버라서, ssr과 엮일일이 없답니다 ㅠㅠ

여튼 그래서 원인에 대해 이야기를 해보자면 초기 코드의 router의 경우 { current: undefined } 로 할당해도 query 값 자체는 빈 객체가 됩니다. 그런데 라우터를 개선하는 과정에서 query를 계산하는 방식이 달라져서 undefined가 잔존하게 된가 아닌가 싶네요!

한 번 솔루션 코드 참고해보시면 좋을 것 같아요!