웹 사이트를 만들 때 보통 디자인 도구(Figma 등)에서 만든 이미지들을 그냥 PNG나 JPEG로 뽑아서 아무 생각 없이 프로젝트에 넣곤 했습니다.

그러다 최근 진행했던 웹 서비스에서 큰 문제를 하나 만났습니다. 메인 화면에 들어가는 고화질 배너 이미지 몇 개를 PNG 파일 그대로 넣었더니, 이미지 파일 몇 개 용량만 합쳐도 5MB가 훌쩍 넘어가는 참사가 일어난 것입니다.

모바일로 접속했을 때 이미지가 위에서부터 천천히 스캔되듯이 지저분하게 내려오면서 로딩되는 모습을 보고 "아, 이미지 포맷부터 당장 바꿔야겠다"는 결심을 하게 되었습니다.

오늘은 구시대 포맷인 PNG/JPEG를 버리고 차세대 포맷인 WebP와 AVIF로 전환하면서 겪었던 과정과, 실무에서 브라우저 깨짐 없이 안전하게 적용했던 노하우를 공유해 봅니다.

1. 내가 부딪힌 문제: "화질은 포기하기 싫고, 용량은 너무 크다"

프로젝트 배너에 글씨와 그래픽이 섞여 있다 보니, 화질이 깨지는 JPEG 대신 깨끗하게 나오는 PNG 포맷을 사용했습니다. 하지만 PNG는 무손실 압축 방식이라 파일 용량이 어마무시하게 컸습니다.

  • 메인 배너 PNG 파일 1개 용량: 무려 2.4 MB
  • 문제점: 접속할 때마다 유저의 데이터와 시간을 엄청나게 갉아먹고 있었고, 네트워크가 조금만 느려도 화면이 텅 비어 보이는 현상 발생.

화질을 떨어뜨리지 않으면서 용량만 획기적으로 줄일 수 있는 방법을 찾다가 구글이 만든 WebP와 차세대 비디오 코덱 기반의 AVIF 포맷을 알게 되었습니다.

2. 해결책 1: HTML <picture> 태그로 구형 브라우저 호환성 지키기

WebP나 AVIF가 아무리 용량이 작고 좋아도, "혹시 최신 포맷을 지원하지 않는 구형 브라우저에서는 이미지가 엑박(X)으로 깨지면 어쩌지?" 하는 걱정이 들었습니다.

이 문제를 구원해 준 것이 바로 HTML5의 <picture> 태그였습니다. 브라우저에게 "네가 AVIF를 지원하면 AVIF를 보여주고, 안 되면 WebP를, 그것도 안 되면 기존 JPEG를 보여줘" 하고 우선순위를 정해주는 방식입니다.

HTML
 
<!-- ⭕ 내 프로젝트에 직접 적용한 크로스 브라우징 picture 태그 -->
<picture>
  <!-- 1. 가장 가볍고 최신인 AVIF 포맷을 최우선으로 시도 -->
  <source srcset="/images/main-banner.avif" type="image/avif" />
  
  <!-- 2. AVIF 미지원 시 WebP 포맷 사용 -->
  <source srcset="/images/main-banner.webp" type="image/webp" />
  
  <!-- 3. 최신 포맷을 둘 다 지원하지 않는 구형 브라우저용 기본 이미지 -->
  <img 
    src="/images/main-banner.jpg" 
    alt="메인 프로모션 배너" 
    width="1200" 
    height="600"
    loading="lazy"
  />
</picture>

이렇게 코드를 짜두니 에러 걱정 없이 마음 편하게 최신 이미지 포맷을 도입할 수 있었습니다.

<picture>, <source>, <img> 구조 예시

3. 해결책 2: Node.js(Sharp)로 자동 변환 스크립트 만들어 버리기

이미지가 한두 개가 아니다 보니 일일이 변환 사이트에 업로드해서 다운로드하는 작업이 너무 귀찮았습니다. 그래서 Node.js의 이미지 처리 라이브러리인 sharp를 활용해, 명령어 하나만 치면 프로젝트 내 모든 PNG 파일이 WebP와 AVIF로 자동 변환되는 스크립트를 직접 짜서 해결했습니다.

JavaScript
 
// convert.js (내가 직접 만든 이미지 자동 변환 스크립트)
const sharp = require('sharp');
const path = require('path');

const inputPath = path.join(__dirname, 'public/images/main-banner.png');

// PNG를 WebP로 자동 변환 (품질 80% 설정)
sharp(inputPath)
  .webp({ quality: 80 })
  .toFile(path.join(__dirname, 'public/images/main-banner.webp'))
  .then(() => console.log('✅ WebP 변환 성공!'));

// PNG를 AVIF로 자동 변환 (품질 75% 설정)
sharp(inputPath)
  .avif({ quality: 75 })
  .toFile(path.join(__dirname, 'public/images/main-banner.avif'))
  .then(() => console.log('🚀 AVIF 변환 성공!'));

터미널에서 node convert.js만 실행하면 몇 초 만에 고용량 PNG가 초경량 파일들로 쓱 변환되어 나와서 개발 생산성이 엄청나게 올라갔습니다.

4. 직접 눈으로 확인한 용량 비교: "2.4MB가 210KB가 되는 기적"

변환을 마치고 폴더 안에 있는 동일한 이미지의 포맷별 파일 용량을 직접 확인해 보았습니다.

  • 기존 원본 PNG: 2,400 KB (2.4MB)
  • WebP 변환 후: 420 KB (약 82% 용량 감소!)
  • AVIF 변환 후: 210 KB (약 91% 용량 감소!)

화질은 눈으로 봤을 때 원본 PNG와 차이를 구분할 수 없을 정도로 선명한데, 파일 용량은 무려 10분의 1 수준으로 줄어들었습니다. 그 결과 메인 페이지에 진입할 때 이미지가 버벅거리지 않고 즉시 뜨는 쾌적한 환경을 만들 수 있었습니다.

Node.js 환경에서 sharp 라이브러리로 PNG 이미지를 WebP와 AVIF로 자동 변환해 주는 스크립트 코드

 

5. 깨달은 점 및 요약

이전에는 프론트엔드 성능 최적화라고 하면 무조건 복잡한 코드 리팩토링이나 상태 관리 최적화만 생각했었습니다.

하지만 이번 경험을 통해 이미지 포맷을 WebP나 AVIF로 바꾸는 것 같은 단순한 시도가 웹사이트 성능에 얼마나 파격적인 영향을 주는지 깨달았습니다.

혹시 지금 만들고 계신 프로젝트의 로딩 속도가 답답하다면, 소스 코드보다 먼저 '내가 넣은 이미지 파일의 포맷과 용량'부터 점검해 보시면 좋을 것 같습니다!

웹사이트의 첫인상을 결정짓는 가장 중요한 요소 중 하나는 '로딩 속도'입니다. 구글은 이를 정량적으로 측정하기 위해 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