성능 최적화 방안 설계 - 100-hours-a-week/5-yeosa-wiki GitHub Wiki

1. 성능 모니터링 도구 및 기준(지표)

1.1 로딩 성능 지표

LCP (Largest Contentful Paint) 주요 컨텐츠가 로드될 때까지 걸리는 시간.

  • 페이지가 처음으로 로드를 시작한 시점을 기준으로 뷰포트 내에 있는 가장 큰 이미지 또는 텍스트 블록의 렌더링 시간을 측정한다.
  • Good : ~ 2.5s / Needs Improvement : 2.5s ~ 4.0s / Poor : 4s ~
  • Chrome Dev Tools 를 통해 간단하게 측정할 수 있다. Page Speed Insight

FCP (First Contentful Paint) 첫 요소가 로드될 때까지 걸리는 시간

  • 사용자가 페이지로 처음 이동한 시점부터 페이지 콘텐츠의 일부가 화면에 렌더링되는 시점까지의 시간
  • Good : ~ 1.8s / Needs Improvement : 1.8s ~ 3.0s / Poor : 3초 ~
  • Chrome Dev Tools 를 통해 간단하게 측정할 수 있다. Page Speed Insight

FMP (First Meaningful Paint) 페이지의 기본 콘텐츠가 사용자에게 표시되는 경우를 측정

  • 사용자가 페이지 로드를 시작한 시점과 페이지가 주요 페이지 상단 콘텐츠를 렌더링하는 시점 사이의 시간
  • Lighthouse 에서 성능 섹션에 속한다.

응답성 측정 항목

FID (First Input Delay) 첫 입력 지연

  • 사용자가 페이지와 처음 상호작용한 시점부터 브라우저가 이 상호작용에 대한 응답으로 이벤트 핸들러 처리를 실제로 시작할 수 있는 시점까지의 시간
  • Good: ~ 100ms / Need Improvement: 100ms ~ 300ms / Poor: 300ms ~ 현장 도구 : PageSpeed Insights
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    const delay = entry.processingStart - entry.startTime;
    console.log('FID candidate:', delay, entry);
  }
}).observe({type: 'first-input', buffered: true});

INP (Interaction to Next Paint) 다음 페인트에 대한 상호작용

  • 사용자의 페이지 방문 전체 생애 주기 동안 발생하는 모든 클릭, 탭, 키보드 상호작용의 지연 시간을 관찰하여 사용자 상호작용에 대한 페이지의 전반적인 반응성을 평가하는 측정항목
  • Good : 200ms / Need Improvement : 200ms ~ 500ms / Poor : 500ms
  • 현장 도구 : Page Speed Insight , 실험실 도구 : Chrome Dev Tools
    • 현장 성능 측정에서 문제 되는 부분을 발견 후, 실험실 성능 측정과 함께 성능 개선

TTI (Time to Interactive) 페이지가 완전히 상호작용 가능해질 때까지의 시간

  • 페이지 로드가 시작된 시점부터 기본 하위 리소스가 로드되어 사용자 입력에 빠르고 안정적으로 반응할 수 있는 시점까지의 시간을 측정
  • Lighthouse 내에서 측정

TBT (Total Blocking Time) 메인 스레드 블로킹 시간

  • 콘텐츠가 포함된 첫 페인트 (FCP) 후 입력 응답성을 방지하기에 충분한 시간 동안 기본 스레드가 차단되었던 총 시간
  • Lighthouse 내에서 측정

기타 측정 항목

CLS (Cumulative Layout Shift) 누적 레이아웃 이동

  • 페이지의 전체 수명 주기 동안 발생하는 모든 예기치 않은 레이아웃 변경에 대한 레이아웃 변경 점수 중 가장 큰 버스트를 측정합니다.
  • Good: ~ 0.1 /Need Improvement: 0.1 ~ 0.25 / Poor: 0.25 ~
  • 실험실 도구 : Chrome Dev Tools , 현장 도구: Page Speed Insight

TTFB (Time to First Byte) 서버 응답 시간 측정

  • 리소스 요청과 응답의 첫 번째 바이트가 도착하기 시작하는 시점 사이의 시간
  • Good: ~ 800ms /Need Improvement: 800ms ~ 1800ms / Poor: 1800ms
  • Chrome Dev Tools - Network

1.2 렌더링 성능 지표

React Developer Tools

FPS (Frames Per Second)

  • 60 FPS: 최적 성능 (16.67ms per frame)
  • 30-59 FPS: 허용 가능 (16.67-33.33ms per frame)
  • < 30 FPS: 성능 개선 필요 (> 33.33ms per frame)

렌더링 횟수

컴포넌트별 렌더링 추적

// 렌더링 추적 HOC
const withRenderTracker = <P extends object>(
  WrappedComponent: React.ComponentType<P>,
  componentName: string
) => {
  return (props: P) => {
    const renderCount = useRef(0);
    const lastProps = useRef<P>();
    const lastRenderTime = useRef(0);
    
    // 렌더링 원인 분석
    useEffect(() => {
      renderCount.current++;
      const now = performance.now();
      const timeSinceLastRender = now - lastRenderTime.current;
      
      // Props 변화 감지
      const changedProps = lastProps.current 
        ? Object.keys(props).filter(key => props[key] !== lastProps.current![key])
        : Object.keys(props);
      
      console.log(`🔄 ${componentName} 렌더링 #${renderCount.current}`, {
        timeSinceLastRender: `${timeSinceLastRender.toFixed(2)}ms`,
        changedProps: changedProps.length > 0 ? changedProps : 'initial render',
        props: props
      });
      
      lastProps.current = props;
      lastRenderTime.current = now;
    });
    
    return <WrappedComponent {...props} />;
  };
};

// 사용 예시
const TrackedComponent = withRenderTracker(MyComponent, 'MyComponent');

불필요한 렌더링 감지

// 렌더링 성능 분석 커스텀 훅
const useWastedRenderDetector = (props: any, componentName: string) => {
  const prevProps = useRef();
  const renderReasons = useRef<string[]>([]);
  
  useEffect(() => {
    if (prevProps.current) {
      const reasons = [];
      
      // Props 비교
      for (const key in props) {
        if (props[key] !== prevProps.current[key]) {
          // 객체/배열 깊은 비교
          if (typeof props[key] === 'object' && props[key] !== null) {
            if (JSON.stringify(props[key]) === JSON.stringify(prevProps.current[key])) {
              reasons.push(`⚠️ ${key}: 참조만 변경됨 (실제 값은 동일)`);
            } else {
              reasons.push(`✓ ${key}: 실제 값 변경됨`);
            }
          } else {
            reasons.push(`✓ ${key}: ${prevProps.current[key]}${props[key]}`);
          }
        }
      }
      
      renderReasons.current = reasons;
      
      if (reasons.every(reason => reason.startsWith('⚠️'))) {
        console.warn(`🚨 ${componentName}: 불필요한 렌더링 감지`, reasons);
      }
    }
    
    prevProps.current = props;
  });
  
  return renderReasons.current;
};

메인 스레드 사용량

class MainThreadMonitor {
  private isMonitoring = false;
  private blockingTasks: Array<{duration: number, timestamp: number}> = [];
  
  startMonitoring() {
    if (this.isMonitoring) return;
    this.isMonitoring = true;
    
    // Long Task API 활용
    if ('PerformanceObserver' in window) {
      const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
          if (entry.duration > 50) { // 50ms 이상 블로킹
            this.blockingTasks.push({
              duration: entry.duration,
              timestamp: entry.startTime
            });
            
            console.warn(`🐌 메인 스레드 블로킹: ${entry.duration.toFixed(2)}ms`);
          }
        }
      });
      
      observer.observe({ entryTypes: ['longtask'] });
    }
    
    // 수동 측정 (fallback)
    this.measureMainThreadUsage();
  }
  
  private measureMainThreadUsage() {
    const startTime = performance.now();
    
    // 의도적인 지연으로 메인 스레드 사용률 측정
    setTimeout(() => {
      const actualDelay = performance.now() - startTime;
      const expectedDelay = 10; // 10ms 예상
      const blockingTime = actualDelay - expectedDelay;
      
      if (blockingTime > 10) {
        console.log(`메인 스레드 사용률: ${((blockingTime / actualDelay) * 100).toFixed(1)}%`);
      }
      
      if (this.isMonitoring) {
        this.measureMainThreadUsage();
      }
    }, expectedDelay);
  }
  
  getReport() {
    const totalBlockingTime = this.blockingTasks.reduce((sum, task) => sum + task.duration, 0);
    const recentTasks = this.blockingTasks.filter(task => 
      task.timestamp > performance.now() - 60000 // 최근 1분
    );
    
    return {
      totalBlockingTime,
      recentBlockingCount: recentTasks.length,
      averageBlockingTime: totalBlockingTime / this.blockingTasks.length || 0,
      worstBlockingTime: Math.max(...this.blockingTasks.map(t => t.duration), 0)
    };
  }
}

1.3 모니터링

1.3.1 Chrome DevTools

Performance 탭, Memory 탭 활용

1.3.2 React Profiler

프로파일러 API 활용

import { Profiler, ProfilerOnRenderCallback } from 'react';

// 렌더링 성능 측정
const onRenderCallback: ProfilerOnRenderCallback = (
  id,           // 프로파일러 ID
  phase,        // "mount" 또는 "update"
  actualDuration,    // 실제 렌더링 시간
  baseDuration,      // 최적화 없이 예상되는 시간
  startTime,         // 렌더링 시작 시간
  commitTime,        // 커밋 시간
  interactions       // 상호작용 추적
) => {
  // 성능 데이터 수집
  const performanceData = {
    componentId: id,
    phase,
    renderTime: actualDuration,
    potentialTime: baseDuration,
    efficiency: ((baseDuration - actualDuration) / baseDuration) * 100,
    timestamp: commitTime
  };
  
  // 성능 임계값 체크
  if (actualDuration > 16) { // 16ms 초과 시
    console.warn(`🐌 느린 렌더링: ${id} (${actualDuration.toFixed(2)}ms)`);
  }
  
  // 성능 데이터 저장/전송
  sendPerformanceData(performanceData);
};

// 사용 예시
const App = () => (
  <Profiler id="App" onRender={onRenderCallback}>
    <Profiler id="Header" onRender={onRenderCallback}>
      <Header />
    </Profiler>
    <Profiler id="MainContent" onRender={onRenderCallback}>
      <MainContent />
    </Profiler>
  </Profiler>
);

1.3.3 Lighthouse


2. 개선 방안

2.1 초기 로드 속도 개선

목적 : 사용자 첫 경험 개선을 통한 이탈률 감소 목표 : LCP < 2.5초, FCP < 1.8초

2.1.1 코드 스플리팅 (React.lazy, dynamic import)

React.lazy를 활용한 컴포넌트 레벨 스플리팅 import { lazy, Suspense } from 'react'

  • 조건부 로딩 + 동적 임포트 적용
  • 접근 권한 별, 페이지 별, 기능별 청크 분리

Next.js 마이그레이션 이후 Webpack 설정을 통한 청크 최적화

// next.config.js
module.exports = {
  webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
    config.optimization.splitChunks = {
      chunks: 'all',
      minSize: 20000,
      maxSize: 244000,
      cacheGroups: {
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true,
        },
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: -10,
          chunks: 'all',
        },
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          chunks: 'all',
          priority: 20,
        },
        commons: {
          name: 'commons',
          minChunks: 2,
          chunks: 'all',
          priority: 10,
          enforce: true,
        },
      },
    };
    return config;
  },
  // 실험적 기능 활용
  experimental: {
    optimizeCss: true,
    optimizePackageImports: ['lodash', 'date-fns'],
  },
};

2.1.2 번들 사이즈 최적화

Tree Shaking 최적화

ES6 모듈 형태로 import

  • 개별 모듈 임포트
  • 필요한 함수만 임포트
  • 조건부 임포트
// package.json에 sideEffects 설정 
{ 
  "name": "my-app", 
  "sideEffects": ["*.css", "*.scss", "./src/polyfills.js"] 
}

Critical CSS 인라인화

Tailwind CSS 최적화 설정

// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  // 사용하지 않는 CSS 자동 제거
  purge: {
    enabled: process.env.NODE_ENV === 'production',
    content: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
  },
  theme: {
    extend: {},
  },
  plugins: [],
}

컴포넌트 별 CSS 분리

// _app.tsx
import dynamic from 'next/dynamic';

// Critical 스타일만 즉시 로드
import '../styles/critical.css'; // 폴드 위 필수 스타일만

// Non-critical 스타일은 지연 로드
const NonCriticalStyles = dynamic(() => import('../styles/non-critical.css'), {
  ssr: false
});

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

폴드 위(Above the fold) 컴포넌트 식별

// 초기 화면에 보이는 핵심 컴포넌트들
const CriticalComponents = {
  Header: ['bg-white', 'shadow-lg', 'fixed', 'top-0', 'w-full'],
  Hero: ['h-screen', 'flex', 'items-center', 'justify-center', 'bg-blue-500'],
  Navigation: ['flex', 'space-x-4', 'text-white'],
  LoadingSpinner: ['animate-spin', 'w-8', 'h-8', 'border-4', 'border-gray-300']
};

Tailwind JIT 모드 활용

// tailwind.config.js - JIT 모드로 필요한 CSS만 생성
module.exports = {
  mode: 'jit', // Just-In-Time 모드
  purge: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  // 동적 클래스명 보호
  safelist: [
    'bg-red-500',
    'text-3xl',
    'lg:text-4xl',
    // 동적으로 생성되는 클래스들
  ],
}

2.1.3 서버사이드 렌더링/정적 생성 최적화 (Next.js)

// TODO

2.1.4 주의사항 및 트레이드 오프

과도한 스플리팅 주의 문제: 너무 많은 청크로 인한 HTTP 요청 증가 해결: 적절한 청크 크기 유지 (50KB-250KB) 모니터링: 네트워크 요청 수와 로딩 시간 균형 측정

SSR/SSG 비용 서버 부하: SSR 시 서버 리소스 사용량 증가 캐싱 전략: CDN과 캐싱 레이어 구축 필요 하이브리드 접근: 중요 페이지만 SSR, 나머지는 CSR

Tree Shaking 한계 동적 임포트: 런타임에 결정되는 임포트는 tree shaking 불가 사이드 이펙트: 의도치 않은 코드 제거 방지 필요 라이브러리 호환성: 모든 라이브러리가 tree shaking 지원하지 않음

2.1.5 성능 측정 및 결과

측정 도구 Lighthouse: 종합적인 성능 지표 측정 WebPageTest: 실제 네트워크 환경에서의 성능 테스트 Chrome DevTools: 네트워크, 성능 프로파일링 Bundle Analyzer: 번들 크기 및 구성 분석

최적화 전 (Before) 번들 크기: FCP: LCP: TTI: 초기 요청 수:

최적화 후 (After) 번들 크기 FCP LCP TTI 초기 요청 수


2.2 렌더링 성능 개선 방안

목적 : 상호작용 반응성 향상 목표 : INP < 100ms, 60fps 유지

2.2.1 메모이제이션 최적화

React.memo 적용 대상

  • Props가 자주 변경되지 않는 컴포넌트
  • 렌더링 비용이 높은 컴포넌트
  • 부모 컴포넌트 재렌더링 시 불필요하게 재렌더링되는 컴포넌트

useMemo 적용 기준

  • 계산 비용이 높은 연산 (100ms 이상)
  • 의존성 배열이 자주 변경되지 않는 경우
  • 복잡한 배열 변환, 필터링, 정렬 작업

useCallback 적용 대상

  • 자식 컴포넌트 props로 전달되는 함수
  • 이벤트 핸들러 함수
  • useEffect 의존성 배열에 포함되는 함수

2.2.2 Virtual scrolling

  • react-window / react-virtualized-auto-sizer 사용
  • 동적 높이 지원 (VariableSizeList) 적용 대상
  • 보관함 페이지에 적용 (100개 이상 아이템일 때 적용)

2.2.3 Context API 최적화

자주 변경되는 데이터와 정적 데이터 분리

  • Context Provider 최적화 적용
  • Context 사용 최적화
  • 컴포넌트 트리 내 데이터 전달 최적화

2.2.4. 주의사항 및 트레이드오프

메모이제이션 과용 주의 문제: 모든 컴포넌트에 React.memo 적용 해결: 성능 측정 후 필요한 곳에만 적용 기준: 렌더링 시간 > 50ms 또는 자주 재렌더링되는 컴포넌트

의존성 배열 관리 문제: 의존성 배열이 자주 변경되어 메모이제이션 효과 상실 해결: 의존성 최소화, 안정적인 참조 사용

메모리 사용량 증가 트레이드오프: 메모이제이션으로 인한 메모리 사용량 증가 대응: 메모리 프로파일링을 통한 모니터링

2.2.5 성능 측정 및 결과

Before/After 비교 지표 INP (Interaction to Next Paint) FPS 메모리 사용량

측정 도구 React DevTools Profiler: 컴포넌트 렌더링 시간 Chrome DevTools: 메모리 사용량, 성능 프로파일 Web Vitals: 실제 사용자 경험 지표 Bundle Analyzer: 번들 크기 분석

지속적 모니터링

// 성능 측정 훅
const usePerformanceMonitor = (componentName) => {
  useEffect(() => {
    const startTime = performance.now();
    
    return () => {
      const endTime = performance.now();
      console.log(`${componentName} 렌더링 시간: ${endTime - startTime}ms`);
    };
  });
};

// 사용 예시
const ExpensiveComponent = () => {
  usePerformanceMonitor('ExpensiveComponent');
  // 컴포넌트 로직
};

2.3 이미지 및 리소스 최적화

기본적인 UI 요소나 로고 같은 것들은 정적 이미지로 처리하여 최대한 빠른 로딩을 보장하고, 사용자 상호작용이 필요한 부분만 동적으로 처리합니다. 사용자가 해당 이미지와 얼마나 자주 상호작용하는지, 그리고 그 상호작용의 복잡성이 어느 정도인지를 기준으로 판단하는 것이 좋습니다.

2.3.1 정적 이미지 최적화

정적 이미지 분류 기준

  • UI 요소: 로고, 아이콘, 버튼 배경, 브랜딩 요소
  • 변경 빈도: 사이트 전반에서 변경되지 않는 요소
  • 사용자 상호작용: 클릭/호버 외 복잡한 상호작용이 없는 요소
  • 캐시 지속성: 장기간 캐시 가능한 요소

정적 이미지 최적화

  • 빌드 타임 최적화 적용
  • 장기 캐시 설정 (1년)
  • 다양한 해상도 사전 생성
  • Critical 이미지 preload 적용

동적 이미지 분류 기준

  • 사용자 생성 콘텐츠: 프로필 사진, 업로드 이미지, 사용자 갤러리
  • 상호작용 빈도: 확대/축소, 필터링, 편집 등 복잡한 상호작용
  • 실시간 변경: 썸네일 생성, 크롭, 리사이징이 필요한 이미지
  • 개인화: 사용자별로 다르게 표시되는 이미지

동적 이미지:

  • 런타임 최적화 적용
  • 단기 캐시 설정
  • 요청 시점 리사이징
  • Lazy loading 기본 적용

2.3.2 이미지 사이즈 개선

CDN 기반 사이즈 최적화 전략

  • 반응형 이미지 제공 전략

    • Device Pixel Ratio 대응: 1x, 2x, 3x 해상도별 이미지 제공
    • 뷰포트별 최적화: Mobile(320-768px), Tablet(768-1024px), Desktop(1024px+)
    • 컨텍스트별 사이즈: 썸네일, 미디엄, 풀사이즈, 히어로 이미지
  • CDN 활용 사이즈 최적화

    • URL 파라미터 기반 리사이징: 실시간 사이즈 조정
    • 자동 크롭 및 포커스: 중요 영역 기반 자동 크롭
    • 스마트 압축: 품질과 용량의 최적 균형점 자동 탐지
    • 대역폭 감지: 네트워크 상태에 따른 적응형 품질 조정

2.3.3 이미지 포맷 개선

WebP/AVIF 포맷 적용

포맷별 우선순위 및 적용 기준

1순위: AVIF (지원 브라우저 대상)
- 용량: 기존 대비 50% 감소
- 품질: JPEG 대비 동등 또는 우수
- 적용 대상: 모던 브라우저 (Chrome 85+, Firefox 93+)

2순위: WebP (폴백)
- 용량: 기존 대비 25-35% 감소  
- 호환성: 대부분의 모던 브라우저 지원
- 적용 대상: IE 제외 모든 브라우저

3순위: 기존 포맷 (최종 폴백)
- JPEG: 사진류, 복잡한 이미지
- PNG: 투명도 필요한 이미지, 단순한 그래픽

2.3.4 Image 컴포넌트 (Next.js)

Layout Shift 방지

사전 공간 확보

  • 고정 크기 지정: width, height 속성으로 레이아웃 공간 사전 확보

  • Aspect Ratio 활용: CSS aspect-ratio 또는 padding-bottom 기법

  • Placeholder 활용: 로딩 중 시각적 대체 요소 제공

  • Skeleton UI: 실제 콘텐츠 구조와 유사한 로딩 상태

  • 목표 CLS 점수: < 0.1

  • 측정 방법: Lighthouse, Web Vitals 라이브러리

Lazy Loading

즉시 로딩 (eager):
- 폴드 위(Above the fold) 이미지
- Critical path의 핵심 이미지
- 히어로 섹션, 로고, 주요 CTA 이미지

지연 로딩 (lazy):
- 폴드 아래 이미지
- 갤러리, 목록의 이미지
- 사용자 스크롤 시 노출되는 이미지

Image Preloading

  • Critical 이미지: 페이지 로드와 함께 우선 로드
  • 사용자 의도 예측: 마우스 호버, 링크 접근 시 사전 로드
  • Route 기반 Preloading: 다음 페이지의 주요 이미지 사전 로드
  • 우선순위 기반: 중요도에 따른 로딩 순서 조정

2.3.5 성능 측정 및 최적화 지표

로딩 성능 지표

  • 이미지 로드 시간: 개별 이미지별 로딩 완료 시간
  • First Image Paint: 첫 번째 이미지 렌더링 시간
  • Largest Contentful Paint: 가장 큰 이미지 요소의 로딩 시간
  • 전체 이미지 로드 완료: 페이지 내 모든 이미지 로딩 완료 시간

사용자 경험 지표

  • Cumulative Layout Shift: 이미지 로딩으로 인한 레이아웃 변화
  • 사용자 상호작용 지연: 이미지 관련 인터랙션 응답 시간
  • 이미지 로딩 실패율: 네트워크 오류, 포맷 미지원 등

리소스 효율성 지표

  • 대역폭 사용량: 페이지당 이미지 데이터 전송량
  • 캐시 히트율: CDN 및 브라우저 캐시 활용률
  • 포맷 채택률: WebP/AVIF 포맷 제공 및 사용 비율

2.4 데이터 fetching 최적화

불필요한 데이터 로딩을 줄이고, 필요한 데이터만 효율적으로 가져오도록 캐싱, 프리페칭, 데이터 로딩 전략 선택, 데이터 분할 등의 기법 등을 활용하여 최적화한다.

2.4.1 데이터 prefetching

마우스 호버 (High Priority):

  • 링크 호버 시 목적지 페이지 데이터 미리 로드
  • 트리거: mouseenter 이벤트
  • 지연 시간: 100-200ms (의도 확실성 확보)

뷰포트 진입 (Medium Priority):

  • 스크롤로 특정 섹션 진입 시 관련 데이터 로드
  • 트리거: Intersection Observer
  • 임계값: 뷰포트 50% 진입 시점

Link Component Prefetching

  • 자동 프리페칭: 뷰포트 내 Link 컴포넌트 자동 인식
  • 조건부 프리페칭: 네트워크 상태, 데이터 세이버 모드 고려
  • 우선순위 조정: 중요한 링크 우선 프리페칭

Router Prefetching

  • 경로 기반: 사용자 네비게이션 패턴 분석 후 예측 프리페칭
  • 조건부 실행: 모바일 환경 및 저속 네트워크 시 제한
  • 메모리 관리: 사용하지 않는 프리페치 데이터 자동 정리

2.4.2 TanStack Query 최적화 전략

쿼리 키 설계

계층적 키 구조:
['users', userId, 'posts', { page, limit, filter }]

장점:
- 관련 데이터 일괄 무효화 가능
- 부분 매칭으로 선택적 업데이트
- 쿼리 관계 추적 용이

키 설계 원칙:
1. 일반적인 것부터 구체적인 순서
2. 필터/페이지네이션은 객체로 분리
3. 사용자별 데이터는 userId 포함

2.4.3 데이터 분할

점진적 데이터 로딩

1단계: 스켈레톤 UI 표시
2단계: 핵심 메타데이터 로드 (썸네일)
3단계: 상세 데이터 로드 (추가 정보)

2.3.4 성능 측정 및 최적화 지표

데이터 로딩 성능

  • TTFB (Time To First Byte): 서버 응답 시간
  • 데이터 페칭 완료 시간: API 요청부터 UI 렌더링까지
  • 캐시 히트율: 전체 요청 대비 캐시 활용 비율
  • 프리페치 효율성: 실제 사용된 프리페치 데이터 비율

사용자 경험 지표

  • 데이터 가용성: 사용자 요청 시점 데이터 준비 상태
  • 로딩 상태 지속 시간: 스켈레톤/로딩 상태 노출 시간
  • 데이터 신선도: 마지막 업데이트로부터 경과 시간
  • 오프라인 가용성: 네트워크 불안정 시 서비스 지속성

2.5 브라우져 캐싱 전략

HTTP 캐싱 헤더 설정에서는 정적 자산에 대해 긴 만료 시간을 설정하되, 파일명에 해시를 포함하여 내용이 변경될 때마다 새로운 파일명을 생성하도록 해야 합니다. 이를 통해 캐싱과 업데이트를 모두 효과적으로 관리할 수 있습니다.

2.5.1 캐시 정책 설계 원칙

리소스별 캐시 전략 분류

영구 캐시 대상 (Immutable Resources)

  • 정적 에셋: JS, CSS, 이미지, 폰트 파일 (해시 포함)
  • 캐시 정책: Cache-Control: public, max-age=31536000, immutable
  • 업데이트 방식: 파일명 해시 변경을 통한 버스트
  • 장점: 완벽한 캐시 활용, 네트워크 요청 제거

중기 캐시 대상 (Semi-Static Resources)

  • API 응답: 마스터 데이터, 설정 정보, 메타데이터
  • 캐시 정책: Cache-Control: public, max-age=3600, must-revalidate
  • 업데이트 방식: ETag 기반 조건부 요청
  • 검증 주기: 1시간마다 서버 검증

단기 캐시 대상 (Dynamic Resources)

  • HTML 문서: 메인 페이지, 동적 콘텐츠
  • 캐시 정책: Cache-Control: public, max-age=300, s-maxage=3600
  • 업데이트 방식: 즉시 무효화 또는 짧은 TTL
  • CDN 활용: 엣지에서 더 긴 캐시, 브라우저에서 짧은 캐시

2.5.2 해시 기반 캐시 버스팅

파일명 해시 생성 및 관리

정적 에셋 해시 전략:
- Webpack: [contenthash] 사용
- Next.js: 자동 해시 생성 및 관리
- 예시: main.a1b2c3d4.js, styles.e5f6g7h8.css

해시 길이 최적화:
- 8자리: 일반적인 용도, 충돌 확률 낮음
- 12자리: 대규모 프로젝트, 더 안전한 고유성
- 16자리: 엔터프라이즈급, 최대 안전성

2.5.3 Service Worker 캐싱

캐시 우선순위 및 전략

Stale While Revalidate

적용 대상: 주기적 업데이트 콘텐츠
동작 방식:
1. 캐시된 데이터 즉시 반환
2. 백그라운드에서 네트워크 요청
3. 응답 받으면 캐시 업데이트

장점: 빠른 응답 + 데이터 신선도
적용 사례: 뉴스 피드, 상품 목록

⚠️ **GitHub.com Fallback** ⚠️