지난번 쇼핑몰 프로젝트를 진행할 때의 일입니다. 테스트할 때 고화질 상품컷을 몇 십 장씩 업로드하면서 메인 페이지 접속 속도가 터무니없이 느려지는 현상이 발생했습니다.

크롬 개발자 도구를 켜서 네트워크(Network) 탭을 확인해 보니, 메인 페이지 전체 용량 8MB 중 무려 6.5MB가 고용량 PNG/JPEG 이미지였습니다. 메인 배너 이미지 하나 용량이 3MB에 달하다 보니, 모바일 환경에서는 화면이 뜨는 데 4~5초 이상 걸려 사용자 이탈률이 치솟고 있었죠.

그때 깨달았습니다. 수백 줄의 자바스크립트 로직을 줄이는 것보다 이미지 한 장의 용량을 3MB에서 100KB로 압축하는 것이 훨씬 더 파괴적인 성능 개선을 가져온다는 사실을요. 오늘은 제가 직접 프로젝트에 적용해 보고 효과를 크게 봤던 이미지 최적화 3대 전략을 공유해 봅니다.

1. 차세대 포맷(WebP, AVIF)으로 변환: 용량이 반토막 나는 마법

가장 먼저 한 작업은 수십 십 개의 JPEG, PNG 파일들을 차세대 포맷인 WebP와 AVIF로 변환하는 것이었습니다.

  • WebP: PNG처럼 투명도(Alpha)를 지원하면서도, JPEG 대비 용량이 25~35% 이상 감소합니다.
  • AVIF: 현존 최강의 압축률을 자랑하며, WebP보다도 20% 더 가볍습니다.

구형 브라우저 분기 처리를 위해 <picture> 태그를 활용해 코드를 변경했습니다.

HTML
 
<!-- 브라우저가 지원하는 최신 포맷을 알아서 골라 로드하도록 분기 처리 -->
<picture>
  <source srcset="banner.avif" type="image/avif" />
  <source srcset="banner.webp" type="image/webp" />
  <img src="banner.jpg" alt="메인 기획전 배너" />
</picture>

이렇게 포맷만 바꿔줬는데도 이미지 전체 용량이 6.5MB에서 1.8MB로 대폭 줄어드는 것을 눈으로 확인했습니다.

squoosh.app을 이용해 이미지 용량 대폭 감소

2. 지연 로딩(Lazy Loading)과 나의 실수: LCP 이미지는 건드리지 마라!

화면을 아래로 스크롤하기 전까지는 보이지도 않는 하단 상품 이미지들까지 한 번에 로드할 필요가 전혀 없었습니다. 그래서 HTML5 표준 속성인 loading="lazy"를 도입했습니다.

HTML
 
<!-- 화면 뷰포트에 가까워지면 그때 다운로드를 시작합니다 -->
<img src="product-detail.webp" loading="lazy" alt="상품 상세 설명" />

🚨 내가 겪었던 삽질: 메인 배너에 lazy를 걸었다가 점수가 깎이다?

여기서 욕심을 내서 메인 최상단 배너 이미지에도 loading="lazy"를 적용했었는데, 오히려 구글 Lighthouse의 LCP(Largest Contentful Paint - 가장 큰 콘텐츠가 뜨는 시간) 점수가 더 떨어지는 기현상이 일어났습니다.

알고 보니 첫 화면에 바로 보여야 하는 핵심 이미지에는 절대 lazy를 걸면 안 되는 것이었습니다. 브라우저가 로딩을 오히려 미루기 때문이죠. 상단 배너에는 loading="eager"와 함께 fetchpriority="high" 속성을 주어 우선순위를 높여주는 것이 정답이었습니다.

3. responsive images (srcset): 모바일 사용자 데이터 아껴주기

데스크톱용 1920px 너비의 이미지를 모바일 사용자(너비 390px)에게 전송하는 건 명백한 데이터 낭비였습니다. 기기 해상도에 맞춰 최적화된 크기를 전달하기 위해 srcset 속성을 적용했습니다.

HTML
 
<img
  src="banner-small.webp"
  srcset="banner-small.webp 500w, banner-medium.webp 1000w, banner-large.webp 2000w"
  sizes="(max-width: 600px) 480px, 800px"
  alt="메인 배너"
/>

이렇게 설정해 두니 브라우저가 알아서 현재 모바일 기기인지, 데스크톱인지 파악하여 가장 딱 맞는 크기의 이미지만 쏙 골라서 다운로드하더군요.

Lighthouse Performance 점수

4. 실무 이미지 최적화 체크리스트

프로젝트를 진행하며 정리한 저만의 최적화 루틴입니다.

  1. 상세 페이지 이미지 용량 제어: 웹용 이미지는 무조건 장당 200KB 이하로 맞추기
  2. Layout Shift(CLS) 방지: <img> 태그에 width와 height 속성을 명시해서 이미지가 늦게 떠도 화면이 덜컹거리지 않게 영역 미리 잡기
  3. EXIF 메타데이터 제거: 카메라 촬영 정보, 위치 정보 등 불필요한 메타데이터는 압축 툴로 싹 지워주기

5. 글을 마치며

이미지 최적화는 단순히 기술적인 도전을 넘어, 우리 서비스를 이용하는 사용자의 데이터와 시간을 아껴주는 작업이었습니다.

실제로 이미지 다이어트 작업을 마친 뒤 메인 페이지 체감 로딩 속도가 3초 이상 단축되었고, 팀 내부에서도 만족도가 매우 높았습니다. 만약 지금 운영 중인 사이트나 블로그가 답답하게 느껴진다면, 자바스크립트 코드를 건드리기 전에 먼저 이미지들부터 가볍게 다이어트시켜 보시는 걸 강력히 추천합니다!

웹사이트의 첫인상을 결정짓는 가장 중요한 요소 중 하나는 '로딩 속도'입니다. 구글은 이를 정량적으로 측정하기 위해 Core Web Vitals라는 지표를 도입했으며, 그중에서도 가장 큰 비중을 차지하는 것이 바로 LCP(Largest Contentful Paint)입니다.

LCP는 페이지에서 가장 큰 콘텐츠(보통 히어로 이미지나 큰 텍스트 블록)가 화면에 렌더링 되는 시간을 의미합니다. 놀랍게도 대부분의 웹사이트에서 LCP 점수를 갉아먹는 주범은 바로 '최적화되지 않은 이미지'입니다. 오늘은 Next.js가 제공하는 강력한 도구인 next/image를 활용하여 LCP 점수를 획기적으로 개선하는 전략을 심층 분석해 보겠습니다.


1. 왜 일반 <img> 태그보다 next/image인가?

전통적인 <img> 태그는 단순히 이미지를 불러올 뿐, 성능 최적화에 대한 어떠한 책임도 지지 않습니다. 반면 Next.js의 Image 컴포넌트는 다음과 같은 작업을 자동으로 수행합니다.

  1. 이미지 포맷 최적화: 브라우저가 지원한다면 WebP나 AVIF 같이 용량이 훨씬 작은 최신 포맷으로 자동 변환합니다.
  2. 리사이징 (Resizing): 디바이스 크기에 맞춰 최적화된 크기의 이미지를 생성하여 전송합니다. 4K 이미지를 모바일 사용자에게 전송하는 낭비를 막아줍니다.
  3. Lazy Loading: 화면에 보이지 않는 이미지는 로딩을 뒤로 미뤄 초기 로딩 속도를 높입니다.
  4. Placeholder 제공: 이미지가 로드되기 전 저해상도 이미지나 블러(Blur) 효과를 보여주어 사용자 경험을 개선합니다.

2. LCP 개선의 핵심: priority 속성

LCP 점수를 높이는 가장 쉽고 강력한 방법은 '가장 중요한 이미지'를 먼저 로드하는 것입니다.

보통 페이지 상단의 히어로 이미지나 배너가 LCP 요소가 됩니다. next/image는 기본적으로 레이지 로딩(Lazy Loading)이 적용되는데, LCP 요소에 레이지 로딩이 걸려 있으면 오히려 렌더링이 늦어져 LCP 점수가 나빠집니다.

JavaScript
 
// LCP 대상이 되는 히어로 이미지 예시
import Image from 'next/image';

export default function Hero() {
  return (
    <section>
      <Image
        src="/hero-banner.png"
        alt="메인 배너 이미지"
        width={1200}
        height={600}
        priority // 이 속성이 LCP 개선의 핵심입니다!
        className="object-cover"
      />
    </section>
  );
}

priority 속성을 추가하면 Next.js는 해당 이미지를 높은 우선순위(Fetch Priority)로 처리하고 프리로드(Preload) 태그를 삽입합니다. 이는 브라우저가 HTML을 파싱 할 때 해당 이미지를 가장 먼저 다운로드하도록 유도하여 LCP 시간을 단축시킵니다.


3. 레이아웃 시프트 방지와 sizes 속성 활용

이미지 로딩 중 화면이 덜컥거리는 현상인 CLS(Cumulative Layout Shift) 역시 성능 점수에 큰 영향을 줍니다. 이를 방지하려면 이미지의 비율을 미리 브라우저에 알려줘야 합니다.

3.1 정적 이미지와 고정 비율

width와 height를 명시하면 Next.js는 이미지 비율에 맞춰 공간을 미리 확보합니다. 만약 반응형 이미지를 구현해야 한다면 sizes 속성을 반드시 사용해야 합니다.

3.2 sizes 속성 최적화

sizes는 브라우저에게 "이 이미지는 뷰포트 크기에 따라 이 정도 너비를 차지할 거야"라고 미리 알려주는 힌트입니다.

JavaScript
 
<Image
  src="/product.jpg"
  alt="상품 이미지"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

위 설정은 모바일에서는 화면 꽉 차게(100vw), 태블릿에서는 절반(50vw), 데스크탑에서는 1/3(33vw) 크기의 이미지만 요청하도록 만듭니다. 불필요하게 큰 이미지를 다운로드하지 않게 되어 로딩 속도가 비약적으로 향상됩니다.


4. 이미지 포맷과 퀄리티 조정 (next.config.js)

더 극단적인 최적화를 원한다면 설정 파일을 통해 이미지 포맷을 제어할 수 있습니다. WebP보다 압축률이 높은 AVIF를 지원하도록 설정해 보세요.

JavaScript
 
// next.config.js
module.exports = {
  images: {
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
  },
};

또한, 기본 퀄리티(75)를 프로젝트 성격에 맞춰 조정하여 용량과 화질 사이의 균형을 맞출 수 있습니다.


5. 실무 트러블슈팅: 외부 도메인 이미지 처리

많은 실무 프로젝트(예: CMS, 쇼핑몰 가비아 이미지 서버 등)에서 외부 URL의 이미지를 가져옵니다. 이때 next/image를 쓰려면 반드시 허용된 도메인 설정을 거쳐야 합니다.

JavaScript
 
// next.config.js
images: {
  remotePatterns: [
    {
      protocol: 'https',
      hostname: 'your-cdn-server.com',
      pathname: '/images/**',
    },
  ],
},

이 설정을 통해 외부 서버의 이미지도 Next.js 서버가 대신 최적화(Optimization API)하여 사용자에게 전달할 수 있게 됩니다.

+ Recent posts