이전 업무에서 일부 페이지를 Next.js 기반으로 마이그레이션하면서, 기존에 사용하던 <img> 태그를 <Image /> 컴포넌트로 교체한 경험이 있었다.
Next.js는 이미지 처리 과정에서 기본적으로 몇 가지 최적화 기능을 제공한다.
마이그레이션 당시에는 이미지가 화면에서 언제 노출되는지를 기준으로 우선순위를 다르게 적용했다.
첫 화면에 바로 보이는 배너 이미지는 priority를 사용해 먼저 요청되도록 했고, 리스트 내부 썸네일은 기본 lazy loading 동작을 유지해 초기 렌더링 비용을 줄이고자 했다.
이번에는 Next.js가 제공하는 이미지 최적화 기능을 하나씩 정리하면서, 어떤 요소가 실제 체감 성능에 가장 큰 영향을 주는지도 함께 확인해보려고 한다.
총 4가지 경우를 두고, 같은 이미지를 렌더링이 어떻게 이뤄지고 있는지 확인해보았다.
앞으로 정리할 테스트는 아래 링크를 통해서 직접 볼 수 있다.
https://test-image-improvement.vercel.app/
<img/>
A 와 동일한 리소스를 활용하고 대신 img 태그의 loading 속성을 lazy 로 설정하여 Lazy Loading 되도록 설정한 후 다시 확인해보았다.
<img src="image.jpg" alt="..." loading="lazy" />
<iframe src="video-player.html" title="..." loading="lazy"></iframe>
초기에는 렌더링시에 바로 노출되는 이미지들만 로드한다. 이후 스크롤을 내려 하단에 위치한 이미지 리소스가 필요한 경우에 로드되는 것을 확인할 수 있다.
<Image /> 역시 기본적으로 lazy loading이 적용되며, viewport 밖 이미지는 즉시 요청되지 않는다.Next.js 의 <Image />는 원본 파일 자체를 직접 변경하는 방식이 아니라, 요청 시점에 최적화된 이미지를 별도 endpoint를 통해 전달하는 구조로 동작한다.
실제 Network 탭을 보면 원본 이미지 URL이 그대로 사용되지 않고, _next/image 경로를 통해 다시 요청되는 것을 확인할 수 있다.
w(width), q(quality) 같은 쿼리도 추가되어서 이미지 최적화가 적용된 것을 볼 수 있다.
priority가 적용된 이미지는 lazy loading이 비활성화되고 preload 대상으로 먼저 요청된다.<Image
src={...}
sizes="(max-width: 768px) 100vw, 33vw"
/>
이번 최적화 케이스들을 비교하면서 Next.js 공식 문서도 함께 살펴보았는데, 이번에 확인한 옵션 외에도 이미지 품질, 크기, placeholder 등 다양한 설정이 제공되고 있었다.
| 케이스 | 초기 요청 수 | 전송량 | LCP |
|---|---|---|---|
A. <img> eager | 24개 | 4,588 kB | 28.47s |
B. <img> lazy | 8개 | 1,827 kB | 13.06s |
C. <Image /> | 8개 | 1,198 kB | 8.44s |
D. <Image priority /> | 8개 | 1,198 kB | 11.33s |
E. <Image + sizes /> | 8개 | 378 kB | 3.3s |
가장 먼저 체감되는 차이는 lazy loading 여부에서 나타났고, 가장 큰 전송량 감소는 sizes 설정에서 확인할 수 있었다.
<img> 태그에서도 loading="lazy"만으로 초기 요청 수를 줄일 수 있었지만, <Image />는 여기에 포맷 변환과 _next/image 기반 최적화 경로까지 함께 제공한다는 점에서 차이가 있었다.
반면 priority는 첫 화면에서 반드시 빠르게 보여야 하는 이미지에는 유효했지만, 여러 이미지에 동시에 적용할 경우 기대한 만큼 성능이 개선되지 않을 수도 있었다.
특히 이번 비교에서는 sizes 없이 priority만 적용했을 때 LCP가 오히려 길어지는 결과도 확인할 수 있었다.
결국 <Image />는 단순히 태그를 교체하는 것보다, 어떤 이미지를 어떤 크기로 먼저 보여줄지까지 함께 설계해야 효과가 커지는 기능에 가까웠다.
특히 sizes를 함께 지정했을 때 responsive image 최적화 효과가 가장 분명하게 나타나는 것을 확인할 수 있었다.
물론 모든 이미지에 <Image />가 필요한 것은 아니다. 이미 충분히 작은 리소스나 SVG처럼 포맷 최적화 이득이 크지 않은 경우에는 기본 <img> 태그가 더 단순한 선택이 될 수 있다.
특히 반복적으로 사용하는 정적 이미지라면, 처음부터 WebP 같은 가벼운 포맷으로 준비해 <img> 태그로 직접 사용하는 편이 오히려 불필요한 최적화 단계를 줄이는 방법이 될 수 있다.
이번 내용을 정리하면서 lazy loading과 priority를 적용할 때의 판단이 크게 잘못되지 않았다는 점을 다시 확인할 수 있었다 😩
특히 Network 타임라인으로 리소스 요청 시점을 직접 비교해보니, 어떤 최적화가 실제로 먼저 체감되는지 더 직관적으로 이해할 수 있었다.
또한, <Image />를 사용하는 경우뿐 아니라, 기본 <img> 태그를 선택하는 기준도 함께 정리할 수 있었다.