Claude Code로 이미지 지연 로딩 안전하게 구현하기
Claude Code로 이미지 지연 로딩을 구현하며 LCP, CLS, srcset, SEO 함정과 측정 방법을 정리합니다.
이미지 지연 로딩은 모든img에loading="lazy"를 붙이는 작업이 아닙니다. 첫 화면의 hero 이미지까지 늦게 불러오면 Largest Contentful Paint, 즉 사용자가 핵심 콘텐츠를 보는 시간이 나빠질 수 있습니다. 반대로 상품 목록, 갤러리, 관련 글 썸네일까지 전부 먼저 가져오면 모바일 회선에서 불필요한 전송량이 커집니다.
이 글은 초보자도 Claude Code에 안전하게 맡길 수 있는 기준을 정리합니다. 네이티브loading="lazy", LCP 후보의 eager 처리,decoding, fetchpriority, CLS를 막는 치수 지정, 반응형srcset/sizes, 고급 상황의 IntersectionObserver, placeholder, SEO와 UX 실패 사례, 성능 측정, 안전한 Claude Code prompt를 모두 다룹니다. 기준은 MDN의img요소, Intersection Observer API, web.dev의브라우저 수준 이미지 lazy loading, Core Web Vitals, Chrome의LCP request discovery를 확인했습니다.
이미지 압축과 포맷 변환까지 함께 보려면이미지 최적화, 로딩 중 골격 UI는스켈레톤 로딩, 사이트 전체 속도는성능 최적화와 연결해서 읽으면 좋습니다.
먼저 기준을 정한다
첫 번째 기준은 단순합니다. 사용자가 첫 화면에서 바로 보는 이미지는 lazy로 만들지 않습니다. 스크롤해야 보이는 이미지만 lazy 대상으로 잡습니다. 그리고 모든 이미지는width와height, 또는 같은 역할의aspect-ratio로 공간을 미리 확보합니다.
| 이미지 위치 | loading | fetchpriority | decoding | 필수 보호 장치 |
|---|---|---|---|---|
| Hero 이미지, LCP 후보 | eager 또는 생략 | high 검토 | sync 또는 async | 초기 HTML에서 바로 발견 가능 |
| 본문 중간 도해 | lazy | auto | async | 치수 유지 |
| 상품 목록 두 번째 화면 이후 | lazy | auto | async | srcset과sizes 사용 |
| 캐러셀, 무한 스크롤 | 상황별 | auto | async | 필요하면 IntersectionObserver |
초보자가 자주 하는 실수는 “이미지가 크니까 lazy”라고 판단하는 것입니다. 실제 기준은 “첫 의미 있는 화면에 필요한가”입니다. Chrome의 LCP 문서는 LCP 이미지가 HTML에서 빨리 발견되고 적절히 우선되어야 하며,loading=lazy를 피해야 한다고 설명합니다.
복사해서 쓰는 HTML
첫 번째 이미지는 hero 이미지이므로 eager와 높은 우선순위를 사용합니다. 두 번째 이미지는 글 아래쪽 또는 상품 목록 후반에 있는 이미지라서 네이티브 lazy와 async decoding을 사용합니다.
<img
class="hero-image"
src="/images/hero/product-dashboard-1200.webp"
srcset="
/images/hero/product-dashboard-640.webp 640w,
/images/hero/product-dashboard-1200.webp 1200w
"
sizes="100vw"
alt="제품 대시보드 첫 화면"
width="1200"
height="675"
loading="eager"
fetchpriority="high"
decoding="sync"
/>
<img
class="article-image"
src="/images/articles/setup-step-800.webp"
srcset="
/images/articles/setup-step-400.webp 400w,
/images/articles/setup-step-800.webp 800w
"
sizes="(max-width: 720px) 100vw, 720px"
alt="설정 단계 스크린샷"
width="800"
height="450"
loading="lazy"
decoding="async"
/>
decoding="async"는 브라우저에게 이미지 디코딩을 다른 렌더링 작업과 병행하기 쉽게 하라는 힌트입니다. 본문 이미지와 썸네일에는 자연스러운 기본값입니다. Hero 이미지는sync와async를 실제로 측정해서 결정합니다.
CLS를 막는 CSS
CLS는 Cumulative Layout Shift입니다. 쉽게 말해 페이지가 로드되는 동안 글, 버튼, 광고, CTA가 얼마나 밀리는지 보는 지표입니다. 이미지가 나중에 높이를 갖게 되면 사용자가 누르려던 버튼 위치가 바뀌고, 구매나 문의 전환에도 영향을 줍니다.
.image-frame {
aspect-ratio: 16 / 9;
background: #f3f4f6;
overflow: hidden;
}
.image-frame > img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
@media (prefers-reduced-motion: no-preference) {
.image-frame > img {
transition: opacity 180ms ease-out;
}
}
CMS에서 원본 이미지의 너비와 높이를 알 수 있다면 반드시 저장해서 템플릿으로 전달하세요. 외부 이미지라 치수를 알 수 없다면 컨테이너에 안정적인aspect-ratio를 주고 실제 이미지를object-fit으로 맞춥니다. 완벽하지는 않아도 0 높이 placeholder보다는 훨씬 안전합니다.
React 컴포넌트로 정책 고정
Claude Code에 React 수정을 맡길 때는 작은 컴포넌트 안에 정책을 넣는 편이 안정적입니다. priority 이미지는 eager와 high priority, 일반 콘텐츠 이미지는 lazy, 모든 이미지는 치수를 유지한다는 계약을 코드로 고정합니다.
type SmartImageProps = {
src: string;
srcSet?: string;
sizes?: string;
alt: string;
width: number;
height: number;
priority?: boolean;
className?: string;
};
export function SmartImage({
src,
srcSet,
sizes,
alt,
width,
height,
priority = false,
className,
}: SmartImageProps) {
const loading = priority ? "eager" : "lazy";
const fetchPriority = priority ? "high" : "auto";
const decoding = priority ? "sync" : "async";
return (
<span
className={`image-frame ${className ?? ""}`}
style={{ aspectRatio: `${width} / ${height}` }}
>
<img
src={src}
srcSet={srcSet}
sizes={sizes}
alt={alt}
width={width}
height={height}
loading={loading}
fetchPriority={fetchPriority}
decoding={decoding}
/>
</span>
);
}
이 컴포넌트는 의도적으로 단순합니다. 중요한 이미지를 JavaScript 뒤에 숨기지 않고, 기존 프레임워크의 이미지 파이프라인과도 충돌하지 않습니다.
실제 제품 유스케이스
첫 번째는 이커머스 상품 목록입니다. 첫 화면의 몇 개 상품은 바로 보일 수 있으므로 전부 lazy로 만들면 안 됩니다. 두 번째 화면 이후의 상품, 추천 상품, 리뷰 이미지, 최근 본 상품은 좋은 대상입니다. 실패 예시는 모든 썸네일에fetchpriority="high"를 붙이는 것입니다. 브라우저가 모든 이미지를 긴급 요청으로 보고 CSS나 hero 이미지가 늦어질 수 있습니다.
두 번째는 미디어 글과 튜토리얼입니다. 커버 이미지는 LCP 후보가 되기 쉬우므로 eager로 두고, 본문 중간 스크린샷과 관련 글 카드는 lazy로 둡니다. 실패 예시는 blur placeholder만 강조하고 실제 이미지의alt를 비워 버리는 것입니다. 의미 있는 도해라면 검색 엔진과 스크린 리더가 이해할 설명이 필요합니다.
세 번째는 SaaS 대시보드입니다. 아바타, 고객 로고, 리포트 썸네일, 감사 로그 이미지가 대량으로 나올 수 있습니다. 보이지 않는 행을 lazy로 두는 것은 효과적입니다. 하지만 상단의 핵심 차트, 온보딩 이미지, CTA 근처 이미지를 늦추면 사용자가 첫 행동을 시작하기 어려워집니다. CTA와 수익 이벤트까지 보려면Analytics 구현과 함께 설계합니다.
네 번째는 갤러리와 가로 캐러셀입니다. 네이티브 lazy로 충분한 경우가 많지만, 커스텀 스크롤 컨테이너, 숨겨진 slide, 세밀한 선로딩 거리 제어가 필요하면 IntersectionObserver가 유용합니다.
IntersectionObserver를 쓸 때
기본은 네이티브 lazy입니다. IntersectionObserver는 300px 전에 불러오기,data-src교체, placeholder class 제거, 특정 스크롤 영역 관찰처럼 더 많은 제어가 필요할 때 사용합니다.
<img
class="js-lazy-image"
src="/images/placeholders/report-thumb.svg"
data-src="/images/reports/report-2026.webp"
data-srcset="/images/reports/report-2026.webp 1x"
alt="월간 리포트 썸네일"
width="640"
height="360"
/>
const lazyImages = document.querySelectorAll("img[data-src]");
function loadImage(img) {
img.src = img.dataset.src;
if (img.dataset.srcset) {
img.srcset = img.dataset.srcset;
}
img.removeAttribute("data-src");
img.removeAttribute("data-srcset");
}
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver((entries, currentObserver) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
loadImage(entry.target);
currentObserver.unobserve(entry.target);
});
}, {
rootMargin: "300px 0px",
threshold: 0.01,
});
lazyImages.forEach((image) => observer.observe(image));
} else {
lazyImages.forEach(loadImage);
}
JavaScript 경로에서도 치수는 유지합니다. data-src이미지는 JavaScript가 실패하면 로드되지 않을 수 있으므로, 주요 상품 이미지나 글의 hero 이미지에는 이 패턴을 쓰지 않는 편이 안전합니다.
측정 방법
Core Web Vitals의 핵심은 LCP, INP, CLS입니다. 좋은 기준은 LCP 2.5초 이내, INP 200ms 이하, CLS 0.1 이하입니다. 이미지 lazy loading에서는 특히 LCP와 CLS가 바뀌기 쉽습니다.
npm install web-vitals
import { onCLS, onINP, onLCP } from "web-vitals";
onLCP((metric) => {
const lastEntry = metric.entries.at(-1);
console.log("LCP", metric.value, lastEntry?.element);
});
onCLS((metric) => {
console.log("CLS", metric.value, metric.entries);
});
onINP((metric) => {
console.log("INP", metric.value, metric.entries);
});
Chrome DevTools의 Performance 패널에서 LCP 요소가 예상한 hero 이미지인지, 이미지 요청이 HTML에서 빨리 발견되는지, 어떤 요소가 layout shift를 만드는지 확인합니다. 배포 후에는 PageSpeed Insights, Search Console, 자체 analytics를 함께 봅니다.
Claude Code 안전 prompt
Claude Code는 일괄 변경을 잘합니다. 그래서 범위, 금지 사항, 검증을 먼저 줘야 합니다.
{
"goal": "이미지 지연 로딩을 안전하게 추가한다",
"scope": [
"본문 이미지와 상품 목록 이미지만 대상으로 한다",
"첫 화면 이미지와 LCP 후보 이미지는 lazy로 만들지 않는다"
],
"rules": [
"모든 img에 alt, width, height를 유지한다",
"아래쪽 이미지는 loading=\"lazy\"와 decoding=\"async\"를 사용한다",
"hero 이미지는 loading=\"eager\"를 쓰거나 loading을 생략한다",
"fetchpriority=\"high\"는 LCP 후보 1장까지만 사용한다"
],
"verification": [
"MDX와 코드 fence를 확인한다",
"DevTools로 LCP와 CLS를 확인한다",
"모바일 폭에서 이미지, 텍스트, CTA가 겹치지 않는지 본다"
]
}
Claude Code에는 어떤 이미지를 lazy로 바꾸지 않았는지와 이유도 보고하게 합니다. 좋은 최적화에는 의도적인 비변경이 포함됩니다.
실패 예시 체크리스트
- Hero 이미지, 메인 상품 이미지, 상단 CTA 배경을 lazy로 만들었다.
- 리팩터링 중
width와height를 제거했다. - 모든 이미지에
fetchpriority="high"를 붙였다. srcset후보 이미지의 비율이 서로 다르다.- CSS 배경 이미지에
loading속성이 적용된다고 착각했다. - 의미 있는 도해의
alt를 비웠다. - 중요한 이미지가 JavaScript로 주입되는
src에만 의존한다. - placeholder가 실제 콘텐츠보다 더 눈에 띈다.
CTA와 검증 메모
이미지 지연 로딩은 점수만 위한 작업이 아닙니다. 읽기 깊이, CTA 가시성, 상품 탐색, 앱의 첫 행동까지 영향을 줍니다. 개인은무료 Claude Code cheatsheet로 시작하고, 재사용 prompt와 리뷰 템플릿이 필요하면제품 페이지를 봅니다. 팀이 성능, SEO, analytics, 수익 경로를 함께 정리하려면Claude Code 교육과 상담이 다음 단계입니다.
이번 업데이트에서는 위 공식 문서를 기준으로 판단을 다시 확인했고, “먼저 lazy로 만들지 않을 이미지를 정한다”는 흐름으로 예시를 다시 구성했습니다. Masa의 작업에서는 hero eager, 본문 하단 스크린샷 lazy, 모든 이미지 치수 지정, 모바일용srcset, 실패 예시와 검증 항목을 포함한 Claude Code prompt가 가장 안정적이었습니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Claude Code 권한 세이프티 래더: 통제력을 잃지 않고 allow 넓히기
read-only에서 제한 편집, 검증 명령, deploy 확인까지 권한을 단계적으로 넓히는 방법.
Claude Code Small PR Proof Pack: 작은 PR을 리뷰 가능한 상태로 만드는 증거 세트
Claude Code의 작은 PR에 diff, 검증, 공개 URL, CTA 경로, rollback을 붙이는 실무 체크리스트.
Claude Code 커밋 전 리뷰 게이트: diff, 테스트, 공개 URL, CTA 확인
Claude Code 작업을 커밋하기 전에 diff 범위, build, 공개 URL, Gumroad 링크, 상담 CTA, 테스트 누락과 무관한 파일을 확인하는 방법입니다.