
Vercel Usage Limit Warning 해결기 (feat. 이미지 최적화)
2022.10.06
blog
dev
nextjs
next/image 컴포넌트에 대한 이해이미지를 최적화해보자Image Cache 설정Resolution Switching외부 이미지 로더 사용Lazy Loading맺으며Reference

블로그를 잘 배포하고 글을 쓰던 무렵, Vercel로부터 Usage Limit이 넘었다는 경고 메일을 받게 된다. 내용은 Image Optimization Source Images가 108.7%를 넘었다는 것. 헉…
나는 SSG 를 이용해 블로그 페이지를 보여주고 있고, 이미지 컴포넌트의 경우 따로 최적화와 관련된 옵션을 주지 않고 배포를 해놓고 보니 Vercel에서 이미지 최적화를 과도하게 사용하여 한도가 초과되었다는데 무엇이 문제였을까?
next/image 컴포넌트에 대한 이해
우선 나는 이미지를 보여주기 위해
<img />
대신 Next에서 권장하는 <Image />
컴포넌트를 사용하고 있다. next/image를 사용할 시, 여러가지 옵션(width, height, loading, quality, sizes 등)을 체크할 수 있는데, 기본 옵션으로 설정을 해놓으면 자동으로 최적화가 들어간다. (Next 짱…) 다만 이렇게 자동으로 해놓다보니 무슨 이유에선가 최적화할 이미지 소스가 많아져 Vercel 서버에 무언가 부하를 많이 준 것으로 보인다. 아직 그렇게 글이 많은 상태가 아니라 더욱 의아했지만 다른 조치를 취하지 않으면 메일에서 언급한대로 나를 block할까봐 허둥지둥 이미지 최적화를 해야했다.
next/image는 자동으로 소스 이미지의 quality, size, format을 바꿔주며 자동으로 optimize를 한다고 한다.
unoptimized option을 추가한다면
<Image src={src} alt="post_thumbnail" width={width} height={height} className={className} unoptimized // 추가 />
unoptimized를 추가하면 최적화는 꺼지고 이미지 로딩속도는 더 늘어날 것이다. 대신 vercel 서버에서 이미지 최적화를 적게할 수 있어 좋을 것 같다.
이미지를 최적화해보자
이전에 단순히 서비스 서빙을 할 때는 몰랐는데, 찾아보면서 이미지 최적화를 위해서는 아래와 같은 다양한 방법이 있음을 알았다.
- image lazy loading
- image resizing (resolution switching)
- image compress
- image cache 설정하기
위의 최적화 방법을 모두 사용한 것은 아니다. lazy loading을 할 때 blur 효과도 고려했지만, 나의 목표는 페이지에서 이미지 로딩속도를 빠르게 하고 Vercel 서버에서 실제보다 많은 이미지 최적화가 일어나는 것을 막는 것이기 때문에 내가 이 문제를 해결하기 위해 시도한 것을 시간 순으로 적어보겠다.
Image Cache 설정
우선 이미지 최적화 하면 내가 제일 먼저 드는 생각은 ‘캐시가 잘 되고 있나?’였다.
캐싱을 하면 동일한 이미지를 반복해서 요청하지 않아도 되므로 lazy loading이나 resizing 등의 다른 최적화보다 눈에 띄는 성능 개선을 할 수 있을거라 생각했다. 다만 next/image는 다른 설정 없이도 이미지 캐싱을 기본적으로 지원하는 것으로 알고 있다.
우선 현재 사이트에서 네트워크 탭을 확인해보자.

request header에
cache-control
헤더가 no-cache로 설정되어있고, Response Headers에 X-Nextjs-Cache
가 MISS
로 되어있다. ISR이 적용된 페이지의 경우 캐시 상태를 제대로 확인하기 위해서는 response 헤더의 X-Nextjs-Cache
속성을 봐야하고, 이 속성은 세가지, MISS
, STALE
, HIT
값을 가질 수 있다. 즉, 현재 이미지는 캐시되지 않았다.Cache-Control이
max-age=60
으로 설정되어있음에도 새로고침을 연속으로 여러차례 해도 HIT이 아닌 MISS라는 것은 무언가 path 설정이 잘못되어 캐시된 이미지를 가져오지 못하고 있기 때문이다.(
no-cache
가 캐싱을 사용하지 않겠다는 뜻이 아니다. 캐시된 이미지를 가지고 있어도 서버에 새로운 컨텐츠가 있는지 묻고 갱신되었다면 가져오겠다는 뜻. 캐싱을 원치 않는다면 no-store
을 사용하면 된다.)
반면 about 페이지에 있는 프로필 사진을 확인해보니 public/images 안에 static하게 저장되어있어서 캐시가 제대로 작동하는 것을 확인할 수 있었다.
notion cover 이미지를 가져올 때에는 Notion private API를 이용해서 가져온다. 현 프로젝트에서는 GET /pages/api/posts 요청을 보내면 아래
getNotionPosts
가 호출되어 posts 정보를 가져온다.export const getNotionPosts = async () => { const posts = await fetch(URL, { headers: { Authorization: `Bearer ${process.env.NOTION_SECRET_KEY}`, "Notion-Version": "2021-05-13", }, method: "POST", }) .then((res) => { return res.json(); }) .then((res) => { return res.results.filter( (post: Post) => post.properties.visible.checkbox, ); }); return posts; };

Notion에 이미지를 직접 업로드하면 AWS S3 스토리지를 이용하는 것으로 보인다. ‘어쩌면 cover 이미지의 url이 동적으로 바뀌는 것이 아닐까?’ 의심을 했다.
테스트하기 위해 다시 request를 날려보니 역시 예상대로였다. 같은 이미지임에도 요청을 보낼때마다
X-Amz-Signature
의 값이 바뀌어 url의 string이 바뀐다. 이미지는 보통 immutable한 리소스로 간주되지만 이렇게 url 자체가 바뀌게 되면 실제로 이미지가 바뀐게 아님에도 새로운 image url이 오니 매 페이지 요청마다 vercel 서버에서는 새로 optimizing을 해주는 로직이 돌아갔을 것이다. 이제 저 메일을 받게 된 원인을 찾은 것 같다.구글링해보니 나와 비슷한 고민(image over optimizing)을 한 분이 있었다. image request를 보낼 때 API route를 해주는 방법, custom image loader을 만들어서 사용하는 방법도 있다. 괜찮은 방법으로 보여서 일단 감사의 코멘트를 남겼다.
순간 머리속에 Image hosting 사이트에 업로드하고 url을 거는 식으로 바꿔줘야하나… 고민이 들었다.
imgbb에 업로드하고 링크를 걸어본다.


커버가 external url로 바뀌었다. 주소도 바뀔 일이 없어보인다.
그러면 캐시는 잘 작동할까?

역시 MISS(처음 접속)난 후 다시 새로고침하니 HIT으로 역시나 잘 작동한다.
// Response Headers cache-control: public, max-age=315360000 ... x-vercel-cache: HIT
외부 이미지 호스팅(imgbb)을 이용하니 s3에 비해 로딩 속도도 훨씬 빨라졌지만, 초기 next blog를 만들 때 notion에서 한번에 글을 관리하자는 목표에서는 조금 빗나간다. 이미지를 호스팅 사이트에 업로드하고 링크를 붙여넣어야하는 번거로움이 있다. 뿐만 아니라 무료 호스팅 서비스라 업로드하면 이미지 화질도 눈에 띄게 떨어진다.
따라서 이 부분은 추후에 다른 방법으로 개선할 생각을 하고 있다.

Resolution Switching
모바일 화면(390px 남짓)에서 4k에 해당하는 이미지를 가져올 필요는 없을 것이다. 필요 이상 크기의 이미지를 가져오는 것은 네트워크 대역폭을 낭비하는 것이다. 이미지를 화면의 크기에 맞게 로드하는 것을 말한다.

next/image를 쓴 것을 보면 img 태그 안에
srcset
속성으로 deviceSizes 별(위 사진에서 1x, 2x)로 보여주어야 할 다른 이미지 파일이 생성되어 들어가있는 것을 볼 수 있다. 바로 Vercel에서는 이 srcset을 자동으로 적용해주는 과정에서 AWS s3에서 보내주는 이미지의 origin이 image optimization source images가 무수히 많이 만들어졌을 것이고, 이게 경고 메일을 발송하게 된 이유라고 생각된다.
외부 이미지 로더 사용
custom 이미지 URL 서버를 이용하는 방법도 있을 것이다.
const myLoader = ({ src, width, quality }) => { return `https://example.com/${src}?w=${width}&q=${quality || 75}` }
다만 이 경우에는 외부 서버를 따로 이용해야한다는 점에서 다룰 내용이 너무 많아져 이 글에서 다루지는 않겠다.
Lazy Loading
loading
는 html 기본 img 태그에도 있는 속성인데 loading=”lazy”
로 설정 하면 뷰표트로부터 계산된 거리에 도달할 때까지 리소스 로딩을 지연시킨다. 반대로 eager은 페이지에서의 위치와 관계없이 리소스를 즉시 로드한다.이미 Skeleton으로 작업해놓은 부분이 있어서 lazy loading까지 적용할 필요는 없어보였고, 블로그에서 image lazy loading은 아직 도입하지 않았다.
이 부분은 추후 다른 부분에서 적용할 기회가 있으리라 생각한다.
맺으며
아직 30일이 지나지 않아 확실하게 해결됐는지 확인이 어렵지만 아무래도 해결된 것으로 보인다.
이미지 로딩 속도도 훨씬 빨라져 Slow 3G 정도의 느린 네트워크 환경이 아닌 이상 이전에 열심히 만들어둔 Skeleton을 거의 볼 수 없어졌다(다행이다).