Vercel 서버리스 함수 250MB 초과 — 원인 분석과 해결
Vercel에 배포했더니 이런 에러가 떴다.
A Serverless Function has exceeded the unzipped maximum size of 250 MB.
원인을 추적하기 전에, Vercel이 어떻게 동작하는지부터 정리한다.
Vercel의 배포 구조
git push를 하면 Vercel이 빌드를 돌리고, 결과물을 두 곳에 나눠서 배치한다.
CDN — 정적 파일 서빙
빌드 때 미리 만들어진 HTML, CSS, JS, 이미지가 올라간다. 사용자가 요청하면 CDN에서 바로 응답한다. 서버가 할 일이 없다.
public/ 폴더에 있는 파일도 자동으로 CDN에 올라간다.
별도 설정 없이 https://도메인/images/xxx.png로 접근할 수 있다.
서버리스 함수 — 동적 요청 처리
요청이 올 때만 잠깐 켜지는 서버다. FaaS(Function as a Service)의 한 형태로, 서버 인프라를 직접 관리할 필요 없이 함수 코드만 올리면 클라우드가 알아서 실행해준다.
서버리스 함수 번들에 들어가는 건 브라우저에 내려주는 HTML/CSS/JS가 아니다. 서버에서 실행되는 Node.js 코드와 그 코드가 의존하는 패키지가 들어간다.
예를 들어 댓글 API(/api/comments)라면:
import { supabase } from "@/lib/supabase";
export async function GET(request: Request) {
const comments = await supabase.from("comments").select("*");
return Response.json(comments);
}이 코드와 @supabase/supabase-js 패키지가 번들에 포함된다.
Next.js에서 app/api/ 안에 route.ts를 만들면, 그게 서버리스 함수가 된다.
Vercel은 이 번들의 크기를 250MB로 제한하고 있다.
라우팅은 배포 시점에 결정된다
Vercel이 매 요청마다 "이건 CDN? 서버리스?" 하고 판단하는 게 아니다. 배포할 때 이미 라우팅이 정해져 있다. 이 경로는 CDN, 이 경로는 서버리스 함수 — 요청이 오면 그 규칙에 따라 바로 보내준다.
SSG vs SSR
Vercel이 라우팅을 나누는 기준은 결국 SSG냐 SSR이냐다.
SSG (Static Site Generation)
빌드할 때 HTML을 미리 만들어놓는 방식이다. CDN에 올라가고, 사용자가 접속하면 바로 내려준다.
핵심은 콘텐츠가 HTML에 이미 채워져 있다는 거다. SPA(Single Page Application)도 CDN에서 서빙하지만, SPA는 빈 HTML 하나를 내려주고 브라우저에서 JS가 화면을 그린다. SSG는 완성된 HTML이 내려오기 때문에 브라우저가 받자마자 바로 내용이 보인다.
이 블로그의 포스트 페이지가 SSG다. 빌드할 때 Velite가 MDX에서 글 내용을 꺼내고, Next.js가 그걸 HTML에 다 넣어놓은 채로 CDN에 올린다. 누가 봐도 글 내용은 똑같으니까 가능한 방식이다.
SSR (Server Side Rendering)
요청이 올 때마다 서버가 HTML을 만드는 방식이다. 사용자마다 다른 내용을 보여줘야 할 때 사용한다.
네이버 메인 페이지를 생각하면 된다. 로그인한 사용자 이름, 맞춤 뉴스, 실시간 검색어 — 전부 사용자마다, 시간마다 다르다. 빌드 때 미리 만들어둘 수가 없다.
Vercel에서는 SSR을 서버리스 함수가 실행한다. 요청이 오면 Vercel이 함수를 깨우고, 함수가 HTML을 만들어서 응답한다.
Next.js는 어떻게 판단하나?
Next.js가 코드를 분석해서 자동으로 판단한다. 코드에 이런 게 있으면 **동적(SSR)**으로 분류한다.
cookies()— 쿠키 읽기headers()— 요청 헤더 읽기searchParams— 쿼리스트링 사용fetch에cache: "no-store"— 캐시 없이 매번 새로 가져오기
빌드 시점에는 쿠키가 뭔지, 헤더가 뭔지 알 수가 없다. 실제 요청이 와야 알 수 있으니까 정적으로 못 만드는 거다.
이런 게 없으면 "빌드 때 다 만들 수 있겠다" 하고 SSG로 처리한다.
빌드 로그에서 확인할 수 있다.
Route (app)
┌ ƒ / ← 서버리스 함수 (SSR)
├ ƒ /api/comments ← 서버리스 함수 (API)
├ ● /posts/[slug] ← SSG (정적 생성)
├ ○ /sitemap.xml ← Static (완전 정적)
한 페이지 안에서 SSG와 서버리스 함수가 공존한다
이 블로그의 포스트 페이지가 좋은 예다.
- CDN에서 SSG HTML을 내려줌 (댓글 영역은 비어있음)
- 브라우저가 HTML을 렌더링
- 댓글 컴포넌트의 JS가 실행되면서
/api/comments로 요청 - 서버리스 함수가 DB에서 댓글 가져와서 응답
- 브라우저가 댓글 영역을 채움
"SSR로 서버에서 댓글까지 말아서 한 번에 보내주면 더 편하지 않나?" 라고 생각할 수 있다. 트레이드오프가 있다.
SSR (서버에서 다 말아서 줌)
- 한 번에 완성된 페이지가 옴
- 요청마다 서버가 HTML을 만들어야 해서 응답이 느림
SSG + 클라이언트 API 호출 (지금 방식)
- HTML은 CDN에서 바로 내려오니까 페이지 로딩이 빠름
- 댓글이 살짝 늦게 뜸
글 내용이 핵심이고 댓글은 부가 요소다. 글을 빨리 보여주는 게 더 중요하니까 SSG + API 호출 방식이 맞다. 댓글 때문에 페이지 전체 로딩을 느리게 만들 필요가 없다.
에러 원인 추적
배경 지식은 됐고, 이제 250MB 에러를 추적한다.
Next.js는 빌드할 때 각 라우트가 런타임에 어떤 파일을 필요로 하는지 추적한다.
이걸 Output File Tracing이라 하고, 결과는 .nft.json 파일에 기록된다.
추적된 파일이 서버리스 함수 번들에 포함된다.
posts/[slug] 라우트의 트레이싱 결과를 확인해봤다.
Total traced: 238개
---
2.5MB public/images/news/2026-01-31-1.png
2.7MB public/images/news/2026-01-31-2.png
1.0MB public/images/news/2026-01-31-3.png
...
public/images/news/ 폴더 전체가 서버리스 번들에 포함되고 있었다.
209MB, 130개 이미지
이게 범인이었다.
왜 이미지가 서버리스 번들에?
이 블로그는 Velite로 MDX 콘텐츠를 빌드한다. 빌드된 콘텐츠 데이터에 이미지 경로가 포함돼 있다. Next.js의 file tracing이 이 경로를 따라가면서 "이 이미지도 런타임에 필요하겠다"고 판단하고 서버리스 번들에 넣어버린 거다.
실제로는 public/ 폴더에 있으면 Vercel이 자동으로 CDN에 올려주기 때문에, 서버리스 함수에 포함될 이유가 없다.
쓰지도 않는 이미지가 번들에 끼어들어서 209MB를 차지하고 있었다.
해결
1. outputFileTracingExcludes로 이미지 제외
next.config.ts에 outputFileTracingExcludes를 추가해서 이미지 파일을 서버리스 번들에서 제외한다.
const nextConfig: NextConfig = {
outputFileTracingExcludes: {
"*": ["public/images/**"],
},
};이미지는 CDN이 서빙하니까 서버리스 번들에 없어도 된다.
2. 빌드 전용 패키지를 devDependencies로 이동
package.json에는 두 종류의 의존성이 있다.
- dependencies — 런타임에 필요한 패키지. 서버리스 번들에 포함됨.
- devDependencies — 빌드할 때만 필요한 패키지. 서버리스 번들에 포함 안 됨.
shiki는 코드 블록에 구문 하이라이팅을 입히는 라이브러리다.
빌드할 때 이미 색칠을 끝내고 HTML로 변환해놓기 때문에 런타임에는 필요 없다.
그런데 dependencies에 있으면 Vercel이 서버리스 번들에 포함시킨다.
pnpm remove shiki rehype-pretty-code rehype-slug velite
pnpm add -D shiki rehype-pretty-code rehype-slug velite결과
트레이싱 파일 수가 238개에서 90개로 줄었고, 이미지 파일은 0개가 됐다. 250MB 에러 없이 배포 성공.
정리
| 원인 | 해결 |
|---|---|
public/images/news/ 이미지 130개(209MB)가 서버리스 번들에 포함 | outputFileTracingExcludes로 제외 |
빌드 전용 패키지(shiki 등)가 dependencies에 위치 | devDependencies로 이동 |
핵심은 런타임에 필요 없는 건 서버리스 번들에서 빼라는 거다.
public/ 폴더 이미지는 CDN이 서빙하고, 빌드 전용 패키지는 빌드가 끝나면 역할이 없다.
서버리스 함수는 코드 실행에 필요한 최소한의 파일만 가지고 있으면 된다.
댓글
불러오는 중...