본문 바로가기
카테고리 없음

Lazy Loading (LCP, CLS, 스켈레톤 UI)

by BOOST YOUR INFORMATION 2026. 6. 16.

Lazy Loading (LCP, CLS, 스켈레톤 UI)
Lazy Loading

loading="lazy" 한 줄이면 웹 성능 최적화가 끝난다고 생각한 적 있으셨나요? 저도 그랬습니다. 처음 이 속성을 발견했을 때 "이게 전부야?" 싶어서 사이트 이미지 전체에 붙여버렸습니다. 결과는 오히려 점수가 떨어지는 역효과였습니다. 한 줄짜리 최적화처럼 보이는 것이 잘못 쓰면 독이 된다는 걸, 직접 겪고 나서야 제대로 이해했습니다.

Lazy Loading과 LCP: 무조건 적용이 아닌 선택적 적용

일반적으로 Lazy Loading은 "적용할수록 좋다"고 알려져 있습니다. 제 경험상 이건 반은 맞고 반은 틀립니다.

Lazy Loading이란 사용자의 뷰포트, 즉 현재 화면에 보이는 영역에 이미지가 들어오기 전까지 로드를 미루는 기술입니다. HTML <img> 태그에 loading="lazy" 속성 하나만 추가하면 Chrome, Firefox 등 주요 브라우저에서 별도 스크립트 없이 동작합니다. 여기까지만 보면 쓰지 않을 이유가 없어 보입니다.

문제는 LCP(Largest Contentful Paint)입니다. LCP란 페이지에서 가장 큰 콘텐츠 요소가 화면에 렌더링되는 데 걸리는 시간을 측정하는 Core Web Vitals 지표입니다. 구글은 LCP 2.5초 이하를 우수 기준으로 제시하고 있습니다(출처: web.dev). 첫 화면에 바로 보이는 히어로 이미지에 loading="lazy"를 붙이면, 브라우저는 이미 뷰포트 안에 있는 이미지임에도 로드를 늦춥니다. 당연히 LCP 점수가 나빠집니다.

제가 직접 겪은 수치로 말씀드리면, 사이트 전체 이미지에 lazy를 적용했을 때 Lighthouse 기준 LCP가 3.2초였습니다. 히어로 이미지에만 loading="eager"fetchpriority="high"를 붙이고 스크롤 하단 이미지에만 lazy를 제한적으로 적용한 뒤에는 1.8초로 줄었습니다. 코드 한 줄을 바꾼 게 아니라 어디에 쓸지 판단을 바꾼 결과입니다.

Above the Fold 영역, 즉 스크롤 없이 처음 보이는 화면 안의 이미지에는 절대로 lazy를 붙이면 안 됩니다. 반드시 loading="eager"를 명시하거나 속성 자체를 생략해야 합니다. 더 세밀한 제어가 필요한 상황이라면 Intersection Observer API를 활용한 JavaScript 구현이나 lazysizes, Vanilla-lazyload 같은 라이브러리를 고려할 수 있습니다. Intersection Observer API란 특정 요소가 뷰포트에 진입하거나 벗어나는 시점을 비동기적으로 감지하는 브라우저 내장 API로, 스크롤 이벤트를 직접 감지하는 방식보다 성능 부담이 훨씬 적습니다.

올바른 적용 방식을 정리하면 다음과 같습니다.

  • 히어로 이미지, 첫 화면 로고 등 Above the Fold 요소: loading="eager" 또는 fetchpriority="high" 적용
  • 스크롤 아래 콘텐츠 이미지, 카드 썸네일 등: loading="lazy" 적용
  • 이미지 태그에는 반드시 widthheight 속성을 명시해 CLS 방지

CLS(Cumulative Layout Shift)란 페이지 로딩 중 요소가 예고 없이 이동하면서 발생하는 레이아웃 불안정성을 수치화한 지표입니다. 브라우저가 이미지 크기를 미리 모르면 공간을 예약하지 못하고, 이미지가 뒤늦게 로드될 때 주변 텍스트와 버튼이 밀리는 현상이 생깁니다. CLS 점수는 0.1 이하를 유지하는 것이 권장 기준입니다(출처: web.dev Core Web Vitals).

스켈레톤 UI: 체감 속도는 실제 속도와 다르다

솔직히 이건 예상 밖이었습니다. 스켈레톤 UI를 도입했을 때 실제 로딩 시간은 거의 달라지지 않았는데, 사용자 피드백에서 "로딩이 빨라진 것 같다"는 반응이 나왔습니다.

스켈레톤 UI(Skeleton UI)란 콘텐츠가 로드되기 전 실제 레이아웃 형태와 유사한 회색 자리 표시자를 보여주는 UI 패턴입니다. 빈 화면이나 스피너 대신 카드 모양, 텍스트 줄 모양의 placeholder를 미리 그려두는 방식입니다. 이 패턴은 CLS 방지에도 효과적인데, 이미지가 로드되기 전부터 공간을 잡아두기 때문에 레이아웃이 흔들리지 않습니다.

제가 구현할 때는 단순한 회색 박스 대신 CSS keyframes 애니메이션으로 shimmer 효과를 넣었습니다. 빛이 가로로 훑고 지나가는 느낌을 주는 그 효과입니다. 이 애니메이션 하나가 체감 로딩 속도에 꽤 큰 영향을 미쳤습니다. 사람은 아무것도 없는 화면보다 무언가 진행 중이라는 시각적 신호가 있을 때 기다림을 덜 느낀다는 걸, 숫자가 아니라 반응으로 확인한 경험이었습니다.

React에서 스켈레톤을 구현할 때 한 가지 함정이 있습니다. 제가 직접 써봤는데 이미지를 조건부 렌더링(isLoading && <img>)으로 처리하면 onLoad 이벤트가 제대로 발생하지 않아 스켈레톤이 영원히 사라지지 않는 버그가 생깁니다. 이미지는 항상 DOM에 유지하되 opacity: 0 또는 visibility: hidden 등 CSS로 숨기고, 로드 완료 시 표시하는 방식으로 처리해야 합니다.

다만 스켈레톤 UI 남용은 경계해야 합니다. 제 경험상 이건 좀 다릅니다. 페이지의 모든 요소에 스켈레톤을 붙이면 화면 전체가 반짝이는 회색 박스로 가득 차서, 빠른 느낌보다 오히려 불안한 느낌을 줍니다. LCP나 CLS 같은 수치 지표와 마찬가지로, 스켈레톤도 어디에 필요한지 판단하는 게 먼저입니다.

웹 성능 최적화에서 loading="lazy"는 분명 강력한 도구입니다. 하지만 이것이 이미지 파일 자체를 가볍게 만들어주는 건 아닙니다. 로딩을 미룰 뿐입니다. 파일 크기를 줄이는 포맷 최적화(WebP, AVIF 변환), 화면 크기에 맞는 이미지를 제공하는 srcsetsizes 속성, 그리고 Lazy Loading과 스켈레톤 UI가 함께 맞물릴 때 진짜 최적화가 완성됩니다. 한 줄의 속성보다 어디에, 무엇과 함께 쓰느냐가 결국 더 중요합니다.


참고:


소개 및 문의 · 개인정보처리방침 · 면책조항

© 2026 ⚡ 정보 부스터 🚀