이번 프로젝트에 Next.js를 도입함으로써 가장 크게 체감됐었던게 Next웹서버와 이미지였다.
이번에는 그 중에서 이미지에 대해 다뤄보게 됐다.
Next/Image는 별다른 설정 없이도 자체적인 이미지 최적화를 제공한다.
그렇다고 Next/Image import 딸깍 ⇒ “이미지 최적화 완료” 를 외치는 것보다, 최소한 이미지 최적화에 대한 내용과 Next/Image는 어떠한 특징을 가지고 있는지 알아보려 한다.
우선 여기선 기본적인 이미지 최적화에 대한 것들을 살펴보고, 다음에 Next/Image는 어떠한 방식으로 최적화를 진행하는지 살펴볼 것이다
이미지 최적화는 웹 사이트의 성능을 개선하기 위해
이미지의 품질 저하 없이 사용자에게 이미지를 빠르고 효율적으로 제공하는 최적화 프로세스이다.
이미지 최적화를 하기 위한 수많은 방법이 있지만, 대표적인 것들을 먼저 알아보자
이미지는 크게 아래 2가지로 구분된다
비트맵 ( .jpg, .png, .gif, .webp 등 )
각 픽셀이 모여 만들어진 정보의 집합. 픽셀 단위로 화면상에 렌더링히며
일반적으로 사용하는 대부분의 이미지가 비트맵 형식으로 그림판, 포토샵 등의 툴로 편집할 수 있다.
정교하고 다양한 색상을 자연스럽게 표현 가능하지만,
이미지 확대/축소 시 계단 현상, 품질 저하가 발생한다
벡터 ( .svg )
수학적 정보의 형태들이 만들어내는 결과물.
이미지가 가지고 있는 점, 선, 면의 위치(좌표), 색상 등의 정보를 온전히 가지고 있으며 그것을 화면상에 렌더링한다.
이미지 확대/축소 시에도 깨지지 않고 용량 변화도 없지만,
정교한 이미지(인물, 풍경 등)를 표현하기 어렵다.
이 중에서 흔히들 최적화의 대상으로 많이 거론되는 비트맵 이미지의 파일 형식들에 대해 알아보자.
JPG
가장 널리 쓰이는 이미지 포맷
손실 압축 방식을 사용한다
이미지의 용량을 획기적으로 줄여 사용 가능하며 용량 절약에 효율적이다 (= 압축률이 높다)
그렇지만 저장하는 과정에서 이미지가 손상된다
저장이 누적될수록 손실도 누적된다
표현 색상도(24비트 컬러, 약 1600만 색상)가 뛰어나 고해상도 표시 장치에 적합하다
이미지의 품질과 용량을 쉽게 조절 가능하다
GIF
비손실 압축 방식을 사용한다
이미지를 손상시키지 않으면서 저장할 수 있다
그렇지만 용량을 획기적으로 줄이기에는 어려움이 있다
이미지 파일 내에 이미지 및 문자열 같은 정보를 저장할 수 있다
여러 장의 이미지를 하나의 파일에 담을 수 있어 동영상 같은 이미지(애니메이션, 움짤 등)를 지원한다
8비트 컬러만 지원하므로 다양한 색상을 표현하기 위한 작업에는 부적합하다
PNG
gif의 대체 포맷으로 개발됐다
비손실 압축 방식을 사용한다. 그렇지만 여전히 용량을 획기적으로 줄이기에는 어려움이 있다.
8비트(256 색상)/24비트(약 1600만 색상) 컬러 이미지를 동시에 지원한다
Alpha Channel(투명도)을 지원한다
W3C의 권장 포맷이다
WEBP
구글에서 개발한 .jpg, .png, .gif를 모두 대체할 수 있는 이미지 포맷이다
완벽한 손실/비손실 압축 동시 지원(선택 사용 가능).
.gif와 같은 애니메이션 지원.
Alpha Channel(투명도) 지원(손실/비손실 모두).
지원되는 브라우저에 제한이 있다 (Explorer에서는 지원 X)
크로스 플랫폼을 고려한다면 WEBP는 완전한 호환이 되지 않는다는 점에 주의해야 한다.
이런 특징들을 종합해 봤을 때, 이미지 최적화를 위한 파일 형식은
바로 webp를 사용하는 것이다.
실제로 webp의 무손실 압축의 경우 동일 화질에서 PNG 대비 26%, JPG 대비 25~34% 더 작은 크기의 이미지를 만들 수 있다고 하며
손실 압축을 해도 PNG 대비 1/3 수준까지 가능하다고 한다.
출처 : https://developers.google.com/speed/webp?hl=ko
❗최근에는 webp를 뛰어넘는 AVIF라는 포맷도 있다고 한다
AVIF는 훨신 더 나은 무손실 압축과 고품질을 자랑한다.
webp보다 훨씬 적은 용량으로 저장이 가능한 압축률이 20%나 높다고 한다.
특정 페이지에 수많은 이미지가 있을 때,
실제 사용자가 아직 보지 않은 (뷰포트에 감지되지 않은) 이미지들은 굳이 불필요하게 로드하지 않는 것이다.
화면에 나타나기 전에는 placeholder 이미지를 넣어두었다가, 화면에 나타났을 때(뷰포트에 걸렸을 때) 이미지 리소스를 요청하도록 하게 된다.
이렇게 하면 필요한 순간에만 네트워크에 요청을 하도록 만들어 성능을 최적화할 수 있다.
이미지 리소스를 캐싱해둬서 네트워크 요청 시 응답 속도를 매우 향상시킬 수 있다.
그렇지만, 캐싱 역시 추가적인 메모리가 할당되므로 선택적으로 사용하는게 좋다
사실, 이미지 캐싱은 방법이 너무나도 다양하다. 그래도 가장 기본적인 매커니즘은 아래와 같다.
브라우저 캐시 사용
로컬 이미지 (public 폴더)
보통 프론트 서버배포를 할 때 사용되는 Vercel, Express.js, Nginx 등은 React 애플리케이션의 정적 파일을 제공하는 역할을 하게 되는데
여기서 추가적인 설정을 통해 웹 서버는 정적 파일을 제공할 때 캐싱 정책을 설정할 수 있다.
외부 이미지 (URL로 불러오는 이미지)
http요청으로 이미지 리소스를 요청
api 서버는 응답에 캐싱 관련 HTTP 헤더(Cache-Control, Expires, ETag, Last-Modified 등)를 포함시키게 된다.
브라우저는 헤더 정보에 맞게 브라우저의 캐시 스토리지에 저장을 한다
재방문 시 브라우저는 캐시된 자원이 유효한 경우, 서버에 요청하지 않고 로컬 캐시에서 자원을 불러온다
캐시된 자원의 유효 기간이 만료되었거나 변경되었을 가능성이 있는 경우, 서버에 조건부 요청을 보낸다
헤더를 포함한 요청을 서버에 보내고
CDN의 캐싱사용
CDN으로 외부 이미지를 요청할 때, CDN 내부에서 캐싱한 이미지로 응답을 함으로써 더욱 빠른 응답을 보장받을 수 있다.
Blob객체를 활용한 Tanstack-Query를 적용할 수도 있다. (실제로 이 방법을 쓰는지는 모르겠다)
import { useQuery } from 'react-query';
function MyImageComponent({ imageUrl }) {
const { data: image, isLoading } = useQuery(['image', imageUrl], () =>
fetch(imageUrl).then(res => res.blob())
);
if (isLoading) return <div>Loading image...</div>;
return <img src={URL.createObjectURL(image)} alt="Fetched" />;
}
모든 디바이스 화면에서 같은 화질, 같은 사이즈로 이미지를 보여줄 필요는 없다.
디바이스의 크기에 알맞게 때로는 작은 이미지를 보여줌으로써 성능을 최적화할 수 있다.