How We Saved Big and Simplified Our Image Pipeline: Adopting bunny.net on DEV
Hey everyone, Ben here. If you’ve been following the journey of DEV and our open source project...
개요 #
DEV(Dev.to)를 운영하는 Forem 팀은 웹 성능을 플랫폼의 핵심 철학으로 삼아왔다. HTML 페이지는 Fastly를 통해 엣지에서 캐싱되어 대부분의 요청이 서버에 닿기 전에 처리된다. 하지만 사용자가 업로드하는 이미지는 전혀 다른 문제였다.
포스트 커버, 프로필 아바타, 댓글 스크린샷까지 수십억 장의 이미지를 전 세계에 서빙하다 보니 멀티-CDN 구조가 복잡하게 엉켜 매달 청구서가 불어났다. Forem 창업자 Ben Halpern은 이 문제를 해결하기 위해 bunny.net을 도입한 과정을 공유했다.
멀티-CDN의 함정 #
기존 이미지 파이프라인은 여러 CDN 공급자, 동적 리사이징 프록시, AWS S3 원본 스토리지가 섞인 패치워크 구조였다. 사용자가 10MB짜리 JPEG를 업로드해도 Rails 앱이 미리 여러 크기로 변환하지 않는다. 대신 브라우저가 image.jpg?width=800처럼 요청하면 그때 동적으로 변환해 서빙한다.
이론적으로는 효율적이지만 실제 운영에서는 세 가지 문제가 겹쳤다.
스크래퍼 트래픽 과금. RSS 리더, 검색 크롤러, 공격적인 스크래퍼가 캐시를 우회하거나 처음 보는 파라미터 조합으로 요청하면 원본 스토리지에서 이미지를 다시 가져와 변환해야 했다. 처리 비용이 순식간에 치솟는다.
클라우드 egress 요금. CDN 캐시 미스가 발생할 때마다 AWS S3에서 이미지를 가져오는데, 이 egress 비용이 쌓이면 적잖은 부담이 된다.
변환 요금 구조. 프리미엄 이미지 CDN 대부분이 "처리된 이미지 수 기준" 과금을 적용하는데, 수백만 개의 포스트와 아바타를 처리하다 보면 카운트가 기하급수적으로 늘어난다.
Photo by Kindel Media on Pexels
bunny.net으로의 전환 #
Ben은 개인 프로젝트에서 오랫동안 bunny.net을 써왔다고 밝혔다. 엔터프라이즈 클라우드 벤더 특유의 복잡한 요금제와 두꺼운 문서 대신, 개발자 친화적인 인터페이스와 합리적인 가격 구조가 마음에 들었다고 한다.
DEV 규모에서 결정적이었던 건 Bunny Optimizer와 Edge Scripting 두 가지 조합이었다.
Bunny Optimizer와 Perma-Cache #
Bunny Optimizer는 URL 파라미터(?width=600&height=300&crop=1:1)만 붙이면 동적 리사이징, 크롭, 압축을 처리해준다. 브라우저의 Accept 헤더를 읽어 WebP나 AVIF로 자동 변환하며, 원본 대비 파일 크기를 최대 80%까지 줄인다.
여기에 Perma-Cache가 핵심 무기가 됐다. 일반적인 CDN은 자주 쓰이지 않는 이미지 변형 버전을 캐시에서 제거하는데, 이때 다음 요청에서 원본 스토리지를 다시 참조하며 egress 비용이 발생한다. Perma-Cache는 한 번 처리된 이미지 변형본을 Bunny Storage에 영구 저장한다. AWS 원본을 다시 건드릴 일이 없어지면서 egress 비용이 사실상 사라졌다.
Edge Scripting: 엣지에서 돌아가는 TypeScript #
이미지를 얼마나 빠르게 서빙하느냐와 별개로, "어떤 크기로 서빙할지"를 Rails 앱에서 매번 결정해야 한다면 코드가 복잡해진다. Edge Scripting은 Deno와 V8 기반으로 CDN 엣지에서 JavaScript/TypeScript를 직접 실행할 수 있는 기능이다.
요청이 Optimizer에 닿기 전에 미들웨어가 가로채서 파라미터를 재작성할 수 있다. Rails 앱은 원본 URL만 넘기고, 나머지 최적화 판단은 엣지에서 처리하는 구조다.
Forem의 이미지 최적화 아키텍처 #
Forem은 처음부터 이미지 파이프라인을 교체 가능한 구조로 설계해왔다. Rails 뷰에서 이미지를 렌더링할 때는 Images::Optimizer 서비스를 거친다.
<%= Images::Optimizer.call(Settings::General.favicon_url, width: 32) %>Images::Optimizer는 환경 변수 IMAGE_OPTIMIZATION_PROVIDER 값에 따라 bunny.net, Cloudflare, Fastly 등 서로 다른 URL 포맷으로 라우팅하는 전략 패턴으로 구현돼 있다.
module Images
class Optimizer
def self.call(url, options = {})
return url if url.blank?
case provider
when :bunny
BunnyProvider.call(url, options)
when :cloudflare
CloudflareProvider.call(url, options)
when :fastly
FastlyProvider.call(url, options)
else
url
end
end
def self.provider
ENV.fetch("IMAGE_OPTIMIZATION_PROVIDER", "bunny").to_sym
end
end
endForem을 자체 호스팅하는 커뮤니티는 환경 변수 하나만 바꿔서 원하는 CDN을 연결할 수 있다.
엣지에서의 스마트 처리 #
Rails가 생성하는 URL이 항상 완벽하지는 않다. 레거시 포스트에 width 파라미터 없는 외부 URL이 박혀 있을 수도 있고, 템플릿이 파라미터를 빠뜨릴 수도 있다. Edge Script는 이런 경우를 CDN 레이어에서 일괄 처리하는 안전망 역할을 한다.
export default async function handleRequest(request: Request) {
const url = new URL(request.url);
if (url.pathname.startsWith('/uploads/')) {
const isAvatar = url.pathname.includes('/avatars/');
const isThumbnail = url.pathname.includes('/thumbnails/');
const hasWidth = url.searchParams.has('width');
if (isAvatar && !hasWidth) {
url.searchParams.set('width', '150');
url.searchParams.set('height', '150');
url.searchParams.set('crop', '1:1');
} else if (isThumbnail && !hasWidth) {
url.searchParams.set('width', '400');
}
url.searchParams.set('auto', 'format');
return fetch(url.toString(), request);
}
return fetch(request);
}이 스크립트는 GitHub Actions를 통해 자동 배포된다. 최적화 규칙을 바꾸거나 새 레이아웃을 추가할 때 커밋 하나만 푸시하면 수 초 안에 전 세계 엣지에 반영된다.
또 하나의 성과는 이미지 로드 실패 처리다. 기존에는 외부 이미지가 삭제되거나 마이그레이션 중 경로가 깨지면 브라우저에 깨진 이미지 아이콘이 나타났다. 이제는 엣지 스크립트가 4xx/5xx 응답을 감지해 "이미지를 불러올 수 없습니다"라는 커스텀 플레이스홀더로 교체한다. Rails 앱에 onError 핸들러를 모든 <img> 태그에 달 필요가 없어졌다.
다음 단계: 동영상 #
이미지 파이프라인 정비는 1단계다. HLS/DASH 어댑티브 스트리밍, 다해상도 트랜스코딩이 필요한 동영상 서빙은 이미지보다 훨씬 복잡하다. Ben은 bunny.net 동영상 스트리밍 제품도 이미지 파이프라인과 같은 철학으로 만들어져 있어 다음 단계로 자연스럽게 이어질 것 같다고 했다.
이 글은 위 출처를 바탕으로 한국 독자를 위해 재작성한 기사입니다. 원문의 사실과 수치에 근거하며, 별도의 견해를 포함하지 않습니다.

