next/image, 그냥 쓰면 안 되는 이유
Article
<img>를 next/image로 바꾸면 최적화가 끝난다고 생각했던 시절이 있다. 아니었다. Lighthouse를 돌려보니 LCP가 여전히 3초대였다. next/image는 도구지 마법이 아니다. 옵션을 제대로 줘야 일을 한다.
sizes 없는 fill은 낭비다
제일 흔한 실수. fill을 쓰면서 sizes를 안 주는 것.
// ❌ 뷰포트 100% 기준으로 큰 이미지를 받아온다
<Image src={thumb} alt="" fill />
// ✅ 실제 렌더 폭을 알려준다
<Image
src={thumb}
alt=""
fill
sizes="(max-width: 768px) 100vw, 33vw"
/>
sizes가 없으면 브라우저는 이미지가 뷰포트 전체 폭이라고 가정하고 srcset에서 큰 소스를 고른다. 3열 그리드 카드에서 실제로는 400px짜리가 필요한데 1200px짜리를 받아오는 식이다. 그리드 썸네일에 sizes 한 줄 넣었더니 이미지 전송량이 눈에 띄게 줄었다. fill을 쓰는 모든 곳에 sizes가 있는지부터 확인하자.
priority는 LCP 요소 하나에만
next/image는 기본이 lazy loading이다. 문제는 히어로 이미지처럼 첫 화면의 LCP 요소까지 lazy로 처리된다는 것.
// 첫 화면 히어로 — preload 대상으로 승격
<Image src={hero} alt="" priority />
priority를 주면 lazy가 풀리고 <link rel="preload">가 걸린다. 히어로에 이거 하나 넣은 게 이번 최적화에서 LCP를 가장 크게 깎았다. 3초대였던 LCP가 2초 밑으로 내려왔다.
주의할 점. priority를 남발하면 의미가 없다. preload가 여러 개 걸리면 서로 대역폭을 나눠 먹어서 정작 중요한 이미지가 늦어진다. 첫 화면에서 가장 큰 이미지 하나, 많아야 둘까지만.
placeholder로 체감을 챙긴다
로딩이 빨라져도 이미지가 "툭" 하고 나타나면 체감이 나쁘다. blur placeholder를 깔면 흐린 미리보기 → 선명한 이미지로 자연스럽게 전환된다.
<Image src={hero} alt="" priority placeholder="blur" />
정적 import 이미지는 저 한 줄이면 끝이다. 문제는 나처럼 외부 저장소에서 이미지를 가져오는 경우인데, 이때는 blurDataURL을 직접 만들어 넘겨야 한다. 나는 빌드 타임에 작은 base64 썸네일을 생성해서 넣는 방식을 택했다. 손이 좀 가지만 레이아웃이 잡혀 있는 상태에서 이미지가 차오르는 느낌이 확실히 다르다.
포맷은 설정 한 줄로
next.config.ts에서 avif를 켜두면 브라우저 지원에 따라 avif → webp 순으로 협상한다.
images: {
formats: ["image/avif", "image/webp"],
},
같은 품질에서 avif가 jpeg 대비 절반 이하까지 내려가는 경우가 많다. 변환 비용 때문에 빌드가 아주 약간 느려지지만 감수할 만하다.
정리
fill쓰는 곳엔 반드시sizes— 실제 렌더 폭을 알려줄 것priority는 첫 화면 LCP 이미지 하나에만placeholder="blur"로 로딩 체감 개선, 외부 이미지는 blurDataURL 직접 생성- 포맷은 config에서 avif/webp 활성화
컴포넌트 이름만 바꾸고 최적화됐다고 믿으면 안 된다. 결국 어떤 화면에서 어떤 크기로 쓰이는지 아는 사람이 옵션을 채워야 도구가 일을 한다.