성능 최적화 방안 설계 - 100-hours-a-week/5-yeosa-wiki GitHub Wiki
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
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)
};
}
}
Performance 탭, Memory 탭 활용
프로파일러 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>
);
목적 : 사용자 첫 경험 개선을 통한 이탈률 감소 목표 : LCP < 2.5초, FCP < 1.8초
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'],
},
};
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',
// 동적으로 생성되는 클래스들
],
}
// TODO
과도한 스플리팅 주의 문제: 너무 많은 청크로 인한 HTTP 요청 증가 해결: 적절한 청크 크기 유지 (50KB-250KB) 모니터링: 네트워크 요청 수와 로딩 시간 균형 측정
SSR/SSG 비용 서버 부하: SSR 시 서버 리소스 사용량 증가 캐싱 전략: CDN과 캐싱 레이어 구축 필요 하이브리드 접근: 중요 페이지만 SSR, 나머지는 CSR
Tree Shaking 한계 동적 임포트: 런타임에 결정되는 임포트는 tree shaking 불가 사이드 이펙트: 의도치 않은 코드 제거 방지 필요 라이브러리 호환성: 모든 라이브러리가 tree shaking 지원하지 않음
측정 도구 Lighthouse: 종합적인 성능 지표 측정 WebPageTest: 실제 네트워크 환경에서의 성능 테스트 Chrome DevTools: 네트워크, 성능 프로파일링 Bundle Analyzer: 번들 크기 및 구성 분석
최적화 전 (Before) 번들 크기: FCP: LCP: TTI: 초기 요청 수:
최적화 후 (After) 번들 크기 FCP LCP TTI 초기 요청 수
목적 : 상호작용 반응성 향상 목표 : INP < 100ms, 60fps 유지
React.memo 적용 대상
- Props가 자주 변경되지 않는 컴포넌트
- 렌더링 비용이 높은 컴포넌트
- 부모 컴포넌트 재렌더링 시 불필요하게 재렌더링되는 컴포넌트
useMemo 적용 기준
- 계산 비용이 높은 연산 (100ms 이상)
- 의존성 배열이 자주 변경되지 않는 경우
- 복잡한 배열 변환, 필터링, 정렬 작업
useCallback 적용 대상
- 자식 컴포넌트 props로 전달되는 함수
- 이벤트 핸들러 함수
- useEffect 의존성 배열에 포함되는 함수
- react-window / react-virtualized-auto-sizer 사용
- 동적 높이 지원 (VariableSizeList) 적용 대상
- 보관함 페이지에 적용 (100개 이상 아이템일 때 적용)
자주 변경되는 데이터와 정적 데이터 분리
- Context Provider 최적화 적용
- Context 사용 최적화
- 컴포넌트 트리 내 데이터 전달 최적화
메모이제이션 과용 주의 문제: 모든 컴포넌트에 React.memo 적용 해결: 성능 측정 후 필요한 곳에만 적용 기준: 렌더링 시간 > 50ms 또는 자주 재렌더링되는 컴포넌트
의존성 배열 관리 문제: 의존성 배열이 자주 변경되어 메모이제이션 효과 상실 해결: 의존성 최소화, 안정적인 참조 사용
메모리 사용량 증가 트레이드오프: 메모이제이션으로 인한 메모리 사용량 증가 대응: 메모리 프로파일링을 통한 모니터링
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');
// 컴포넌트 로직
};
기본적인 UI 요소나 로고 같은 것들은 정적 이미지로 처리하여 최대한 빠른 로딩을 보장하고, 사용자 상호작용이 필요한 부분만 동적으로 처리합니다. 사용자가 해당 이미지와 얼마나 자주 상호작용하는지, 그리고 그 상호작용의 복잡성이 어느 정도인지를 기준으로 판단하는 것이 좋습니다.
정적 이미지 분류 기준
- UI 요소: 로고, 아이콘, 버튼 배경, 브랜딩 요소
- 변경 빈도: 사이트 전반에서 변경되지 않는 요소
- 사용자 상호작용: 클릭/호버 외 복잡한 상호작용이 없는 요소
- 캐시 지속성: 장기간 캐시 가능한 요소
정적 이미지 최적화
- 빌드 타임 최적화 적용
- 장기 캐시 설정 (1년)
- 다양한 해상도 사전 생성
- Critical 이미지 preload 적용
동적 이미지 분류 기준
- 사용자 생성 콘텐츠: 프로필 사진, 업로드 이미지, 사용자 갤러리
- 상호작용 빈도: 확대/축소, 필터링, 편집 등 복잡한 상호작용
- 실시간 변경: 썸네일 생성, 크롭, 리사이징이 필요한 이미지
- 개인화: 사용자별로 다르게 표시되는 이미지
동적 이미지:
- 런타임 최적화 적용
- 단기 캐시 설정
- 요청 시점 리사이징
- Lazy loading 기본 적용
CDN 기반 사이즈 최적화 전략
-
반응형 이미지 제공 전략
- Device Pixel Ratio 대응: 1x, 2x, 3x 해상도별 이미지 제공
- 뷰포트별 최적화: Mobile(320-768px), Tablet(768-1024px), Desktop(1024px+)
- 컨텍스트별 사이즈: 썸네일, 미디엄, 풀사이즈, 히어로 이미지
-
CDN 활용 사이즈 최적화
- URL 파라미터 기반 리사이징: 실시간 사이즈 조정
- 자동 크롭 및 포커스: 중요 영역 기반 자동 크롭
- 스마트 압축: 품질과 용량의 최적 균형점 자동 탐지
- 대역폭 감지: 네트워크 상태에 따른 적응형 품질 조정
WebP/AVIF 포맷 적용
포맷별 우선순위 및 적용 기준
1순위: AVIF (지원 브라우저 대상)
- 용량: 기존 대비 50% 감소
- 품질: JPEG 대비 동등 또는 우수
- 적용 대상: 모던 브라우저 (Chrome 85+, Firefox 93+)
2순위: WebP (폴백)
- 용량: 기존 대비 25-35% 감소
- 호환성: 대부분의 모던 브라우저 지원
- 적용 대상: IE 제외 모든 브라우저
3순위: 기존 포맷 (최종 폴백)
- JPEG: 사진류, 복잡한 이미지
- PNG: 투명도 필요한 이미지, 단순한 그래픽
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: 다음 페이지의 주요 이미지 사전 로드
- 우선순위 기반: 중요도에 따른 로딩 순서 조정
로딩 성능 지표
- 이미지 로드 시간: 개별 이미지별 로딩 완료 시간
- First Image Paint: 첫 번째 이미지 렌더링 시간
- Largest Contentful Paint: 가장 큰 이미지 요소의 로딩 시간
- 전체 이미지 로드 완료: 페이지 내 모든 이미지 로딩 완료 시간
사용자 경험 지표
- Cumulative Layout Shift: 이미지 로딩으로 인한 레이아웃 변화
- 사용자 상호작용 지연: 이미지 관련 인터랙션 응답 시간
- 이미지 로딩 실패율: 네트워크 오류, 포맷 미지원 등
리소스 효율성 지표
- 대역폭 사용량: 페이지당 이미지 데이터 전송량
- 캐시 히트율: CDN 및 브라우저 캐시 활용률
- 포맷 채택률: WebP/AVIF 포맷 제공 및 사용 비율
불필요한 데이터 로딩을 줄이고, 필요한 데이터만 효율적으로 가져오도록 캐싱, 프리페칭, 데이터 로딩 전략 선택, 데이터 분할 등의 기법 등을 활용하여 최적화한다.
마우스 호버 (High Priority):
- 링크 호버 시 목적지 페이지 데이터 미리 로드
- 트리거: mouseenter 이벤트
- 지연 시간: 100-200ms (의도 확실성 확보)
뷰포트 진입 (Medium Priority):
- 스크롤로 특정 섹션 진입 시 관련 데이터 로드
- 트리거: Intersection Observer
- 임계값: 뷰포트 50% 진입 시점
Link Component Prefetching
- 자동 프리페칭: 뷰포트 내 Link 컴포넌트 자동 인식
- 조건부 프리페칭: 네트워크 상태, 데이터 세이버 모드 고려
- 우선순위 조정: 중요한 링크 우선 프리페칭
Router Prefetching
- 경로 기반: 사용자 네비게이션 패턴 분석 후 예측 프리페칭
- 조건부 실행: 모바일 환경 및 저속 네트워크 시 제한
- 메모리 관리: 사용하지 않는 프리페치 데이터 자동 정리
쿼리 키 설계
계층적 키 구조:
['users', userId, 'posts', { page, limit, filter }]
장점:
- 관련 데이터 일괄 무효화 가능
- 부분 매칭으로 선택적 업데이트
- 쿼리 관계 추적 용이
키 설계 원칙:
1. 일반적인 것부터 구체적인 순서
2. 필터/페이지네이션은 객체로 분리
3. 사용자별 데이터는 userId 포함
점진적 데이터 로딩
1단계: 스켈레톤 UI 표시
2단계: 핵심 메타데이터 로드 (썸네일)
3단계: 상세 데이터 로드 (추가 정보)
데이터 로딩 성능
- TTFB (Time To First Byte): 서버 응답 시간
- 데이터 페칭 완료 시간: API 요청부터 UI 렌더링까지
- 캐시 히트율: 전체 요청 대비 캐시 활용 비율
- 프리페치 효율성: 실제 사용된 프리페치 데이터 비율
사용자 경험 지표
- 데이터 가용성: 사용자 요청 시점 데이터 준비 상태
- 로딩 상태 지속 시간: 스켈레톤/로딩 상태 노출 시간
- 데이터 신선도: 마지막 업데이트로부터 경과 시간
- 오프라인 가용성: 네트워크 불안정 시 서비스 지속성
HTTP 캐싱 헤더 설정에서는 정적 자산에 대해 긴 만료 시간을 설정하되, 파일명에 해시를 포함하여 내용이 변경될 때마다 새로운 파일명을 생성하도록 해야 합니다. 이를 통해 캐싱과 업데이트를 모두 효과적으로 관리할 수 있습니다.
리소스별 캐시 전략 분류
영구 캐시 대상 (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 활용: 엣지에서 더 긴 캐시, 브라우저에서 짧은 캐시
파일명 해시 생성 및 관리
정적 에셋 해시 전략:
- Webpack: [contenthash] 사용
- Next.js: 자동 해시 생성 및 관리
- 예시: main.a1b2c3d4.js, styles.e5f6g7h8.css
해시 길이 최적화:
- 8자리: 일반적인 용도, 충돌 확률 낮음
- 12자리: 대규모 프로젝트, 더 안전한 고유성
- 16자리: 엔터프라이즈급, 최대 안전성
캐시 우선순위 및 전략
Stale While Revalidate
적용 대상: 주기적 업데이트 콘텐츠
동작 방식:
1. 캐시된 데이터 즉시 반환
2. 백그라운드에서 네트워크 요청
3. 응답 받으면 캐시 업데이트
장점: 빠른 응답 + 데이터 신선도
적용 사례: 뉴스 피드, 상품 목록