
최근에 최적화에 관심을 가지게 되면서 공부를 하던 중에 이미지가 웹 사이트 성능을 크게 좌지우지된다고 듣게 되었다.
빌드된 상태로는 사용하는데 문제가 하나도 없었기에 아직 내 프로젝트에는 최적화를 할 때가 아닌가보다 생각했다.
하지만 모바일 버전에서는? 라는 생각과 함께 모바일 용으로 lighthouse를 돌려보니 낮은 수치의 LCP와 큰 용량의 이미지를 확인했다.

이미지 최적화를 통해 웹사이트 성능을 향상 시킬 목적으로 공부하고 고민한 내용을 잊지않도록 기록하려고 한다.
이미지 확장자는 크게 PNG , JPG/JPEG / Webp / AVIF 이렇게 4가지가 있다. 각자의 특징을 정의 해 본다면

상황 별 이미지 확장자 선택하기
내가 중요한 사진을 올릴꺼라 화질이 중요한 사진이 필요하다 ⇒ PNG
내가 서브사진을 올릴꺼라 화질이 엄청 중요하지않다 ⇒ JPG / JPEG
따라서 용량이 중요하다면 JPG 화질이 중요하다면 PNG라고 설명이 가능한데 둘다 잡을려면 WEBP 또는 AVIF를 쓰면된다.
하지만 호환성에 따라 제한이 있기때문에 호환이 되지않는 브라우저에서는 강제로 PNG 또는 JPG를 써야하는데 최대한 WEBP와 AVIF를 쓰고싶다.
그래서 VSC에서는 Picture 태그와 Source 태그를 제공한다.
Picture 태그안에 Source태그를 같이 넣고 코드를 짜면 가장 위에있는 것부터 호환성을 확인한 후에 호환된다면 그 한개의 이미지만 쓸 수 있다.
이미지 지연로딩때문에 source안에 이미지 경로도 data-set에 넣어야하는것도 주의하자 !
Squoosh는 구글에서 만든 이미지 컨버터 웹 애플리케이션이다.
https://squoosh.app/

squoosh라는 사이트에서 확장자를 변환할 수 있다. 위의 사진을 기준으로 얘기한다면 png인 이미지가 webp로 확장자를 바꾸자 94% 이미지 용량이 줄어든 것을 볼 수 있다. 이렇게 확장자만 바꿔도 의미있는 결과가 나오는걸 볼 수 있다.
확장자 다음 중요한 부분은 Quality이다.
퀄리티는 이미지의 화질을 의미한다. 기본값은 75%인데 75%인 이유는 100%나 75%나 사람이 시각적으로 변화를 느끼지 못하기때문이다.
만약 75%이하가 된다면 사용자가 불편함을 느낄 수 있으니 주의하자.
확자자명을 쓰는 이유는 운영체제가 확장자 명에 대한 올바른 처리를 하기 위함이다.
따라서, 본래 확장자명을 수동으로 바꾸게 된다면 운영체제가 파일을 해석하는데 예상치 못한 오류가 날 수 있다.
예를 들자면 수동으로 .hwp를 .pdf로 바꾸면 파일을 열었을 때, 이상하게 나온다.
따라서, 변환해주는 프로그램이나 서비스를 활용해서 바꿔줘야 정상적으로 바뀐다.
squoosh를 이용한 이미지 최적화는 이미지들이 만약 "큰 돌들" 이라면 큰 돌을 작게 만든 과정이고 lazyLoading을 이용한 최적화는 큰 돌들을 천천히 한개씩 보여주는 과정이다.
우리의 웹사이트를 사용자가 봤을 때 스크롤이 있다면 모든 이미지들을 한 화면에 다 담겨있지 않을 것이다.
그렇다면 아직 보이지않는 요소들을 사용자가 스크롤을 내리면서 이미지를 마주칠 때 그때 로딩하면 어떨까? 라는 생각으로 lazyLoading이 만들어졌다.
import React, { useEffect, useState } from 'react';
const UseLazyLoading = (
ref: React.RefObject<HTMLImageElement | HTMLElement> | null,
) => {
const [isView, setIsView] = useState(false);
useEffect(() => {
if (ref?.current) {
const observer = new IntersectionObserver(
(entries: IntersectionObserverEntry[]) => {
entries.forEach((entry: IntersectionObserverEntry) => {
if (entry.isIntersecting) {
setIsView(true);
} else if (ref.current?.tagName === 'ARTICLE') {
setIsView(false);
}
});
},
{ threshold: 1 },
);
observer.observe(ref.current);
return () => observer.disconnect();
}
return () => undefined;
}, [isView, ref]);
return [isView];
};
export default UseLazyLoading;
나는 Intersection Observer을 활용했고 커스텀훅을 만들어 lazy Loading을 구현했다. 이미지가 보인다면 view라는 상태가 true가 되면서 이미지가 보이도록 설계했다.

네트워크 탭을 보면 사용자가 이미지를 보는 시점에 로딩되는 것을 볼 수 있다.
위에서 대표적인 이미지 최적화 두가지를 적용해보았는데 next.js에서는 next/image라는 것을 제공한다.
https://nextjs.org/docs/app/building-your-application/optimizing/images
next/image의 장점은 크게 3가지가 있다.
여기서 확장자 최적화는 빌드를 했을 때 이미지를 최신 확장자 포멧인 webp로 바꿔준다는 뜻이다. 그렇기때문에 일일이 squoosh에서 확장자 최적화를 해줄 필요가 없어졌다.
이 부분은 결과부분에서 차이를 보여줄 예정이다.
두번째로 CLS방지
CLS(Cumulative Layout Shift)이란 사용자에게 발생하는 레이아웃 이동(layout shift) 빈도를 측정한 값이다. 값이 작을수록 좋은 것이다.
CLS를 next에서 제공을 해야하기 떄문에 width와 height를 넣어줘야한다.
import Image from 'next/image'
export default function Page() {
return (
<Image
src="https://s3.amazonaws.com/my-bucket/profile.png"
alt="Picture of the author"
width={500}
height={500}
/>
)
}
만약에 반응형이 가능한 이미지를 만들고 싶다면 fill 속성을 이용하면 된다.
import Image from 'next/image'
import mountains from '../public/mountains.jpg'
export default function Background() {
return (
<Image
alt="Mountains"
src={mountains}
placeholder="blur"
quality={100}
fill
sizes="100vw"
style={{
objectFit: 'cover',
}}
/>
)
}
위 처럼 quality나 크기 조절이 가능하기 떄문에 굉장히 유용하다.
추가로 next에서는 이미지 lazyLoading이 기본적으로 들어가 있다고 언급했는데 만약 나중에 볼 이미지가 아니고 바로 사용자가 이미지를 마주치는 이미지라면 priority 속성을 넣어주자.
<article className={styles.bannerImg}>
<Image priority src={wallet} alt="wallet" fill />
</article>
현재 내 프로젝트에서 배너가 바로 보이기때문에 priority 속성을 넣어줬다.
위의 과정을 거쳐서 메인페이지를 리팩토링했다.
빌드된 사이트에서 측정한 결과


이미지 용량이 대폭 줄어들었다.



LCP가 5.0에서 1.1으로 바뀌었다.
성능이 좋아지는 수치가 내 눈에 보여서 재밌었다.
그리고 이미지 최적화를 잘 하면 aws s3와 같은 클라우드 버킷에 이미지를 올려놓고 관리하는 경우에 서버에서 클라이언트에게 데이터를 전송하는 용량에 따라 금액 차이가 주로 나기 때문에 이러한 서버 비용을 크게 절약할 수 있다.
https://nextjs.org/docs/app/building-your-application/optimizing/images#examples