Cloudflare Images Transformations으로 이미지 최적화를 엣지에 맡기기

Cloudflare Images Transformations으로 이미지 최적화를 엣지에 맡기기

블로그나 커머스 사이트를 운영해 보신 분이라면 이미지 때문에 골머리를 앓아본 경험이 한 번쯤 있으실 텐데요. 원본은 4000픽셀짜리인데 썸네일은 300픽셀이면 충분하고, 어떤 브라우저는 AVIF를 좋아하는데 어떤 브라우저는 WebP만 받아먹고, 심지어 같은 이미지를 카드용과 히어로용으로 다르게 잘라야 하는 경우도 있죠. 😅

예전에는 이런 작업을 하려면 Sharp 같은 라이브러리로 직접 변환 파이프라인을 짜거나, imgix 같은 별도의 이미지 CDN을 붙이는 게 일반적이었는데요. Cloudflare는 조금 다른 접근을 제안합니다. 이미 Cloudflare를 통해 서빙 중인 이미지가 있다면, URL 경로에 옵션 몇 개만 끼워 넣어 엣지에서 바로 변환하라는 거죠. 이게 바로 이번 글에서 살펴볼 Cloudflare Images Transformations입니다.

Images Transformations가 뭔가요?

Cloudflare Images는 크게 두 가지 기능으로 나뉘는데요. 하나는 원본 이미지를 Cloudflare에 업로드하고 관리하는 Cloudflare Images이고, 다른 하나는 이미 어딘가에 있는 이미지를 가져와서 변환만 해주는 Images Transformations입니다. 오늘 이야기할 주제는 후자입니다.

Transformations의 핵심 아이디어는 간단합니다. 원본 이미지는 기존 자리(R2, S3, 자체 서버 등)에 그대로 두고, Cloudflare가 중간에서 요청을 가로채 엣지에서 변환해 돌려주는 거예요. 브라우저가 변환된 URL을 요청하면 Cloudflare는 먼저 캐시를 확인합니다. 이미 만들어둔 버전이 있으면 바로 돌려주고, 없으면 원본을 가져와 옵션대로 변환한 뒤 캐시에 넣어두고 서빙합니다.

덕분에 원본 스토리지는 그대로 두고도 반응형 이미지, 포맷 네고시에이션, AI 기반 크롭 같은 기능을 공짜로 얻을 수 있는데요. 특히 원본을 Cloudflare R2에 올려두면 스토리지 비용도 절약되고 이그레스 요금도 나오지 않아 궁합이 잘 맞습니다.

URL 문법

Transformations의 매력은 설정이랄 게 거의 없다는 점입니다. 다음과 같은 URL 규칙만 기억하면 되거든요.

변환 URL 구조
https://<도메인>/cdn-cgi/image/<옵션들>/<원본 이미지 경로>

/cdn-cgi/image/ 는 Cloudflare가 예약해둔 고정 경로입니다. 이 경로로 들어오는 요청은 원본 서버로 전달되지 않고 Cloudflare의 이미지 변환 엔진이 가로채요. 그 뒤에 쉼표로 구분된 옵션들을 나열하고, 마지막에 실제 원본 이미지의 경로나 전체 URL을 적습니다.

예를 들어 https://blog.example.com/hero.jpg 라는 원본 이미지가 있다고 해볼까요? 이걸 400픽셀 너비로 줄이고 품질을 75%로 낮추고 싶으면 이렇게 씁니다.

리사이즈와 품질 조정
https://blog.example.com/cdn-cgi/image/width=400,quality=75/hero.jpg

끝입니다. 빌드 스크립트를 돌리지도, 썸네일을 미리 만들어두지도 않습니다. 마크업에서 src 값만 이렇게 바꿔주면 브라우저가 최초로 요청하는 순간 Cloudflare가 400픽셀짜리 최적화된 JPEG를 만들어 돌려주고, 그 다음부터는 캐시에서 곧장 나옵니다.

꼭 알아둬야 할 옵션들

옵션이 꽤 많아 전부 외울 필요는 없고, 실제로 가장 자주 쓰는 것들만 추려보겠습니다.

크기와 핏

width(줄여서 w)와 height(h)로 출력 크기를 지정하는데요. 둘 다 지정하면 fit 옵션으로 비율을 어떻게 맞출지 정해야 합니다.

  • contain — 기본값. 박스 안에 이미지가 전부 들어가도록 축소하며 비율을 유지합니다.
  • cover — 박스를 꽉 채우되 남는 부분은 잘라냅니다. 썸네일에 자주 씁니다.
  • scale-down — 원본보다 크게 키우지 않습니다. 작은 원본을 강제로 확대해 흐릿해지는 사고를 막을 때 유용합니다.
  • pad — 비율을 유지하면서 남는 공간은 배경색으로 채웁니다.
  • crop — 확대 없이 자르기만 합니다.

카드 썸네일을 만들 때는 fit=cover,width=300,height=200 조합이 무난합니다.

포맷 변환

format=auto 한 줄이면 브라우저의 Accept 헤더를 보고 AVIF나 WebP 중 가장 효율적인 포맷으로 자동 변환해 줍니다. 옛날처럼 <picture> 태그로 폴백을 일일이 적어둘 필요가 거의 사라지는 셈이죠.

포맷 자동 변환
/cdn-cgi/image/format=auto,width=800/photo.png

Safari 구버전 같은 변수만 신경 쓰지 않아도 된다면 대부분의 경우 원본 PNG 대비 절반 이하로 용량이 줄어듭니다.

스마트 크롭

정사각형 프로필 이미지를 만드는데 사람 얼굴이 모서리에 잘려버리는 사고, 한두 번 당해보신 적 있으실 거예요. gravity 옵션이 이런 고민을 덜어줍니다.

얼굴을 중심으로 크롭
/cdn-cgi/image/fit=cover,width=300,height=300,gravity=face/avatar.jpg

gravity=face는 얼굴 인식을, gravity=auto는 이미지의 주요 영역(saliency)을 찾아 그 주변을 남기고 잘라냅니다. 상품 이미지에는 auto가, 인물 사진에는 face가 잘 어울립니다.

디바이스 픽셀 비율

레티나 디스플레이 대응을 위한 dpr 옵션도 유용한데요. srcset 에 녹여 쓰면 고해상도 기기에서만 2배 이미지를 받아가게 만들 수 있습니다.

반응형 이미지 마크업
<img
  src="/cdn-cgi/image/width=400,format=auto/hero.jpg"
  srcset="
    /cdn-cgi/image/width=400,dpr=1,format=auto/hero.jpg 1x,
    /cdn-cgi/image/width=400,dpr=2,format=auto/hero.jpg 2x
  "
  alt="히어로 이미지"
/>

효과 조정

blur(0~250), sharpen(0~10), brightness, contrast, saturation 같은 옵션으로 간단한 보정도 할 수 있습니다. 배너 위에 텍스트를 얹을 때 배경을 살짝 흐려서 가독성을 높이는 용도로 많이 씁니다.

배경용 블러 처리
/cdn-cgi/image/width=1600,blur=30,brightness=0.7/banner.jpg

처리 순서가 중요합니다

옵션들은 URL에 적은 순서대로가 아니라 정해진 순서대로 적용됩니다. 회전 → 뒤집기 → 리사이즈 → 크롭 → 효과 순서인데요. 예를 들어 flip=h,rotate=90 이라고 적어도 회전이 먼저 일어나고 그 다음에 뒤집기가 적용됩니다. 머릿속 시나리오와 결과가 어긋난다면 이 순서를 먼저 의심해 보세요.

★ Insight ───────────────────────────────────── 처리 순서가 고정된 덕분에 “이 파라미터 조합은 어떤 결과가 나올까”를 미리 그려볼 수 있습니다. 순서가 자유로운 API보다 디버깅이 오히려 수월한 편이죠. ─────────────────────────────────────────────────

활성화와 요금 구조

Transformations를 쓰려면 Cloudflare 대시보드에서 해당 존(도메인)에 대해 기능을 켜줘야 합니다. Images 메뉴의 Transformations 탭에서 토글 하나로 끝나는데요. 여기서 어떤 오리진의 이미지를 변환 대상으로 허용할지도 함께 지정합니다. 제3자가 우리 존을 통해 남의 이미지를 공짜로 변환해 가는 걸 막기 위한 안전장치죠.

요금은 고유한 변환 조합 단위로 과금됩니다. 같은 원본에 같은 옵션 조합이면 그 달 동안 아무리 많이 호출해도 딱 한 번만 집계됩니다. 캐시 히트는 당연히 공짜고, 캐시 미스도 한 달 안이면 재과금이 되지 않습니다. 따라서 이미지 수 자체보다 옵션 조합의 다양성이 비용에 더 직접적으로 영향을 줍니다. srcset을 10단계로 쪼개는 것보다 4~5단계로 정리하는 게 경제적이라는 뜻이죠.

Workers로 감싸기

URL에 옵션이 그대로 노출된다는 건 누구나 임의 옵션을 붙여 호출할 수 있다는 뜻이기도 한데요. 악의적인 요청이 width=10000,blur=250 같은 극단적 조합을 뿌리면 캐시에 쓰레기 변환본이 쌓여 과금이 늘어날 수 있습니다.

이럴 때는 Cloudflare Workers로 변환 요청을 감싸는 방법이 있습니다. Workers에서 허용 가능한 옵션만 화이트리스트로 걸러 fetch 할 때 cf.image 옵션으로 넘기는 식이죠.

worker.js
export default {
  async fetch(request) {
    const url = new URL(request.url);
    const src = url.searchParams.get("src");
    const width = Math.min(Number(url.searchParams.get("w")) || 800, 1600);

    return fetch(src, {
      cf: {
        image: {
          width,
          format: "auto",
          quality: 80,
          fit: "scale-down",
        },
      },
    });
  },
};

이러면 외부에 노출되는 URL은 /img?src=...&w=400 같은 깔끔한 형태가 되고, 실제 변환 파라미터는 서버 쪽에서 통제됩니다. 악의적 조합을 막는 것은 물론 서명된 URL처럼 한층 더 엄격한 보호도 구현할 수 있어요.

실전에서 쓰는 패턴

실제로 블로그나 커머스에 붙여 쓸 때 제가 자주 쓰는 조합을 몇 가지 정리해 봤는데요. 그대로 복사해서 도메인만 바꿔 써도 무방합니다.

리스트 카드의 썸네일에는 비율 고정 크롭과 자동 포맷을 묶어 씁니다.

카드 썸네일
/cdn-cgi/image/fit=cover,width=400,height=250,format=auto,quality=80/post.jpg

상세 페이지의 히어로 이미지는 원본보다 키우지 않도록 scale-down을 걸어 두는 게 안전합니다.

히어로 이미지
/cdn-cgi/image/fit=scale-down,width=1600,format=auto/hero.jpg

프로필 아바타는 얼굴 중심으로 잘라내고 레티나 대응까지 한 번에 처리합니다.

프로필 아바타 (레티나)
/cdn-cgi/image/fit=cover,width=80,height=80,dpr=2,gravity=face,format=auto/user.jpg

페이지 로딩 중에 보여줄 저해상도 플레이스홀더는 블러를 강하게 먹이면 수십 킬로바이트 수준으로 가벼워집니다.

블러 플레이스홀더
/cdn-cgi/image/width=40,blur=20,format=auto,quality=40/hero.jpg

마치며

Cloudflare Images Transformations는 “이미지 파이프라인은 복잡할수록 위험하다”는 교훈을 잘 체화한 기능 같습니다. 원본은 그대로 두고, 빌드 과정도 건드리지 않고, URL 하나로 엣지에서 필요한 만큼만 변환해 돌려주는 단순함이 매력적이거든요.

블로그, 포트폴리오, 커머스처럼 이미지가 많은 사이트를 운영하신다면 기존 <img> 태그의 src만 바꿔보는 것으로도 체감 성능과 지표(LCP, CLS 등)가 눈에 띄게 개선되는 경우가 많습니다. 반응형 이미지와 AVIF 대응을 한 줄로 해결할 수 있다는 것만으로도 충분히 시도해 볼 가치가 있어 보여요.

Cloudflare 생태계에 관심이 있으시다면 Cloudflare 관련 글 모음도 함께 살펴보세요. 더 자세한 파라미터 레퍼런스는 Cloudflare Images Transformations 공식 문서에서 확인하실 수 있습니다.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord