1초의 로딩 지연이 전환율을 7% 떨어뜨린다는 연구가 있습니다. Amazon은 로딩 시간이 100ms 늘어날 때마다 매출이 1% 감소한다고 밝혔습니다. 성능은 사용자 경험의 일부이며, 검색 엔진 순위에도 영향을 미칩니다. 이 글에서는 실질적인 웹 성능 최적화 방법을 설명합니다.
성능 측정 먼저
최적화하기 전 현재 상태를 측정해야 합니다. 측정 없는 최적화는 추측입니다.
Core Web Vitals
구글이 정의한 핵심 웹 성능 지표입니다.
LCP (Largest Contentful Paint): 가장 큰 콘텐츠가 표시되는 시간. 2.5초 이하가 Good, 4초 이상은 Poor.
FID (First Input Delay): 첫 입력(클릭, 키 입력)에 반응하는 시간. 100ms 이하가 Good.
CLS (Cumulative Layout Shift): 페이지 로딩 중 레이아웃이 얼마나 많이 이동하는지. 0.1 이하가 Good.
측정 도구
- Google PageSpeed Insights: URL 입력만으로 즉시 분석, 개선 제안까지 제공
- Lighthouse: Chrome DevTools에 내장, 더 상세한 분석
- WebPageTest: 다양한 기기와 네트워크 환경에서 테스트
이미지 최적화
이미지는 대부분의 웹페이지에서 가장 큰 파일 크기를 차지합니다.
다음 세대 이미지 포맷 사용
WebP는 JPEG보다 25~34% 작습니다. AVIF는 WebP보다 더 작지만 지원 브라우저가 제한적입니다.
Next.js는 <Image> 컴포넌트가 자동으로 WebP/AVIF 변환, 리사이징, 레이지 로딩을 처리합니다.
import Image from 'next/image';
// next/image는 자동으로 최적화됨
<Image
src="/hero.jpg"
width={800}
height={600}
alt="히어로 이미지"
priority // 첫 화면 이미지는 priority 설정
/>
레이지 로딩 (Lazy Loading)
화면에 보이지 않는 이미지는 나중에 로드합니다. 초기 로딩 시간을 크게 줄입니다.
<img src="below-fold.jpg" loading="lazy" alt="..." />
이미지 크기 지정
이미지에 width와 height를 명시하면 브라우저가 공간을 미리 확보해 CLS를 방지합니다.
JavaScript 번들 최적화
코드 분할 (Code Splitting)
모든 JavaScript를 처음에 다 불러오는 대신, 필요한 시점에 필요한 코드만 불러옵니다.
import dynamic from 'next/dynamic';
// 무거운 컴포넌트를 필요할 때만 로드
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <p>Loading...</p>,
ssr: false // 클라이언트에서만 렌더링
});
트리 쉐이킹 (Tree Shaking)
사용하지 않는 코드를 번들에서 제거합니다. 라이브러리에서 필요한 것만 임포트하세요.
// 나쁜 예: 전체 lodash를 가져옴 (70KB+)
import _ from 'lodash';
const result = _.get(obj, 'a.b.c');
// 좋은 예: 필요한 함수만 가져옴 (1KB)
import get from 'lodash/get';
const result = get(obj, 'a.b.c');
불필요한 패키지 제거
npm bundle-analyzer로 번들에 포함된 패키지 크기를 시각화합니다. 거대한 패키지를 더 가벼운 대안으로 교체하거나 제거합니다.
캐싱 전략
HTTP 캐싱
정적 파일(JS, CSS, 이미지)에 캐시 헤더를 설정해 브라우저가 재방문 시 서버에 요청하지 않게 합니다.
Cache-Control: public, max-age=31536000, immutable
Next.js는 _next/static/ 경로의 파일에 자동으로 긴 캐시 기간을 설정합니다.
CDN (Content Delivery Network) 활용
정적 파일을 전 세계 엣지 서버에 배포해 사용자에게 가장 가까운 서버에서 제공합니다.
Vercel과 Cloudflare는 기본적으로 CDN을 제공합니다. 서울에서 접속하면 서울 서버에서 파일을 받습니다.
CSS 최적화
사용하지 않는 CSS 제거
Tailwind CSS는 사용한 클래스만 최종 번들에 포함시킵니다. 커스텀 CSS를 사용한다면 PurgeCSS로 미사용 스타일을 제거하세요.
Critical CSS
첫 화면에 필요한 CSS를 <head>에 인라인으로 넣으면 스타일이 빠르게 적용됩니다.
서버 사이드 최적화
데이터베이스 쿼리 최적화
N+1 쿼리 문제는 성능 저하의 주범입니다.
// N+1 문제: 포스트 100개 + 각각 작성자 정보 = 101번의 쿼리
const posts = await Post.findAll();
for (const post of posts) {
post.author = await User.findById(post.userId); // 100번 추가 쿼리
}
// 해결: 한 번에 조인
const posts = await Post.findAll({
include: [{ model: User, as: 'author' }] // 1번의 쿼리
});
응답 압축
서버 응답을 gzip/brotli로 압축하면 전송 크기를 60~80% 줄일 수 있습니다.
Next.js는 기본적으로 gzip 압축을 적용합니다.
성능 모니터링
최적화 후에도 지속적으로 성능을 모니터링합니다.
- Vercel Analytics: Next.js 프로젝트의 Core Web Vitals를 자동으로 수집
- Google Search Console: 실제 사용자 기준 Core Web Vitals 데이터
성능 최적화는 한 번에 모든 것을 하려 하지 않아도 됩니다. 측정 후 가장 큰 영향을 미치는 것부터 순서대로 개선하는 것이 효율적입니다. 이미지 최적화와 불필요한 JavaScript 제거만으로도 대부분의 사이트에서 30~50%의 성능 향상을 볼 수 있습니다.