이번에 개인 프로젝트를 새로 런칭하고 설레는 마음으로 크롬 개발자 도구의 Lighthouse 성능 검사를 돌려보았습니다.
결과는 참담하게도 성능 점수 62점, LCP(최초 대형 콘텐츠 도출) 속도 3.2초.
스마트폰 모바일 LTE 환경으로 접속해 보니 메인 페이지 배너 이미지가 위에서부터 턱-턱-턱 둔하게 렌더링되면서 아래쪽에 있던 텍스트들이 밑으로 밀려나는 현상(CLS)까지 발생하고 있었습니다.
"요즘 같은 세상에 로딩에 3초나 걸리는 웹사이트를 누가 기다려줄까?" 하는 위기감이 들었고, 원인을 분석해 보니 아무 생각 없이 넣어둔 고용량 메인 배너 이미지(PNG, 3.8MB)가 범인이었습니다.
오늘은 기본 HTML <img> 태그를 버리고 Next.js의 Image 컴포넌트(next/image)를 도입해 로딩 속도를 0.8초까지 끌어올렸던 생생한 최적화 과정을 공유해 봅니다.
1. 내가 범했던 실수: "원본 PNG 이미지를 그냥 태그에 꽂아 넣었다"
처음엔 그냥 디자인 도구에서 추출한 3.8MB짜리 고해상도 PNG 배너를 public/images 폴더에 넣고 아래처럼 코드를 짰었습니다.
// ❌ 처음 내가 작성했던 최적화 안 된 코드
export default function HeroBanner() {
return (
<div className="banner-container">
<img src="/images/main-banner.png" alt="메인 배너" />
</div>
);
}
이 코드가 왜 문제였냐면:
- 모바일 단말기에서도 3.8MB 원본을 통째로 다운로드받고 있었습니다.
- 이미지가 로딩되기 전까지 div 높이가 0px이었다가, 이미지 다운로드가 끝나는 순간 갑자기 400px로 커지며 아래 요소들을 강제로 밀어내는 불쾌한 현상이 일어났습니다.
- 브라우저가 PNG 확장자 그대로 다운로드하느라 용량이 전혀 줄어들지 않았습니다.
2. 구원투수: Next.js Image 컴포넌트로 리팩토링하기
Next.js 공식 문서를 뒤져가며 next/image 패키지를 적용해 코드를 고쳐나갔습니다.
import Image from 'next/image';
// ⭕ 내 경험을 바탕으로 수정한 최적화 코드
export default function HeroBanner() {
return (
<div className="relative w-full h-[400px] overflow-hidden rounded-2xl shadow-lg">
<Image
src="/images/main-banner.png"
alt="메인 프로모션 배너 이미지"
fill
priority // 1. LCP 핵심 이미지이므로 지연 로딩을 끄고 최우선 다운로드!
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
className="object-cover"
placeholder="blur" // 2. 이미지가 다 떠오르기 전 부드러운 블러 미리보기 적용
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU58EUg=="
/>
</div>
);
}
💡 직접 적용해보며 깨달은 꿀팁 2가지
- priority 속성의 중요성: 메인 페이지 상단에 바로 보이는 배너 이미지에는 반드시 priority를 붙여주어야 합니다. 이걸 안 붙이면 Next.js가 기본적으로 lazy loading(스크롤 내릴 때 로드)을 적용해버려서 오히려 초기 로딩이 더 늦어집니다.
- fill과 부모 relative 관계: 이미지 크기를 반응형으로 고정할 때 fill을 쓰면 편리한데, 이때 반드시 감싸고 있는 부모 div에 relative 포지션과 height 값이 지정되어 있어야 배너가 깨지지 않고 예쁘게 잡힙니다.

3. 외부 이미지(Unsplash/S3) 불러올 때 만난 에러와 해결
개발하다가 유저 프로필 사진을 외부 서버 URL로 불러왔을 때 Invalid src prop ... hostname is not configured under images 라는 빨간색 에러 화면을 만났습니다.
Next.js는 보안과 악성 이미지 서버 공격을 막기 위해 허용된 도메인의 외부 이미지만 가져오도록 제한하고 있었습니다. next.config.js 파일에 제가 사용하는 외부 이미지 도메인을 명시해 주니 깔끔하게 해결되었습니다.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.unsplash.com', // 사용 중인 외부 이미지 서버 도메인 추가
},
],
},
};
module.exports = nextConfig;
4. 최종 결과: 3.8MB ➔ 320KB, 성능 점수 94점 달성!
수정을 마치고 개발 서버를 다시 빌드한 뒤 크롬 개발자 도구 Network 탭을 열어보았습니다.
- 이미지 포맷 변화: 원본 .png 파일이 브라우저에서 자동으로 최신 압축 포맷인 .webp로 변환되어 들어오고 있었습니다.
- 용량 감축: 3.8MB ➔ 320KB (약 90% 용량 감소)
- Lighthouse 점수: 62점 ➔ 94점으로 폭등!

5. 회고 및 느낀 점
이전에는 "이미지는 그냥 img 태그 써서 띄우면 되는 거 아닌가?" 하고 안일하게 생각했었습니다.
하지만 이번에 직접 성능 수치를 측정해 보고 next/image를 도입해 리팩토링해 보면서, 작은 이미지 최적화 하나가 전체 서비스의 품질과 사용자 경험(UX)을 얼마나 크게 좌우하는지 뼈저리게 체감할 수 있었습니다.
앞으로 작성하는 모든 프로젝트에서는 개발 초기 단계부터 이미지 최적화 전략을 적용하면 좋을 것 같습니다!
'개발' 카테고리의 다른 글
| [React] useState와 useEffect 실무 적용 중 겪었던 3가지 초보적 실수와 해결책 (0) | 2026.07.28 |
|---|---|
| [Git] "내 코드가 어디 갔지?" 초보 개발자가 자주 범하는 Git 실수 3가지와 해결법 (0) | 2026.07.28 |
| [CSS] Tailwind CSS 도입 후기: BEM 클래스명 지옥 탈출과 가독성 극복 전략 (0) | 2026.07.28 |
| [Web] 웹 액세서빌러티(A11y): 모두를 위한 접근성 높은 인터페이스 구축 (1) | 2026.05.06 |
| [Future] AI 도구와 Wasm을 직접 써보며 느낀 프론트엔드 개발자의 미래와 고민 (0) | 2026.05.05 |