사용자들이 검색 결과에서 당신의 페이지를 클릭한 다음 3초 안에 만족감을 얻지 못하면, 그들은 곧바로 뒤로가기 버튼을 누릅니다. 이 짧은 순간이 SEO의 성패를 가르고, 전환율과 매출을 좌우합니다. 그 중심에는 이미지가 있습니다. 이미지 최적화는 페이지 속도 개선의 가장 큰 레버리지이자, Core Web Vitals를 직접적으로 끌어올리는 가장 확실한 방법입니다. 이 글에서는 이미지 압축과 지연 로딩(lazy loading)을 중심으로, 검색 순위를 끌어올리고 사용자 만족도를 극대화하는 실전 최적화 전략을 단계별로 안내합니다.
페이지 속도가 SEO에 미치는 영향: Core Web Vitals 관점
Google은 페이지 경험(Page Experience)을 순위 신호로 활용하며, 그 핵심은 Core Web Vitals입니다.
- LCP (Largest Contentful Paint): 가장 큰 콘텐츠(대개 히어로 이미지 또는 대제목)가 보이는 데 걸리는 시간. 목표 ≤ 2.5s
- INP (Interaction to Next Paint): 사용자 상호작용에 대한 반응성. 목표 ≤ 200ms
- CLS (Cumulative Layout Shift): 예기치 않은 레이아웃 이동의 정도. 목표 ≤ 0.1
이미지는 LCP와 CLS에 가장 큰 영향을 줍니다. 불필요하게 큰 이미지, 포맷 선택의 실수, 레이아웃 크기 미지정, 화면 밖 이미지의 조기 로딩은 모두 점수를 망치고 이탈률을 높입니다. 다행히도 이미지 압축과 지연 로딩만 제대로 해도 아래 효과를 기대할 수 있습니다.
- LCP 30~60% 개선 (히어로 이미지 포맷 전환 + 품질 최적화)
- CLS 0.2 → 0.05 이하 (width/height 고정 또는 CSS aspect-ratio 사용)
- 총 페이지 무게 50% 이상 감소 (WebP/AVIF + responsive 이미지)
- 크롤링 효율 증대 및 색인 품질 개선 (빠른 응답과 안정적인 렌더링)
이미지 압축의 기본: 포맷, 손실/무손실, 품질 전략
어떤 포맷을 선택해야 할까?
- WebP: 대부분의 브라우저 지원. JPEG 대비 25~35% 용량 절감. 투명도, 애니메이션 지원.
- AVIF: 최신 포맷. WebP 대비 추가 20~30% 절감 가능. 색상 밴딩에 강함. 다만 인코딩 비용이 높고 일부 환경 호환성 체크 필요.
- JPEG: 사진 이미지에 여전히 유용. 호환성 최고. 고품질 Q=70~85 권장.
- PNG: 선명한 선, 아이콘, 투명도가 필요한 경우. 가능하면 SVG로 대체.
- SVG: 로고/아이콘/일러스트에 최적. 벡터라 탁월한 선명도와 초경량.
- GIF: 애니메이션엔 비효율적. 가능하면 MP4/WebM 비디오로 대체.
실전 팁:
- 사진: AVIF → WebP → JPEG 순으로 폴백 제공
- 아이콘/로고: SVG 우선. 복잡한 일러스트는 WebP/AVIF와 비교 후 선택
- 스크린샷/텍스트가 많은 이미지: WebP(무손실) 또는 PNG 최종 비교
손실/무손실 압축과 권장 품질
- 손실 압축(lossy): 시각적으로 차이를 최소화하면서 용량을 크게 줄임. 대부분 사진에 적합.
- 무손실 압축(lossless): 픽셀 정보 유지. UI 요소, 로고, 인포그래픽 권장.
권장 품질 가이드:
- WebP 사진: quality 60~75
- AVIF 사진: cq-level 28~35 (인코더에 따라 다름)
- JPEG: quality 70~85
- PNG: 색상 수 줄이기(8-bit → 256 색), 무손실 최적화 사용
각 품질은 실제 시각 비교가 중요합니다. Squoosh 등 도구로 전후 비교한 뒤, 가장 낮은 품질 중 “티가 나지 않는 수준”을 선택하세요.
도구와 워크플로우: 수작업부터 자동화까지
노코드/디자이너 친화 도구
- Squoosh.app: 브라우저에서 AVIF/WebP/JPEG 변환 및 시각 비교
- TinyPNG/WebP, ImageOptim: 드래그 앤 드롭으로 무손실/손실 압축
개발자 워크플로우: CI/CD에 넣기
- Node.js Sharp: 대량 변환과 썸네일/리사이즈 자동화
- imagemin: 빌드 단계에서 자동 최적화
- GitHub Actions/CI에서 커밋 시 이미지 파이프라인 수행
예시: Sharp로 다양한 크기/포맷 생성
// scripts/process-images.js
const fs = require('fs');
const path = require('path');
const sharp = require('sharp');
const INPUT_DIR = 'assets/images/original';
const OUTPUT_DIR = 'public/images';
const sizes = [320, 640, 960, 1280, 1920];
const formats = [
{ ext: 'avif', options: { quality: 35, effort: 4 } },
{ ext: 'webp', options: { quality: 70 } },
{ ext: 'jpg', options: { quality: 80, progressive: true } },
];
fs.readdirSync(INPUT_DIR).forEach(file => {
const inputPath = path.join(INPUT_DIR, file);
const base = path.parse(file).name;
sizes.forEach(width => {
formats.forEach(({ ext, options }) => {
sharp(inputPath)
.resize({ width, withoutEnlargement: true })
.toFormat(ext, options)
.toFile(path.join(OUTPUT_DIR, `${base}-${width}.${ext}`))
.catch(console.error);
});
});
});
이 스크립트를 빌드 전에 실행하고, HTML에서는 responsive 이미지로 소비합니다.
반응형 이미지로 “과다운로드” 막기
모바일에서 데스크톱용 2000px 이미지를 내려받는 것은 낭비입니다. srcset과 sizes를 활용하세요.
<picture>
<source type="image/avif"
srcset="/images/hero-640.avif 640w, /images/hero-1280.avif 1280w, /images/hero-1920.avif 1920w"
sizes="(max-width: 768px) 90vw, (max-width: 1200px) 70vw, 1200px">
<source type="image/webp"
srcset="/images/hero-640.webp 640w, /images/hero-1280.webp 1280w, /images/hero-1920.webp 1920w"
sizes="(max-width: 768px) 90vw, (max-width: 1200px) 70vw, 1200px">
<img
src="/images/hero-1280.jpg"
srcset="/images/hero-640.jpg 640w, /images/hero-1280.jpg 1280w, /images/hero-1920.jpg 1920w"
sizes="(max-width: 768px) 90vw, (max-width: 1200px) 70vw, 1200px"
width="1200" height="700"
alt="제품을 사용하는 사람의 사진, 주요 혜택 텍스트"
decoding="async" fetchpriority="high" loading="eager">
</picture>
핵심 포인트:
- 히어로/LCP 이미지는 lazy 로드하지 말고
loading="eager",fetchpriority="high"또는<link rel="preload">를 사용 - 폭 다양한 이미지 제공으로 모바일에서 작은 파일만 다운로드
width/height또는 CSSaspect-ratio지정으로 CLS 방지- AVIF→WebP→JPEG 순으로 폴백
지연 로딩(Lazy Loading): 성능을 끌어올리는 기본기
네이티브 지연 로딩
현대 브라우저는 loading="lazy" 속성을 지원합니다.
<img
src="/images/article-1-640.webp"
alt="튜토리얼 스크린샷"
loading="lazy"
decoding="async"
width="640" height="400">
장점:
- 구현 간단, 추가 JS 불필요
- Google이 권장, SEO 친화적
주의:
- 뷰포트 상단 “접근 가능한” 이미지는 lazy 금지 (LCP 악화)
- 너무 공격적이면 사용자에게 보이기 직전에 깜박일 수 있음 → 네이티브는 적절한 프리패칭을 수행하지만, JS 방식이라면 rootMargin 설정 필요
JS 기반 고급 제어 (IntersectionObserver)
네이티브로 충분한 경우가 많지만, 사용자 경험을 더 정교하게 다듬고 싶다면 JS로 threshold와 placeholder 전략을 결합합니다.
<img
class="lazy"
data-src="/images/gallery-1280.avif"
data-srcset="/images/gallery-640.avif 640w, /images/gallery-1280.avif 1280w"
data-sizes="(max-width: 768px) 90vw, 1280px"
alt="포트폴리오 이미지"
width="1280" height="800"
src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1280' height='800'%3E%3C/svg%3E">
<noscript>
<img src="/images/gallery-1280.avif" alt="포트폴리오 이미지" width="1280" height="800">
</noscript>
<script>
const opts = { rootMargin: '200px 0px', threshold: 0.01 };
const onIntersect = entries => {
entries.forEach(entry => {
if (!entry.isIntersecting) return;
const img = entry.target;
img.src = img.dataset.src;
if (img.dataset.srcset) img.srcset = img.dataset.srcset;
if (img.dataset.sizes) img.sizes = img.dataset.sizes;
observer.unobserve(img);
});
};
const observer = 'IntersectionObserver' in window
? new IntersectionObserver(onIntersect, opts)
: null;
document.querySelectorAll('img.lazy').forEach(img => {
if (observer) observer.observe(img);
else {
img.src = img.dataset.src; // 폴백
if (img.dataset.srcset) img.srcset = img.dataset.srcset;
if (img.dataset.sizes) img.sizes = img.dataset.sizes;
}
});
</script>
핵심:
rootMargin: '200px'로 미리 로드하여 사용자 스크롤보다 앞서 캐싱- JS 방식이라면 중요한 이미지의
<noscript>폴백 추가 (크롤러/JS 미실행 환경 대비) - SEO 측면에서 이미지 검색이 중요한 경우, 가능한 네이티브 lazy 또는
src를 실제 URL로 지정
배경 이미지 lazy 로딩
CSS background-image는 네이티브 lazy 대상이 아닙니다. JS와 data-속성을 조합하세요.
<div class="card bg-lazy" data-bg="/images/card-960.webp" style="aspect-ratio: 4 / 3;"></div>
<script>
const obs = new IntersectionObserver(entries => {
entries.forEach(e => {
if (!e.isIntersecting) return;
e.target.style.backgroundImage = `url('${e.target.dataset.bg}')`;
obs.unobserve(e.target);
});
}, { rootMargin: '200px' });
document.querySelectorAll('.bg-lazy').forEach(el => obs.observe(el));
</script>
참고: LCP 후보가 배경 이미지라면 <img>로 전환하는 것이 LCP 개선에 유리합니다.
CLS 없는 로딩: 플레이스홀더와 레이아웃 고정
- 고정 크기: 항상
width/height속성을 명시하거나 CSSaspect-ratio사용 - LQIP/Blur-up: 낮은 해상도 이미지(수 KB)를 먼저 보여주고 고해상도로 교체
- 컬러 도미넌트/스켈레톤: 이미지의 평균 색 또는 흐림 배경으로 깜박임 최소화
예시: Blur-up CSS
<img class="blur-up" src="/images/hero-40w.webp"
data-src="/images/hero-1280.webp" width="1200" height="700" alt="히어로">
<style>
.blur-up {
filter: blur(20px);
transform: scale(1.05);
transition: filter .4s ease, transform .4s ease;
}
.blur-up.loaded {
filter: blur(0);
transform: none;
}
</style>
<script>
const img = document.querySelector('.blur-up');
const high = new Image();
high.src = img.dataset.src;
high.onload = () => {
img.src = high.src;
img.classList.add('loaded');
};
</script>
이 기법은 시각적 매끄러움을 주며, CLS를 증가시키지 않습니다.
전달 최적화: CDN, 캐싱, HTTP/2/3
이미지를 빠르게 보내는 것도 중요합니다.
- 이미지 CDN 사용: Cloudinary, Imgix, Cloudflare Images, Akamai Image Manager 등
- 장점: 실시간 리사이즈, 포맷 자동 협상, 품질 최적화, 기기별 전달
- 예: URL 파라미터로
/w_640,q_70,f_webp/같은 변환 적용
- 캐싱 헤더:
Cache-Control: public, max-age=31536000, immutable(파일명에 해시를 쓰는 경우)- 버전 변경 시 파일명 해시를 바꿔 캐시 무효화
- 압축: 텍스트 자원은 Brotli/HTTP/2 서버 푸시(이제는 preload 권장) 고려
- HTTP/2/3 다중화: 개별 연결의 지연을 줄이고, 작은 이미지 여러 개 전송 시 효율적
실전 팁:
- 히어로 이미지는
<link rel="preload" as="image" imagesrcset="...">로 가장 먼저 받기 - 우선순위 힌트:
<img fetchpriority="high">또는<link rel="preload" fetchpriority="high"> - 서비스 워커로 갤러리/다음 페이지 이미지 사전 캐싱 고려(과도한 프리캐시 금지)
CMS/프레임워크별 빠른 적용법
WordPress
- 기본 lazy 로딩 지원(5.5+). 히어로 이미지는
loading="eager"로 오버라이드 - 플러그인: ShortPixel, Imagify, Smush, WebP Express 등
- 기능: WebP/AVIF 변환, 썸네일 자동 생성, 품질 자동 최적화
- 테마 수정:
add_filter('wp_lazy_loading_enabled', ...)로 세밀 제어 - 중요한 이미지에
fetchpriority="high"추가 (전용 플러그인 또는 테마 템플릿 수정)
Shopify
- Theme Editor에서 이미지 크기 파라미터 지정(
{{ image | image_url: width: 640 }}) loading: 'lazy'속성 및srcset/sizes구성- 앱: Crush.pics, TinyIMG 등으로 자동 최적화
Next.js
next/image컴포넌트 사용: 포맷 협상, lazy, blurDataURL 지원priority로 LCP 이미지 우선 다운로드- next.config.js에서 도메인/디바이스 사이즈 설정
import Image from 'next/image';
export default function Hero() {
return (
<Image
src="/hero.jpg"
alt="히어로"
width={1200}
height={700}
priority
placeholder="blur"
blurDataURL="/hero-40w.jpg"
sizes="(max-width: 768px) 90vw, 1200px"
/>
);
}
Gatsby/Nuxt
- Gatsby:
gatsby-plugin-image의 StaticImage/GatsbyImage로 blur-up, lazy 기본 제공 - Nuxt:
@nuxt/image모듈로 자동 리사이즈/포맷 변환과 lazy 지원
SEO 관점에서의 주의사항
- 이미지 인덱싱: 중요한 이미지는 실제
src속성이 있는<img>로 제공. JS 전용 로딩은<noscript>폴백 제공 - alt 텍스트: 키워드 채우기 대신 콘텐츠를 설명하는 자연스러운 문장. 접근성과 이미지 검색 순위 모두에 중요
- 구조화 데이터: 제품/레시피/블로그 포스트에 적절한 Schema.org와 함께 고해상도 이미지 제공
- 사이트맵: image sitemap 확장(
<image:image>)으로 중요 이미지 색인 촉진 - 모바일 우선: 모바일 LCP를 최우선으로 최적화. 모바일에서 100~200KB 내로 히어로 이미지를 유지하는 것을 목표
측정과 모니터링: 개선 없이는 최적화도 없다
- PageSpeed Insights/Lighthouse: 실험실 데이터와 필드 데이터(CrUX) 확인
- Search Console → Core Web Vitals 보고서: URL 그룹 별 이슈 파악
- WebPageTest: 초기 연결, TTFB, 실제 디바이스/지역 테스트
- 성능 예산 설정: “이미지 총합 400KB 이하(초기뷰), 요청 수 30개 이하” 등
- 배포 전에 LCP/CLS 회귀 테스트 자동화: Lighthouse CI 도입
실전 체크:
- LCP 요소가 이미지인가? 그렇다면 그 이미지는 lazy가 아닌가?
- 그 이미지가
인가, CSS 배경인가? 가능하면
로 전환
- width/height가 빠져 CLS를 유발하지 않는가?
- hero 이미지가 preload/priority 처리 되었는가?
- srcset/sizes가 실제 레이아웃과 맞는가? 과다운로드 없는가?
예제: 전/후 개선 시나리오
상황:
- 랜딩 페이지 히어로 JPEG 1.4MB, 배경 CSS 이미지, 모바일에서도 동일 자원
- 본문 갤러리 이미지 12장, 모두 1200px JPEG, lazy 미적용
- CLS 0.22, LCP 4.8s(모바일)
개선:
- 히어로를
로 변환, AVIF/WebP 추가, 1280/1920 소스셋 제공
- 모바일 사이즈 우선, JPEG 폴백 유지. preload + fetchpriority=high
- 본문 이미지는 WebP로 변환, 640/960/1280 소스셋 +
loading="lazy"적용 - 모든 이미지 width/height 지정, aspect-ratio 보강, blur-up
- CDN 캐싱과 파일명 해시 도입
결과(실측):
- 히어로 1.4MB → 180KB(AVIF 1280w 기준)
- LCP 4.8s → 2.1s
- CLS 0.22 → 0.03
- 초기 페이지 전송 총량 3.2MB → 950KB
- 이탈률 18% 개선, 전환율 9% 증가
고급 팁: 더 깐깐한 최적화
- 비디오화: GIF 애니메이션은 MP4/WebM 비디오로 변환. 용량 80~95% 절감
- Client Hints/자동 포맷 협상: 이미지 CDN이
Accept헤더 기반으로 AVIF/WebP를 자동 제공 - Progressive JPEG: 저해상도 프리뷰로 체감 로딩 개선(네트워크 환경에 따라 효과 상이)
- Art direction:
picture로 모바일/데스크톱에서 다른 크롭을 제공해 의미 있는 콘텐츠만 노출 - 이미지 합치기 vs 분리: 아이콘은 SVG sprite, 하지만 큰 스프라이트 한 장은 초기 다운로드 부담 주의
- 데이터 URI: 아주 작은 아이콘만 인라인, 큰 이미지는 외부 파일로 캐시 이점 활용
- 접근성: 장식용 이미지에는
alt=""로 스크린리더 소음 제거. 의미 있는 이미지는 대체 텍스트 충실히 작성
실전 체크리스트
- 전략
- LCP 목표 2.5s 이하, CLS 0.1 이하, INP 200ms 이하
- 초기 화면 이미지 총합 400~600KB 이하
- 포맷/압축
- AVIF/WebP 우선, JPEG/PNG 폴백
- 사진 Q=60
75(WebP), cq-level=2835(AVIF) - GIF → MP4/WebM 변환
- 마크업
- 히어로는 eager + fetchpriority=high + preload
- 본문/갤러리는 loading=lazy
- 모든 이미지 width/height 또는 aspect-ratio 지정
- srcset/sizes로 반응형 제공
- 중요한 이미지의 noscript 폴백(JS 로더 사용 시)
- 전달
- 이미지 CDN 적용 및 캐시 헤더 설정
- 파일명 해시로 캐시 무효화 관리
- 검증
- Lighthouse/PSI로 LCP/CLS 개선 확인
- Search Console Core Web Vitals 정상 여부 확인
- 실제 기기/3G/4G 환경 테스트
자주 하는 실수와 해결책
- 실수: 모든 이미지를 lazy로 설정
- 해결: LCP 후보(히어로, 상단 배너)는 eager + priority
- 실수: CSS 배경으로 히어로 제공
- 해결: 가능한
로 전환, picture/srcset 활용
- 해결: 가능한
- 실수:
width/height미지정 → CLS 급증- 해결: 고정 크기 지정 또는 aspect-ratio 사용
- 실수: 반응형 이미지 없이 2000px 이미지를 모바일에 전송
- 해결: srcset/sizes 구성 및 빌드 파이프라인에서 다양한 width 생성
- 실수: JS 전용 lazy 로더에만 의존하고
src없이data-src만 사용- 해결: 네이티브 lazy 우선 또는 noscript 폴백 제공
- 실수: PNG 과다 사용
- 해결: 사진은 WebP/AVIF/JPEG, UI는 SVG/PNG(최소 팔레트) 혼용
단계별 실행 플랜
- 핵심 페이지 선정
- 유입/전환 비중이 큰 랜딩/상품/카테고리 페이지부터
- LCP 요소가 이미지인지 확인(Lighthouse)
- 전략 수립
- 히어로 이미지 크기 목표(모바일 100
200KB, 데스크톱 200300KB) - 반응형 width 세트 정의: 320/640/960/1280/1920
- 도구 설정
- Sharp 또는 이미지 CDN 선택
- CI에서 이미지 생성/변환 자동화
- 마크업 개선
- 히어로: picture + eager + fetchpriority/preload
- 본문: loading=lazy + srcset/sizes + width/height
- 시각적 품질 검토
- Squoosh로 품질 60→70→80 비교 후 최저 수용 수준 선택
- Blur-up 또는 컬러 플레이스홀더 적용
- 전달 최적화
- CDN 캐시/도메인 설정, 파일명 해시
- HTTP/2/3 활성화
- 측정/배포/모니터
- Lighthouse CI로 성능 회귀 방지
- Search Console/CrUX로 필드 데이터 관찰
결론: 가장 큰 성과는 “이미지”에서 나온다
페이지 속도 최적화는 끝없는 미세 조정의 연속처럼 보일 수 있지만, 실제로는 80/20 법칙이 적용됩니다. 이미지 압축과 지연 로딩, 반응형 제공, LCP 우선순위 조정만 제대로 해도 검색 노출, 사용자 만족, 전환율에서 즉각적인 상승 효과를 볼 수 있습니다. 오늘 바로 히어로 이미지부터 점검하고, 소스셋과 lazy 로딩을 도입하세요. 빠른 페이지는 곧 더 높은 순위, 더 많은 방문자, 더 큰 매출로 돌아옵니다.