프론트엔드에서 굉장히 중요한 작업 중 하나가 UX 최적화일 것이다.
UX 최적화를 판단할 때 사용자들이 서비스와 얼마나 빠르게 인터렉션 할 수 있는지가 매우 중요한 요소이다.
우선 내가 하고있는 프로젝트의
위 장소 상세 페이지 초기 성능을 Lighthouse로 측정해보았다.
여러번의 측정 결과 대략 68~72점으로 좀 아쉬운 점수이다.
jpeg 포맷의 이미지 2개를 받아오는데 드는 용량은 약 1MB로 상당히 큰 것을 확인할 수 있었다.
해당 페이지에서는 큰 이미지 2개를 슬라이더 형식으로 띄워주고 있으므로, 이미지 관련 Optimization만 잘 해줘도 성능을 좀 더 올려줄 수 있을 것 같다.
img 태그 대신 Next/Image
흔히 이미지를 UI에 띄우기 위해 img 태그를 사용하곤 한다.
하지만 Next에서는 img 태그보다 훨씬 더 많은 장점을 가진 Next/image를 제공해준다.
lazy loading
Next/Image를 사용하면 자동으로 이미지들이 lazy loading 처리 된다.
즉, 당장 필요한 이미지들 먼저 불러오게 되고 그렇지 않으면 해당 이미지들이 필요한 시점에서야 로딩된다.
당장 필요없는 이미지들까지 전부 다 불러오게 되면 그만큼 성능이 저하될 수 밖에 없으므로 lazy loading은 이미지를 처리할 때 매우 중요한 기술이다.
다만 모든 이미지들을 무조건 lazy loading 처리해야 좋은 것만은 아니다.
예를 들어, 배너같이 페이지에 크게 하나 띄워야 하는 이미지를 생각해보면 무조건 빨리 불러오는 것이 이득일 것이다.
이와 같은 상황을 위해 priority 속성을 추가해주면 lazy loading을 막아줄 수도 있다.
lazy loading을 적용할 상황과, LCP(Largest Contentful Paint)요소처럼 적용하지 않을 상황을 잘 구분하면 성능을 올릴 수 있을 것이다.
이미지 사이즈 최적화
Next/Image를 사용하면 jpeg 등과 같은 포맷 대신 webp라는 용량이 작은 포맷으로 이미지를 서빙할 수 있다.
또한 srcSet을 미리 지정해둔 후, 사용자의 디바이스 크기에 가장 알맞는 크기의 이미지를 다운로드 할 수 있도록 도와준다. 이에 대해서는 뒤에 더 자세히 언급할 예정이다.
이러한 디바이스에 맞는 크기의 이미지를 만들고, webp 포맷으로 변환하는 과정은 어떤 이미지의 최초 요청 시 Next 서버에서 진행된다고 한다.
최초 요청 이후 같은 이미지에 대해 다시 요청이 오면, 캐시가 만료될 때 까지 캐시된 이미지를 사용하여 매우 빠르게 이미지를 서빙할 수 있다.
위는 최초 요청 이후 이미지에 대한 네트워크 요청 결과이다.
요청 상태가 200이 아닌 304인 것을 확인할 수 있고, 이를 통해 캐시된 자원을 사용했구나라고 알 수 있다.
304 Not Modified 요청에 관한 내용은 https://doyourbestcode.tistory.com/128 에서 자세히 언급한 바 있다.
placeholder 제공(CLS 방지)
Next/Image는 placeholder를 제공하여 CLS(Cumulative Layout Shift)를 방지해준다.
이미지가 로드되기 이전에는 해당 이미지가 들어갈 공간의 높이가 0일 것이고, 로드가 완료되면 갑자기 이미지만큼 영역이 늘 것이다.
placeholder를 사용하면 이미지가 로드되기 전에도 해당 이미지의 크기만큼 영역을 미리 잡아두어 레이아웃이 흔들리지 않게, 즉 CLS가 발생하지 않도록 도와준다.
<Image
...
placeholder="blur"
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFklEQVR42mN8//HLfwYiAOOoQvoqBABbWyZJf74GZgAAAABJRU5ErkJggg=="
...
/>
예를 들어, 리모트 이미지의 placeholder 속성을 "blur"로 지정해주고, blurDataURL를 base64로 인코딩된 data url로 지정해주면
위와 같이 이미지가 로드 되기 전에도 내가 설정한 blurDataURL의 회색으로 레이아웃이 채워져 있는 것을 확인할 수 있다.
위처럼 많은 장점을 가지고 있는 Next/Image를 도입한 후 성능 점수를 다시 측정해보았다.
Next/Image를 도입해 준 것 만으로 성능 점수가 평균 89~93 점으로 20점 이상이 올라가는 드라미틱한 변화를 얻을 수 있었다. 그냥 육안으로 봐도 훨씬 빨랐다.
webp 포맷으로 이미지 2개를 받아오는데 드는 용량은 총 약 100kB로 img 태그를 사용했을 때 보다 1/10 가량 줄일 수 있었다.
불러오는 속도도 매우 빨라진 것을 확인할 수 있다.
img 태그의 srcSet에 포함되는 url 개수(layout="fill")
반응형 작업을 위해 Next/Image 컴포넌트를 relative position을 가진 부모로 묶은 뒤 layout="fill" 속성을 지정하게 되면 고려해야 할 상황이 많아진다.
그 중 특히 sizes 속성에 관해 잘 살펴보아야 한다.
<Image
...
fill
sizes="100vw"
placeholder="blur"
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFklEQVR42mN8//HLfwYiAOOoQvoqBABbWyZJf74GZgAAAABJRU5ErkJggg=="
...
/>
만약 fill 속성을 지정한 후 sizes를 설정하지 않으면 디폴트로 "100vw"로 설정되지만, 설정을 안해주면 콘솔에 에러가 뜨니 웬만하면 지정해주자.
위와 같이 한 후 한번 해당 이미지와 관련된 HTML 내용을 봐보자.
분명히 하나의 이미지인데 img 태그의 srcSet에 포함된 url이 매우 많은 것을 확인할 수 있다.
왜 이런 것일까?
위에서 Next는 이미지를 사용자 디바이스 크기에 맞게 알맞은 크기로 제공한다고 했다.
이 때 크기를 무엇을 기준으로 정하는지를 알아봐야 위의 srcSet과 관련한 의문점을 해결할 수 있다.
module.exports = {
images: {
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
};
Next는 deviceSizes와 imageSizes라는 배열을 가지고 이미지의 크기를 결정한다.
next.config.js 파일에 아무것도 지정해주지 않으면 사실 디폴트로 위처럼 세팅해놓은 것과 마찬가지이다.
위의 정보는 사용자가 임의로 변경할 수 있으며, 이 정보들을 가지고 Next는 사용자의 디바이스에 가장 알맞는 크기의 이미지를 가공 후 제공해준다.
sizes와 관련된 최적화에 관한 정보를 제공하는 블로그 중 정확한 정보를 찾기가 쉽지 않아 하나하나 테스트를 해가며 여러가지 의문점들을 해결해 나가야 했다.
sizes를 vw단위로 제공할 경우
예를 들어 위와 같이 sizes를 "100vw" 등 vw 단위로 지정했다고 가정하자.
위 img 태그의 srcSet을 잘 살펴보면 이미지 주소들 뒤에 &w=640 ~ &w=3840를 가지고 있는 url이 포함되어 있는 것을 확인할 수 있다.
이를 자세히 보면 deviceSizes의 디폴트 배열안에 있는 값들을 이용하여 url을 생성해 낸 것을 확인할 수 있다.
결론: sizes를 vw 단위로 지정한 경우, deviceSizes 배열안에 있는 값들을 width로 하는 url을 전부 srcSet에 포함시킨다(디폴트 설정이라면 8개의 url이 생성됨).
sizes를 px단위로 제공할 경우
위에서 sizes="100vw"를 sizes="500px" 과 같은 px 단위로 지정한 후 srcSet을 봐보았다.
위에서보다 훨씬 많은 url이 srcSet에 포함되어 있는 것을 확인할 수 있다.
자세히 살펴보니 deviceSizes 배열 안의 값 뿐만 아니라 imageSizes 배열 안의 값들을 width로 갖는 url도 생성된 것을 확인할 수 있다.
결론: sizes를 px 단위로 지정한 경우, deviceSizes 배열안에 있는 값들 뿐만 아니라 imageSizes 배열안의 있는 값들도 width로 하는 url을 전부 srcSet에 포함시킨다(디폴트 설정이라면 16개의 url이 생성됨).
이렇게 디폴트로 적용되어 신경도 못쓰고 있었던 부분에서 HTML inflation으로 인해 상당한 자원 낭비 및 성능 저하가 발생할 수 있다.
Next 서버에서 사이즈, 포맷 등이 최적화된 이미지 파일들을 생성하게 되고, 이 때문에 최초로 페이지를 로드하게 되는 사용자는 Next 서버에서 이미지 생성이 완료될 때까지 기다려야 하기 때문에 오랜 시간 대기해야 할 수도 있기 때문이다.
deviceSizes와 imageSizes 배열의 값들이 지나치게 많다고 생각되면 변경해줄 수 있다.
deviceSizes: [640, 750, 828],
imageSizes: [128, 384],
어느정도로 세분화 하여 지정해줘야 할까에는 정답이 없다고 봐야할 것 같다.
나는 우선 모바일만을 고려한 서비스이므로 deviceSizes에서 너무 큰 값들은 지워주었다.
deviceSizes 보다 더 작은 크기를 위한 imageSizes 배열도 내 생각에 알맞게 지정해주었다.
위와 같이 지정한 후 다시 sizes="100vw"의 결과를 봐보자.
디폴트 값이 적용되었을 때 8개의 url이 생성된 이전과 달리, srcSet에 3개의 url만 생성된 것을 확인할 수 있다.
지금처럼 페이지에 사용하는 이미지의 개수가 좀 적다면 효과가 미비하겠지만, 이미지가 많을 수록 srcSet에 생성되는 url 개수를 조절해주는 것이 성능에 더 큰 영향을 미칠 것 같다.
픽셀의 CEO인 Alex Barashkov에 따르면 layout="fill"일 때 srcSet을 4개정도 생성하는 것이 가장 이상적인 것 같다고 말한다.
내려받는 이미지 사이즈에 대한 오해
나는 장소 상세 페이지에서 가로 화면에 꽉차는 사이즈의 이미지를 사용하고 있다.
그럼 실제로 사용되고 있는 이미지의 사이즈를 확인해보자.
로컬 작업을 width 400px 환경에서 하고 있으므로 렌더링된 크기의 width는 400px 이지만 현재 소스는 w=640으로 width가 640px인 것을 알 수 있다.(deviceSizes 배열 값 중 하나)
이렇게 실제 렌더링 되는 크기에 비해 실제 사용되는 이미지가 너무 크다면 불필요한 성능 저하를 낳을 수 있다.
한번 로컬 작업 환경을 width 500px로 바꿔보았다.
sizes="100vw"를 설정하면 500px과 가장 가까운 deviceSizes의 640px이 width가 되어야 한다고 생각했지만 width가 750px인 이미지가 사용되고 있었다.
일단 확실한 점은 Next가 사용자 디바이스에 가장 알맞은 이미지의 크기로 바꿔줄 때 절대 설정한 sizes보다 작은 이미지를 내려주지는 않는다는 것이다.
또한 정확한 기준은 잘 모르겠으나, 위처럼 sizes에 설정된 값 보다 더 큰 값들 중에서 가장 작은 값이 사용되는 것도 아니였다.
즉, sizes에 설정된 값 보다 더 큰 값들 중에서 무조건 가장 작은 값이 아니라 Next가 판단하였을 때 가장 알맞는 이미지를 내려주는 것 같다.
그럼 최대한 작게 하면 좋은거 아닌가?? 한번 sizes="5vw"를 지정하여 테스트 해보았다.
우선 사이즈가 매우 작아졌으므로 deviceSizes가 아닌 imageSizes배열 중 96을 width로 하는 이미지가 내려졌다. 그럼 결과를 봐보자.
렌더링 되는 크기보다 강제로 사진을 줄이니 흐려지고, 오히려 이미지가 나오는데도 더 오래걸렸다.
렌더링 되는 사진 크기보다 너무 작아지면 저런 결과가 나온다.
저런 결과를 원하는 개발자는 없을 것이다. 역시 과유불급인거 같다.
나는 responsive 하게 이미지가 무조건 디바이스에 꽉차게 나와야 했으므로 sizes="100vw"를 유지해주었다.
필요한 경우, priority로 lazy-loading 방지
위 같은 조언이 떴다.
즉, 스크롤 없이 가장 첫 이미지는 바로 보여야 하는데 Next/Image는 디폴트로 lazy-loading 처리 되기 때문에 오히려 최대 콘텐츠 렌더링 시간이 늘어날 수 있다는 경고였다.
<Slider {...sliderSettings}>
{placeImages.map((image, i) => (
<div
key={image + i}
className="w-[100%] h-[30rem] mb-[1.5rem] relative"
>
<Image
src={image}
alt="공간 상세 사진"
fill
sizes="100vw"
priority={i === 0}
placeholder="blur"
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFklEQVR42mN8//HLfwYiAOOoQvoqBABbWyZJf74GZgAAAABJRU5ErkJggg=="
/>
</div>
))}
</Slider>
최종적으로 위처럼 priority를 첫번째 이미지만 true로 설정하여 첫번째 이미지에 대한 lazy-loading을 방지해주었다.
해당 설정 후 lighthouse 재검사 결과 위 경고는 더이상 보이지 않았다.
해당 설정을 다 해준 후 가장 좋게 나온 성능 결과이며, 여러번 테스트를 진행한 결과 매번 비슷한 결과를 얻을 수 있었다.
Next 프로젝트를 진행하고 있다면 Next/Image는 안써서는 안될 최고의 툴이며, 더 섬세하게 세팅을 해주면 더욱 더 좋은 서비스를 만들어낼 수 있을 것 같다.
폰트 최적화 및 번들 최적화에 관한 내용은 바로 다음 블로그에서...
참고
https://pixelpoint.io/blog/next-image/
Things you might not know about Next Image — Pixel Point
Explore Next.js Image component's architecture and functionality, dispel common misconceptions, and master best practices for optimization to maximize its performance impact.
pixelpoint.io
https://fe-developers.kakaoent.com/2022/220714-next-image/
Next/Image를 활용한 이미지 최적화 | 카카오엔터테인먼트 FE 기술블로그
조지영(esme) 무언갈 빠르게 좋아합니다. 그래서 변화가 빠른 FE 개발이 적성에 잘 맞습니다.
fe-developers.kakaoent.com
https://nextjs.org/docs/pages/building-your-application/optimizing/images
Optimizing: Images | Next.js
Optimize your images with the built-in `next/image` component.
nextjs.org
'FrontEnd > Next' 카테고리의 다른 글
Next 성능 개선 일지 2 (Feat: FOUT, bundle-analyzer, Link prefetch) (0) | 2024.05.04 |
---|---|
Next 에서 소셜 로그인 구현하기(Feat : Cookie) (4) | 2024.03.14 |
Next 에서 데이터 관리하기 (Feat : Cache) (2) | 2024.03.09 |
Next + TailwindCSS 세팅하기 (2) | 2023.12.28 |