DEV, bunny.net으로 이미지 파이프라인 대혼란 종식! 비용 절감과 성능, 개발자 경험까지 잡은 이야기
2026. 6. 19.
DEV, bunny.net으로 이미지 파이프라인 대혼란 종식! 비용 절감과 성능, 개발자 경험까지 잡은 이야기
안녕하세요, 10년 차 개발자로서 DEV와 Forem의 오픈소스 여정을 지켜봐 오셨다면, 저희가 웹 성능에 얼마나 강박적으로 집착해왔는지 잘 아실 겁니다. 예전 코드랜드(Codeland) 컨퍼런스에서는 웹사이트를 일본에서 바이럴이 될 정도로 빠르게 만드는 방법에 대해 강연하기도 했죠. 엣지 캐싱의 메커니즘을 파고들어 페이지 로드 시간을 거의 즉각적으로 유지했던 경험을 공유한 바 있습니다.
이 영상에서 당시 강연을 확인하실 수 있습니다.
저희의 핵심 철학은 언제나 단순했습니다. 아키텍처를 최대한 간결하게 유지하고, 엣지에서 공격적으로 캐싱하며, Rails 모놀리스(Forem)는 본연의 역할에 집중하도록 하는 것입니다. 수년간 Fastly는 HTML 엣지 캐싱을 훌륭하게 처리해 주었습니다. 덕분에 대부분의 페이지 요청은 저희 Puma 서버에 닿을 필요조차 없었고, 이는 RAM 사용량을 낮추고 응답 시간을 밀리초 단위로 유지하는 데 큰 도움이 되었습니다. DEV의 모든 문서 콘텐츠는 여전히 Fastly를 통해 제공되고 있고요.
하지만 정적 HTML 엣지 캐싱이 이미 잘 알려진 문제였다면, 사용자 업로드 미디어는 완전히 다른 종류의 야수였습니다. 제가 실무에서 다양한 CDN을 다뤄봤을 때, 엣지 캐싱의 중요성을 늘 절감했지만, 사용자 업로드 미디어는 차원이 다른 문제였습니다.
DEV가 성장하면서 저희는 이미지의 홍수에 빠져들었습니다. 모든 게시물 커버, 사용자 아바타, 댓글 스크린샷, 챌린지 배너 등 커뮤니티가 업로드하는 고해상도 자산들로 가득했죠. 전 세계 수십억 개의 이미지를 제공하면서도 페이지 크기를 가볍게 유지하려다 보니, 결국 조용하지만 치명적인 스케일링 함정에 빠지게 되었습니다. 복잡하게 얽힌 멀티 CDN 미디어 파이프라인, 막대한 클라우드 이그레스(Egress) 비용, 그리고 눈덩이처럼 불어나는 월별 청구서가 그 결과였습니다.
여기서는 저희가 어떻게 멀티 CDN의 혼란을 끝내고, 미디어 아키텍처를 단순화하며, 상당한 비용을 절감하고, bunny.net의 엣지 스크립팅을 활용해 더 스마트하고 빠른 이미지 제공 파이프라인을 구축했는지 그 이야기를 들려드리려 합니다.
멀티 CDN의 혼돈과 청구서의 현실
저희가 왜 이런 변화를 선택했는지 이해하려면, 예전의 이미지 파이프라인이 어떤 모습이었는지 먼저 살펴봐야 합니다.
오랜 기간 동안 저희의 미디어 스택은 그야말로 누더기 같았습니다. 플랫폼의 다양한 부분에 따라 여러 CDN을 사용했고, 동적 리사이징을 위한 이미지 프록싱 서비스, 그리고 AWS S3와 같은 클라우드 스토리지에 원본 자산이 그대로 놓여 있었죠.
사용자가 10MB JPEG 파일을 게시물 커버로 업로드하면, 저희 Rails 앱은 이를 수십 가지 다른 크기로 미리 처리하지 않습니다. 대신, 실시간(on-the-fly) 이미지 변환에 의존했죠. 이론적으로는 훌륭한 방식입니다. 브라우저가 image.jpg?width=800을 요청하면, 동적 이미지 최적화 도구가 이를 리사이징하고 WebP 또는 AVIF로 변환하여 제공하는 식입니다.
하지만 실제로 이러한 설정은 규모가 커질수록 경제성과 메커니즘 측면에서 가혹해졌습니다. 특히 현대 웹 트래픽의 현실을 고려하면 더욱 그렇습니다.
- 스크래퍼 및 트래픽 세금: 공개된 웹에서는 단순히 인간 사용자에게만 서비스를 제공하는 것이 아닙니다. RSS 리더, 검색 크롤러, 고빈도 스크래퍼들의 끊임없는 공격을 받습니다. 전통적인 설정은 이러한 트래픽을 우아하게 처리할 만큼 똑똑하지 못했습니다. 공격적인 스크래퍼들이 캐싱되지 않은 변형을 요청하거나 표준 쿼리 파라미터를 우회하여 파이프라인이 자산을 반복적으로 재가져오고 재처리하도록 강요했고, 결국 컴퓨팅 비용이 천정부지로 치솟았습니다.
- 이그레스 비용: 캐시 미스(대부분 위에서 언급된 트래픽 패턴으로 인해 발생)로 인해 이미지 최적화 도구가 클라우드 스토리지 오리진에서 원본 이미지를 가져와야 할 때마다, 저희는 막대한 클라우드 이그레스 비용을 지불해야 했습니다.
- 변환 처리 비용: 많은 프리미엄 이미지 CDN은 "이미지 처리 1천 건당" 과금하거나 복잡하고 징벌적인 사용량 티어를 적용합니다. 수백만 개의 활성 게시물과 수백 개의 사용자 지정 크기 아바타가 있는 피드를 스크롤하는 사용자들이 있다면, 이러한 변환 횟수는 기하급수적으로 늘어납니다.
- 멀티 CDN 마찰: HTML 캐싱과 이미지 전송/최적화를 위해 서로 다른 제공업체를 운영하는 것은 엄청난 운영 오버헤드를 야기했습니다. 복잡한 헤더 구성, CORS 문제, 그리고 라우팅 규칙이 여러 YAML 파일과 대시보드에 흩어져 있었습니다.
미디어 청구서는 눈덩이처럼 불어나서 엄청나게 비쌌고, 저희 파이프라인이 예측 불가능한 트래픽 스파이크를 원활하게 처리하지 못하는 이유를 디버깅하는 데 너무 많은 시간을 낭비했습니다. 수많은 프로젝트에서 이미지 파이프라인을 설계하며 이 문제에 직면할 때마다 골머리를 앓았죠. 빠르고 안정적이며, 고도로 구성 가능하고, 무엇보다 경제적으로 지속 가능한 솔루션이 절실했습니다.
우리가 bunny.net으로 갈아탄 이유
사실 저는 몇 년 동안 여러 개인 프로젝트에서 bunny.net을 사용해왔습니다. 간단한 사이드 앱을 만들거나 새로운 개념을 테스트할 때마다 항상 bunny.net으로 돌아오곤 했습니다. 정말 잘 설계된 플랫폼에 합리적이고 직관적인 제품들을 제공하기 때문이죠. 전통적인 클라우드 벤더들의 복잡하고 거대한 엔터프라이즈 기능 집착은 없고, 깔끔하면서도 직관적인 개발자 경험(DX)을 제공하여 '그냥 작동'합니다. 이러한 직접적인 경험 덕분에 저는 DEV와 함께 원활하게 스케일링할 수 있는 플랫폼이라는 확신이 있었습니다.
Forem에 bunny.net을 도입하기로 결정한 결정적인 이유는 단순히 대역폭 절감 효과(프리미엄 엔터프라이즈 CDN이 청구하는 비용의 일부분으로 대역폭 청구서를 대폭 줄인 것은 엄청난 성과였습니다) 때문만은 아니었습니다. 그들의 제품 생태계가 **Bunny Optimizer**와 **Edge Scripting**의 조합을 통해 우리의 특정 아키텍처적 문제점을 얼마나 아름답게 해결했는지 때문이었습니다.
1. Bunny Optimizer와 Perma-Cache
Bunny Optimizer는 완전 관리형 동적 이미지 변환 API 역할을 합니다. 간단한 URL 쿼리 파라미터(예: ?width=600&height=300&crop=1:1)를 추가하기만 하면, Optimizer가 실시간으로 크기 조정, 자르기, 자동 압축을 처리해 줍니다. 브라우저의 Accept 헤더를 기반으로 WebP나 AVIF 같은 차세대 포맷을 자동으로 협상하여, 육안으로 품질 저하 없이 파일 크기를 최대 80%까지 줄여줍니다.
하지만 진정한 마법의 재료이자 스크래퍼 트래픽에 대한 우리의 궁극적인 무기는 바로 Perma-Cache였습니다.
일반적으로 CDN 엣지 서버가 자주 접근하지 않는 이미지 변형을 제거하면, 다음 요청은 원본(클라우드 스토리지)까지 다시 이동하여 가져오고 재최적화해야 하며, 이는 더 많은 이그레스 비용을 유발합니다. Perma-Cache는 최적화된 이미지 변형을 Bunny Storage에 영구적으로 복제하여 이 문제를 해결합니다.
이미지가 한 번 처리되면 엣지에 영구적으로 저장됩니다. 다시는 AWS 오리진에 접근할 필요가 없으므로, 백엔드는 불규칙한 트래픽으로부터 보호받고 클라우드 스토리지 이그레스 비용은 하룻밤 사이에 거의 사라졌습니다.
2. Edge Scripting: TypeScript 네이티브 제어
Bunny Optimizer가 이미지 크기 조정에 필요한 강력한 기능을 제공했지만, 이미지를 제공하는 방식에 대한 세밀한 제어가 필요했습니다. 복잡한 URL 생성 로직으로 Rails 뷰를 오염시키고 싶지 않았고, 사용자(또는 봇)가 거대한 원본 이미지를 다운로드하는 것을 막고 싶었습니다.
바로 이 지점에서 Edge Scripting이 등장했습니다.
Deno와 V8을 기반으로 구축된 Edge Scripting은 JavaScript와 TypeScript 코드를 엣지에서 직접 실행하여, 가볍고 타입 안전한 미들웨어를 밀리초 단위로 실행할 수 있게 해줍니다. 이는 사용자 정의 이미지 프록시나 복잡한 Rails 컨트롤러 라우팅의 필요성을 완전히 대체했습니다.
내부 살펴보기: Forem의 플러그형 Images::Optimizer 서비스
Forem의 코드베이스를 살펴보면, 저희가 항상 이미지 파이프라인을 플러그형으로 설계했음을 알 수 있습니다. 특정 CDN의 쿼리 파라미터를 뷰에 하드코딩하고 싶지 않았기 때문이죠. 템플릿이 게시물 커버 이미지를 렌더링하려면, 통합 헬퍼를 호출하여 Images::Optimizer 서비스에 위임하는 방식입니다.
# app/views/layouts/application.html.erb
<%= Images::Optimizer.call(Settings::General.favicon_url, width: 32) %>
app/services/images/optimizer.rb 내부에서는 간단한 전략 패턴을 사용합니다. Optimizer 클래스는 width, height, fit, gravity와 같은 표준화된 파라미터를 활성 CDN 제공업체에 필요한 특정 URL 형식으로 매핑하는 라우터 역할을 합니다.
역사적으로 Forem은 Fastly, Cloudflare, Cloudinary와 같은 제공업체를 지원했으며, bunny.net을 추가하는 과정은 놀랍도록 간단했습니다. 저희 Rails 서비스가 이러한 멀티 CDN 라우팅을 어떻게 처리하는지 살펴보겠습니다. 제가 직접 이 코드를 들여다보고 bunny.net 프로바이더를 추가했을 때, 기존 아키텍처의 유연함에 다시 한번 감탄했습니다.
# app/services/images/optimizer.rb
module Images
class Optimizer
def self.call(url, options = {})
return url if url.blank?
# Select the provider strategy based on our environment configuration
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
end
각 제공업체는 자체 URL 재작성 전략을 구현합니다. 예를 들어, 저희 bunny.net 제공업체는 Bunny Optimizer가 파싱하는 표준 쿼리 스트링을 생성합니다.
# app/services/images/bunny_provider.rb
module Images
class BunnyProvider
def self.call(url, options = {})
uri = URI.parse(url)
query_params = []
query_params << "width=#{options[:width]}" if options[:width]
query_params << "height=#{options[:height]}" if options[:height]
query_params << "crop=#{options[:crop]}" if options[:crop]
query_params << "auto=format"
uri.query = [uri.query, query_params.join("&")].compact.join("&")
uri.to_s
end
end
end
이러한 분리된 아키텍처는 Forem과 같은 오픈소스 프로젝트에 매우 적합합니다. 자체 호스팅 커뮤니티는 IMAGE_OPTIMIZATION_PROVIDER 환경 변수만 변경하여 원하는 CDN을 구성할 수 있습니다.
그러나 Rails 앱이 이러한 최적화된 URL을 생성하더라도, 저희는 흥미로운 운영상의 문제에 직면했습니다. 템플릿이 width 파라미터를 전달하는 것을 잊거나, 레거시 게시물에 원본 외부 URL이 포함되어 있으면 어떻게 될까요?
Rails 측 URL 생성에만 의존한다면, 어떤 폴백(fallback)이나 파싱되지 않은 URL이라도 여전히 무겁고 최적화되지 않은 이미지 로드를 유발할 것입니다. 바로 이 지점에서 저희의 Edge Scripting 전략이 네트워크 경계에서 전역 안전망(global safety net) 역할을 하기 위해 나섰습니다.
심층 분석: 엣지에서의 스마트 다운사이징
Edge Scripting을 사용하면 CDN 레이어에서 이미지 요청을 바로 가로채고, 요청이 옵티마이저나 스토리지에 도달하기 전에 사용자 지정 비즈니스 로직을 적용할 수 있습니다.
예를 들어, 사용자 프로필 아바타와 피드 썸네일은 원본 업로드가 무엇이었든, 또는 어떤 쿼리 파라미터가 요청되었든 상관없이 실제로 필요한 크기보다 크게 제공되지 않도록 하고 싶었습니다. 클라이언트가 원본의 최적화되지 않은 아바타 URL을 요청하면, 저희 엣지 스크립트가 자동으로 이를 가로채 컨텍스트를 확인하고 요청을 재작성하여 엄격한 최대 너비를 강제하고 WebP 압축을 적용합니다.
다음은 간소화된 예시 코드입니다.
// A lightweight middleware script running on bunny.net's Edge
export default async function handleRequest(request: Request) {
const url = new URL(request.url);
// Intercept requests to our user-uploaded uploads path
if (url.pathname.startsWith('/uploads/')) {
const isAvatar = url.pathname.includes('/avatars/');
const isThumbnail = url.pathname.includes('/thumbnails/');
// Check if the request already has optimization parameters
const hasWidth = url.searchParams.has('width');
if (isAvatar && !hasWidth) {
// Dedicatedly downsize all avatar requests to a max of 150px
url.searchParams.set('width', '150');
url.searchParams.set('height', '150');
url.searchParams.set('crop', '1:1');
} else if (isThumbnail && !hasWidth) {
// Enforce a strict mobile-friendly limit on thumbnails
url.searchParams.set('width', '400');
}
// Ensure automatic next-gen format negotiation (WebP/AVIF) is active
url.searchParams.set('auto', 'format');
// Fetch the optimized asset from bunny.net's CDN pipeline
return fetch(url.toString(), request);
}
return fetch(request);
}
이것은 엄청나게 강력한 기능입니다. 이미지 전송에 대한 "계산적 사고"를 전적으로 엣지로 오프로드하기 때문이죠. 저희 Rails 애플리케이션은 반응형 이미지 중단점(breakpoint)을 추적하거나 무겁고 복잡한 마크업을 생성할 필요가 없습니다. 그저 논리적인 자산 URL을 요청하면, 엣지 스크립트가 클라이언트 헤더와 컨텍스트에 따라 나머지를 동적으로 처리합니다.
더 나아가, 저희는 이를 표준 개발 워크플로우에 통합했습니다. GitHub Actions와 세분화된 개인 액세스 토큰을 결합하여 이러한 엣지 스크립트를 자동으로 관리하고 배포합니다. 최적화 규칙을 조정하거나 새로운 레이아웃(예: 빌보드 이미지 최적화 또는 챌린지 페이지의 해상도 조정)에 대한 지원을 추가하고 싶을 때, 커밋을 푸시하기만 하면 CI가 실행되고 새로운 엣지 로직이 몇 초 만에 전 세계적으로 적용됩니다.
우아한 실패 처리: 깨진 이미지 대신 스마트 폴백
라우팅 및 크기 조정 외에도 엣지에서 코드를 실행하는 것은 사용자 경험(UX) 측면에서 엄청난 승리를 가져다주었습니다. 바로 누락되거나 깨진 자산을 우아하게 처리하는 기능이죠.
대규모 커뮤니티 생태계에서는 엣지 케이스가 발생합니다. 사용자가 연결했던 외부 이미지를 삭제하거나, 오래된 업로드 경로가 마이그레이션 중에 깨지거나, 잘못된 요청이 통과할 수도 있습니다. 전통적으로 이미지가 로드되지 않거나 404/500 오류를 반환하면, 브라우저는 레이아웃을 망가뜨리고 전체 사이트가 깨진 것처럼 보이게 하는 불쾌한 "깨진 이미지" 아이콘을 표시합니다.
Edge Scripting을 사용하면 이러한 실패를 중간에 잡아낼 수 있습니다. 저희의 오리진 또는 스토리지가 오류 상태 코드를 반환하면, 엣지 스크립트가 응답을 가로채고 "이미지를 사용할 수 없습니다"라고 표시하거나 저희 UI 테마와 일치하는 아름답게 스타일링된 사용자 정의 플레이스홀더 이미지로 원활하게 재작성하여 제공합니다.
Rails 애플리케이션의 모든 <img> 태그에 복잡하고 무거운 JavaScript 이벤트 리스너(onError)를 작성하는 대신, 네트워크 계층에서 기본적으로 이를 처리하는 셈입니다. 애플리케이션 계층은 폴백 로직에 대해 전혀 생각할 필요가 없으며, 사용자들은 어떤 상황이 발생하더라도 일관되고 끊어지지 않는 시각적 경험을 얻게 됩니다.
향후 고려 사항
이미지 파이프라인을 안정화하고 최적화하는 것은 1단계에 불과했습니다. DEV와 Forem이 앞으로 어떻게 계속 발전할지 내다보면, 동영상은 다음 논리적인 과제입니다.
동영상 전송은 악명이 높을 정도로 복잡합니다. 적응형 비트레이트 스트리밍(HLS/DASH), 다중 해상도 트랜스코딩, 특수 스토리지, 최적화된 동영상 플레이어가 필요하죠. 레거시 아키텍처에서는 일반적으로 또 다른 파편화된 비싼 타사 동영상 프로세서와 복잡한 통합을 구축해야 했습니다.
bunny.net의 미디어 인프라에서 얻은 성공을 바탕으로, 동영상과 관련하여 앞으로 어떻게 나아갈지에 대한 우리의 확실한 첫 번째 선택은 bunny.net입니다. 그들의 통합 플랫폼 접근 방식은 동영상 스트리밍으로도 직접 확장되며, 이미지 옵티마이저와 동일한 합리적이고 개발자 우선적인 철학을 가진 제품들을 제공합니다. 약탈적인 데이터 전송 비용 없이 예측 가능하게 인프라가 스케일링될 것을 신뢰할 수 있으므로, 엣지 아키텍처를 확장하여 차세대 리치 미디어를 처리하는 것은 엄청난 인프라 정비가 아닌 자연스러운 발전처럼 느껴집니다.
마지막 생각
개발자로서 우리는 종종 데이터베이스 쿼리를 최적화하거나, Ruby 코드를 리팩터링하거나, 서버 구성을 미세 조정하는 데 집중합니다. 하지만 때로는 가장 큰 성과는 네트워크 탭 바로 거기에 있습니다.
이그레스 비용과 부풀려진 미디어 전송은 성장하는 플랫폼에 대한 조용한 세금과 같습니다. 엣지 네이티브의 개발자 친화적인 플랫폼인 bunny.net으로 전환함으로써, 저희는 아키텍처를 단순화하고 페이지 로드 속도를 높였으며, 그 과정에서 많은 비용을 절감할 수 있었습니다.
만약 미디어가 많은 플랫폼을 운영하거나 Forem과 같은 오픈소스 커뮤니티 소프트웨어를 구축하고 있다면, 자신을 위해 다음을 해보세요. CDN 청구서를 자세히 살펴보고, 클라우드 스토리지 이그레스 비용을 확인하며, 당신과 함께 성장할 수 있도록 구축된 플랫폼에 그 부담 중 일부를 넘길 수 있는지 알아보세요. 당신의 예산(그리고 사용자들)이 고마워할 것입니다.
즐거운 코딩 되세요! ❤️
원문: https://dev.to/devteam/how-we-saved-big-and-simplified-our-image-pipeline-adopting-bunnynet-on-dev-3d53 수집일: 2026-06-19 00:32:45