이 Next.js 블로그 프로젝트는 최근 한 달간 호스팅 유목 생활을 거쳤습니다. 현재는 AWS S3 + CloudFront로 호스팅을 하며 어느 정도 정착을 했습니다. 여기까지 오는 길이 순탄하지 않았습니다.
Vercel → Netlify → AWS Amplify → S3 + CloudFront
원래 무료로 사용하고 있던 Vercel에서의 문제, 대안인 줄 알았던 Netlify의 배신(정확히는 잘못된 정보), 그리고 유료 호스팅의 시작이었던 AWS Amplify와, 비용 최적화를 위한 S3+CF 호스팅까지. 대안 플랫폼으로 옮기자마자 부딪힌 한계, 그것들을 깨나가며 결국 여기까지 왔습니다. 이 과정이 꽤나 유의미하고 재미 있어서 포스팅으로 남겨보려 합니다.
이 글은 그 첫 번째 이야기예요. Vercel에서 Netlify로 이동하게 된 배경과, 마이그레이션 과정을 적어봅니다.
Vercel은 뉴비 개발자에겐 빛과 같은 존재입니다. 저에게도 그랬고요. GitHub에 올린 프로젝트와 손쉽게 연동하여 CI/CD, 호스팅, 방화벽과 로그, 통계까지, 모든 걸 무료로 제공하는 올인원 플랫폼입니다. Hobby 플랜이 상당히 넉넉하거든요.
하지만 몇 가지 한도가 있고, 제가 크게 부딪힌 부분 중 하나는 Fast Origin Transfer(이하 FOT)입니다.
https://vercel.com/docs/manage-cdn-usage#fast-origin-transferFOT는 Vercel의 Edge Network와 Origin(빌드 결과물이 저장된 서버) 사이에서 전송되는 데이터량을 의미합니다. 배포할 때마다 빌드 결과물이 Edge로 전송되면서 FOT가 소비됩니다. 문제는 Hobby 플랜에서는 한도가 10GB인데, 이 한도를 넘기면 추가 과금 없이 서비스가 즉시 중단된다는 점이에요. Hobby 플랜에서는 초과 사용(overage)이 허용되지 않아서, 한도에 도달하면 다음 결제 주기까지 배포가 막힙니다.
그동안은 10GB의 근처도 간 적이 없었어요. 그런데 AI와 함께 블로그를 본격적으로 개편한 지난 1월, FOT 사용량이 급격히 치솟기 시작했습니다.
원인은 Next.js 메이저 버전 업그레이드였어요.
https://nextjs.org/blog/next-16이 블로그는 원래 Next.js 13 + React 18 기반이었는데, 2025년 말에 Next.js 16 + React 19로 한 번에 업그레이드를 진행했습니다. 이 업그레이드 과정에서 두 가지 큰 변화가 생겼어요.
기존에 MDX 콘텐츠 관리에 사용하던 contentlayer라는 라이브러리가 있었습니다. contentlayer는 공식적으로 Next.js 13까지만 지원하고, 프로젝트 자체가 유지보수 중단 상태였습니다.
contentlayer - Support Next.js 15 Compatibility IssueNext.js 버전을 16으로 올리니 더 이상 동작하지 않게 됐습니다. 그래서 @mdx-js/mdx를 직접 사용하는 방식으로 MDX 파이프라인을 재구축했습니다. 동작은 잘 됐지만, 빌드 결과물의 구조가 이전과 달라졌어요.
Next.js 16은 기본 번들러가 Turbopack으로 변경되면서 .next 폴더의 구조와 크기가 이전 버전과 크게 달라졌습니다. 커뮤니티에서도 .next 폴더 크기가 기하급수적으로 증가했다는 보고가 여럿 올라왔어요.
이 두 가지가 합쳐지면서, 매 배포마다 전체 캐시가 무효화되고 FOT 사용량이 급증하는 상황이 된 것으로 추측하고 있습니다.
위의 이유를 알기 전, 처음에는 단순히 이미지 용량을 줄이면 되지 않을까 생각했습니다. 블로그 프로젝트 내의 /public 폴더에 이미지 파일들을 넣어서 포스팅을 구성했거든요. 파악해보니 public/imgs/ 폴더가 526MB나 됐고, 5MB 이상인 파일만 19개였습니다.
자세한 원인 분석을 하지는 않았지만, 어차피 언젠가 프로젝트랑 분리하려고 생각하고 있던 터라 바로 진행했습니다. 모든 이미지를 AWS S3 + CloudFront CDN으로 옮겼어요. S3 버킷에 이미지를 업로드하고, CloudFront CDN을 앞에 붙여서 글로벌 캐싱이 되도록 구성했습니다. 블로그 코드에서는 MdxImage 컴포넌트가 /imgs/ 경로를 자동으로 CDN URL로 변환하도록 처리했어요.
변경 전: Browser → Vercel (public/imgs/)
변경 후: Browser → CloudFront (CDN) → S3 (원본)한 가지 문제가 더 남아 있었습니다. 저는 Git Merge 전략을 사용하고 있습니다. 모든 개별 commit이 git log에 남는 방식입니다. (반대는 PR 단위로 저장되는 Squash Merge가 있습니다.)
아무튼 Git의 모든 변경 이력을 보관하기 때문에, 파일을 삭제해도 .git 폴더에는 과거에 commit된 대용량 이미지의 기록이 그대로 남아 있었어요. 실제로 .git/objects/pack/이 514MB나 됐습니다. 저장소 자체의 크기가 줄어들지 않는 거예요.
이 상황에서 Claude Code는 git-filter-repo를 제안했습니다.
이력을 정리한다
단순히 git rm으로 파일을 삭제하는 건 "이 파일을 더 이상 추적하지 않겠다"는 새로운 commit을 추가하는 것일 뿐이에요. 과거 commit에는 여전히 해당 파일의 전체 바이너리가 남아 있습니다. Vercel은 배포할 때 Git 저장소를 클론하는데, 히스토리에 남은 대용량 파일까지 모두 내려받으면서 FOT를 소비합니다. 히스토리 자체를 정리하지 않으면 근본적인 해결이 안 되는 거였죠.
hash가 변경됨에 주의하기
이 도구는 모든 과거 commit을 순회하면서 조건에 해당하는 파일 데이터를 물리적으로 제거합니다. 히스토리를 재작성하는 것이기 때문에, 모든 commit의 hash가 변경돼요. 기존의 commit 객체와는 완전히 다른 새로운 객체가 만들어지는 거예요. 그래서 작업 후에는 git push --force로 원격 저장소를 강제 업데이트해야 합니다.
git push --force는 원격 저장소의 히스토리를 완전히 덮어쓰는 작업이에요. 혼자 사용하는 개인 프로젝트에서는 괜찮지만, 협업 중인 저장소에서는 다른 사람의 작업이 유실될 수 있으니 매우 신중하게 사용해야 합니다.
개인 프로젝트의 매력이 뭐겠습니까, 문제 해결을 위해 과감하게 시도해봤습니다.
이렇게까지 해서 저장소 크기를 대폭 줄였지만, FOT 문제는 여전했습니다. 결국 이미지가 아니라 Next.js 16의 빌드 결과물 자체가 커진 것이 근본 원인이었어요. 이미지 마이그레이션 자체는 CDN을 통한 이미지 로딩 속도 개선이라는 부수적인 성과가 있었지만, 원래 해결하려던 FOT 문제에는 효과가 없었습니다.
이쯤 되니 피곤해졌습니다. 가장 간단한 해결책은 돈으로 해결하기, Vercel Pro 플랜으로 업그레이드하는 겁니다. FOT가 $0.06/GB로 사용량에 비례하게 올라가기 때문이에요. Pro 플랜은 기본적으로 매달 $20를 내야 하지만, FOT 자체는 가격이 쌉니다. (출처: Vercel Pricing)
그런데 마음이 내키지 않았습니다. 지금까지 3년 넘게 무료로 잘 쓰고 있었는데 갑자기 매달 $20과 추가금을 내야 한다는 상황이 싫더라고요. 특히 Vercel은 영악해서 여러 명목으로 추가금을 야금야금 뜯어갑니다. 앞으로는 더 활발히 개발이 이어질 텐데, 20달러의 고정 과금과 천장이 없는 추가 과금을 걱정을 해야 한다는 것이 별로였습니다.
그럼에도 호스팅은 당장 필요한 상황이었습니다. 1월 안으로 2025년 연말 회고와 기술 포스트를 작성해 포스팅한다는 목표가 있었거든요. (참고) 어떤 방식이든 빠르게 배포할 수 있는 환경이 필요했습니다.
아예 플랫폼을 바꾸기로 했습니다.
Claude Web에서 여러 호스팅 서비스의 특징을 분석하고 비교한 뒤 방향을 정했어요.
결론부터 스포하자면, Claude가 옛날 Netlify Free 플랜을 기준으로 잘못 비교해 설명해줬습니다. 그리고 저는 그것도 모르고 Netlify를 선택한 바보입니다.
2025년 5월부터 Netlify Free 플랜은 크레딧 정액제로 변경되어 매달 300 크레딧 내에서 크레딧을 소비하는 방식으로 제공되고 있습니다.(Netlify Pricing)
일단은 제가 비교했던 후보군들이 어땠는지 선택의 기준을 보여드리겠습니다.
Netlify Free는 잘못된 정보라는 사실 재차 강조합니다.
이게 가장 큰 이유였어요. Netlify에는 Fast Origin Transfer 같은 별도 전송량 한도가 존재하지 않습니다. Bandwidth 100GB 한도만 있고, 하루 수십 명 방문하는 개인 블로그에서는 이 정도면 충분했어요.
Cloudflare Pages도 후보였지만 Edge Runtime 제약이 걸렸습니다. 블로그의 OpenGraph API Route에서 외부 URL을 fetch해서 HTML을 파싱하는 로직이 있는데, Edge Runtime에서는 일부 Node.js 라이브러리가 동작하지 않을 수 있거든요.
Netlify는 Node.js를 완전히 지원하기 때문에 기존 코드를 거의 그대로 사용할 수 있었어요.(Netlify - Next.js Framework Setup Guide)
Vercel과 Netlify는 동작 방식이 비슷합니다. GitHub 연동 → 자동 빌드 → 배포 흐름이 거의 동일하고, 환경변수와 빌드 설정만 옮기면 됐어요. 코드 변경은 Vercel 전용 패키지 제거와 Netlify 설정 파일 일부 정도로 거의 없었습니다.
Claude Code와 함께 마이그레이션 계획을 5단계로 수립하고 진행했어요.
먼저 현재 Vercel 환경의 설정을 백업했습니다. 환경변수 목록, 빌드 설정, DNS 설정을 기록해뒀어요.
그 다음 Netlify 계정을 생성하고 GitHub 저장소를 Import했습니다.
Netlify Dashboard에서 기본 설정을 구성했어요.
- Build command:
pnpm build - Publish directory:
.next - Node.js 버전: 20
환경변수는 Vercel Dashboard에서 확인해서 Netlify로 옮겼습니다. 블로그 프로젝트에는 환경변수를 사용하는 로직이 많지 않아서 큰 수고는 없었어요.
이 단계가 실제 코드 변경이 필요한 부분이에요. 변경 사항은 생각보다 적었습니다.
Vercel 전용 패키지 제거
pnpm remove @vercel/analytics @vercel/speed-insights@vercel/analytics는 Vercel 자체 방문자 분석 도구이고, @vercel/speed-insights는 Core Web Vitals 측정 도구예요. 둘 다 Vercel 환경에서만 동작하기 때문에 제거해야 했습니다. Google Analytics는 Vercel과 무관하므로 그대로 유지했어요.
layout.tsx 수정
src/app/layout.tsx에서 Vercel 전용 컴포넌트를 제거했습니다.
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';
// JSX에서도 제거
<Analytics />
<SpeedInsights />netlify.toml 생성
프로젝트 루트에 Netlify 빌드 설정 파일을 추가했습니다.
[build]
command = "pnpm build"
publish = ".next"
[build.environment]
NODE_VERSION = "20"
[[plugins]]
package = "@netlify/plugin-nextjs"
[[headers]]
for = "/*"
[headers.values]
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"@netlify/plugin-nextjs
https://www.npmjs.com/package/@netlify/plugin-nextjs@netlify/plugin-nextjs는 Netlify에서 Next.js 앱을 배포할 때 필요한 빌드 후처리를 자동으로 해주는 플러그인이에요. ISR, API Routes, 이미지 최적화 등을 Netlify 환경에 맞게 변환해줍니다. netlify.toml 없이도 Netlify가 Next.js를 자동 감지하지만, 명시적으로 설정을 관리하고 싶어서 추가했어요.
코드 변경을 Push하니 Netlify에서 자동으로 빌드가 시작됐어요. 빌드가 성공한 뒤, 커스텀 도메인을 연결했습니다.
Type: CNAME
Name: blog
Value: [site-name].netlify.appDNS CNAME 레코드를 변경했고, 전파까지 약 10분 정도 걸렸습니다.
플랫폼을 이전할 때 Vercel 프로젝트는 삭제하지 않고 유지해뒀어요. 문제가 생기면 DNS만 다시 Vercel로 변경하면 5분 안에 롤백할 수 있도록 대비한 거예요.
DNS 설정 후, Vercel에서와 완전히 동일하게 동작하는 것을 확인했습니다. Vercel에서 제공하는 GitHub CI/CD, main branch가 아닌 feature 브랜치 단위의 배포 미리보기 기능까지 판박이로 제공하니 말이죠.
Vercel에서 Netlify로의 이전 자체는 수월했어요. 핵심 로직은 하나도 건드리지 않았고, Vercel 전용 패키지 제거와 Netlify 설정 파일 추가가 전부였습니다.
Netlify로 옮기고 나서 바로 연말 회고와 기술 포스트를 배포할 수 있었어요. 당장의 목표는 달성한 셈이었습니다.
하지만 Netlify에서 오래 머물지 못했습니다. 앞서 스포한대로 알고 보니 Netlify Free 플랜이 변경됐던 걸 몰랐는데 피부로 마주하게 되었거든요. 없던 빌드 크레딧 한도가 생겼고요... 사실상 빌드 20번 하면 끝나는 분량입니다. 트래픽과 빌드 결과물에 따라 추가적으로 더 크레딧이 사용됐고요. 네, 오히려 Vercel보다 더 악질이었습니다.
사실 Vercel의 시기를 떠나기 전 포스팅하고 싶은 게 좀 더 있습니다.
- 빌드 메모리 트러블슈팅: 빌드 과정에서 발생한 메모리 문제를 해결하기 위해 SSG를 ISR로 넘겨서 빌드 시간 단축과 메모리 공간 확보
- 빌드 완료 디스코드 알림 연동: 프로젝트 Webhook과 엮어서 Vercel 빌드 결과를 디스코드로 보내는 고도화 기능 구현
하지만 일단 호스팅 유목민 시리즈를 모두 완료하고 다시 시간을 거슬러 돌아와서 도움이 될 내용을 적어보겠습니다.
저는 PoC를 거쳐 AWS Amplify로 이전을 결정했습니다. 다음 글에서 계속됩니다.
