본문 바로가기
MegazoneCloud
← 전체 글
미분류

[Cloudflare] 정상적인 이미지 URL이 404가 되기까지: Cloudflare 이미지 Transformation 문제 해결기

CLOUDFLARE · IMAGE TRANSFORMATIONS · POSTMORTEM

정상적인 이미지 URL이 404가 되기까지: Cloudflare Transformation 고정룰 문제 해결기

원본 파일은 분명 존재했습니다. 그런데 이미지 변환 실패를 원본으로 우회시키려던 고정 룰은 내부 요청을 재귀적으로 발생시켰고, 결국 정상적인 이미지 URL까지 404로 응답하게 만들었습니다. 이 글은 그 원인을 추적하고, Workers 도입 대신 룰과 Snippet의 역할을 재설계해 해결한 기록입니다.

대상 확장자: JPG · JPEG · PNG핵심 구성: URL Rewrite · Response Header Transform · Snippets장애 증상: 원본 존재 상태에서 404 응답
TL;DR

한 줄 결론

문제는 이미지 변환 실패 자체가 아니라, fallback 표식인 ?image=fail이 Snippet의 서브리퀘스트 이전에 사라지면서 이미지 변환 룰이 다시 실행된 것이었습니다. 최종적으로 URL 쿼리는 외부 fallback 표식으로, 요청 헤더는 내부 서브리퀘스트 표식으로 분리해 재진입을 차단했습니다.

1. 이미지 변환 고정 룰의 목표

이번 룰의 목적은 서비스에서 제공하는 모든 JPG, JPEG, PNG 이미지 요청을 Cloudflare Image Transformations로 통일하고, 클라이언트 환경에 맞는 포맷과 품질로 전달하는 것이었습니다.

구분설계 의도적용 방식
대상 이미지JPG, JPEG, PNG 확장자를 가진 이미지 요청이미지 변환 대상 경로에 대한 Rewrite 조건 구성
정상 요청브라우저 환경에 맞는 이미지 포맷과 품질로 전달format=auto,quality=high
변환 실패오류 화면 대신 원본 이미지를 전달onerror=redirect
핵심 제약fallback 요청이 다시 이미지 변환에 진입하면 안 됨쿼리 표식과 내부 요청 헤더를 조합한 재진입 차단

변환 URL의 기본 구조는 아래와 같습니다. ${1}은 Rewrite 규칙에서 캡처한 원본 이미지 경로를 의미합니다.

/cdn-cgi/image/format=auto,quality=high,onerror=redirect/${1}
CloudflareImage TransformationsURL RewriteResponse Header TransformSnippetsFallback307 Redirect

2. 최초 설계: 변환 실패 시 원본 이미지로 fallback

최초 설계는 변환 실패 시 리다이렉트된 요청에 ?image=fail을 붙이고, 해당 요청만 원본 이미지로 통과시키는 방식이었습니다. 이 표식이 있어야 fallback 요청을 일반 이미지 요청과 구분할 수 있습니다.

INITIAL FLOW · TRANSFORMATION FALLBACK

1
Client request

GET https://a.com/bbb.jpg

↓
2
Cloudflare Rewrite 1 — 이미지 변환 요청으로 변경

조건: 쿼리스트링에 image=fail이 없을 때

/cdn-cgi/image/format=auto,quality=high,onerror=redirect/bbb.jpg

↓
3A
정상 변환: 200 OK

Cloudflare가 format=auto, quality=high이 적용된 변환 이미지를 응답합니다.

3B
변환 실패: 307 Redirect

onerror=redirect가 동작하며 원본 이미지 URL로 fallback합니다.

↓
4
Response Header Transform

307 응답의 Location 헤더를 https://a.com/bbb.jpg?image=fail로 변조합니다.

↓
5
307을 따른 Client request

GET https://a.com/bbb.jpg?image=fail

↓
6
Cloudflare Rewrite 2

조건: 쿼리스트링에 image=fail이 있을 때. 동작: 쿼리스트링을 제거하고 원본 경로로 전달합니다.

↓
7
Cloudflare 원본 이미지 응답

https://a.com/bbb.jpg를 원본 이미지로 반환하는 것이 최초 설계의 목표였습니다.

3. 원본이 있는데도 404가 된 이유

기대와 달리 원본 파일이 존재하는 콘텐츠에서도 404가 다수 발생했습니다. 분석 결과, 실제 원인은 Snippet의 서브리퀘스트와 Rewrite 조건이 결합되면서 발생한 재귀 루프였습니다.

INCIDENT FLOW · RECURSIVE SUBREQUEST

1
307 fallback 요청

GET /bbb.jpg?image=fail

↓
2
Rewrite 2가 쿼리스트링을 먼저 제거

fallback 상태를 나타내던 image=fail 표식이 사라집니다.

↓
3
Snippet이 fetch() 서브리퀘스트 생성

표식이 제거된 /bbb.jpg를 기준으로 내부 요청이 발생합니다.

↓
4
Rewrite 1이 다시 매칭됨

조건상 image=fail이 없으므로 /bbb.jpg는 다시 변환 URL로 변경됩니다.

↻
5
fallback → subrequest → rewrite → transform 반복

이미지 변환과 307 fallback이 재반복되고, 내부 request limit에 도달한 뒤 404가 반환됩니다.

즉, 이 사례의 404는 원본 파일이 없어 발생한 오류가 아니었습니다. fallback 상태가 유실된 요청이 이미지 변환 룰에 재진입하면서 발생한 내부 요청 한도 초과가 원인이었습니다.

4. Workers 대안은 왜 바로 채택하지 않았나

룰만으로는 상태 전달과 재진입 제어가 어려워 보였기 때문에 Workers 기반 이미지 변환도 검토했습니다. Workers는 URL, 요청 헤더, 변환 결과, fallback 응답을 하나의 코드 흐름에서 관리할 수 있다는 장점이 있습니다.

기술적으로 기대한 점

상태 전이와 fallback을 코드로 명시해 쿼리 제거 시점과 서브리퀘스트 재진입을 더 명확하게 제어할 수 있습니다.

검토 과정에서 확인한 제약

기존 Workers와의 라우팅 우선순위, service binding, 고객 코드 변경, 요청 수 기반 비용까지 함께 고려해야 했습니다.

검토 항목확인된 문제영향
기존 Workers Route기존 서비스가 /*와 같은 넓은 경로 방식의 Route를 이미 사용상·하위 Worker 연결과 요청 전달 구조를 설계해야 함
확장자 기반 Route검토 당시 /*.jpg와 같은 확장자 방식 Route는 beta 기능이었음광범위 Route와 이미지 Route의 선후 관계를 안정적으로 판단하기 어려움
Service Binding상위 Worker가 동작한 뒤 하위 이미지 Worker로 넘기려면 binding 설정 필요콘솔 설정뿐 아니라 고객 Worker 코드 수정까지 요구될 수 있음
요청 수 과금계약과 플랜에 따라 Worker 요청 수가 비용으로 연결될 수 있음모든 이미지 요청이 Worker를 통과하면 비용 규모가 커질 가능성

5. 최종 해결안: URL 표식과 내부 헤더 표식을 분리

최종 설계의 핵심은 fallback 상태를 쿼리스트링 하나에만 의존하지 않는 것입니다. 외부 요청을 구분하는 표식과 Snippet 서브리퀘스트를 구분하는 표식을 분리했습니다.

외부 fallback 표식

?image=fail은 307을 따라온 클라이언트 요청이 변환 실패 후의 fallback이라는 사실을 알립니다. 이 표식이 있으면 Rewrite 1은 이미지 변환을 다시 시작하지 않습니다.

내부 서브리퀘스트 표식

Snippet은 image-optimization: remove 요청 헤더를 추가한 뒤 쿼리스트링을 제거합니다. 이후 fetch() 요청도 이 헤더로 변환 룰의 재진입을 차단합니다.

FIXED FLOW · URL MARKER + HEADER GUARD

1
Client request

GET https://a.com/bbb.jpg

↓
2
Rewrite 1 — 이미지 변환 룰

조건 A: image=fail이 없을 것. 조건 B: image-optimization: remove 헤더가 없을 것.

운영 규칙: 이미지 변환 Rewrite는 Rewrite 목록의 마지막에 배치합니다.

↓
3
변환 실패 시 307 응답의 Location 헤더 변조

https://a.com/bbb.jpg?image=fail을 fallback URL로 반환합니다.

↓
4
fallback 요청에서는 Rewrite 1을 건너뜀

image=fail이 존재하므로 이미지 변환 경로로 다시 변경하지 않습니다.

↓
5
Snippet 실행

image-optimization: remove 헤더를 먼저 추가하고, image=fail을 제거한 뒤 fetch()를 호출합니다.

↓
6
서브리퀘스트에서도 Rewrite 1을 건너뜀

쿼리스트링은 없어졌지만 image-optimization: remove 헤더가 남아 있어 변환 룰이 재실행되지 않습니다.

↓
7
Cloudflare 원본 이미지 응답: 200 OK

원본 파일이 존재하는 경우에만 정상 원본 이미지로 fallback하며, 재귀 루프는 발생하지 않습니다.

최종 룰의 정책을 의사 코드로 표현하면

아래 코드는 실제 Cloudflare Rules 언어가 아니라, 최종 구성의 판단 조건과 실행 순서를 설명하기 위한 의사 코드입니다.

// Rewrite 1: 이미지 변환 룰
// 반드시 Rewrite 목록의 마지막에 배치한다.
if (
  query.image !== "fail" &&
  request.headers["image-optimization"] !== "remove"
) {
  rewrite(
    "/cdn-cgi/image/format=auto,quality=high,onerror=redirect" +
    request.path
  );
}

// Response Header Transform: 변환 실패 후 307 응답에만 적용
if (response.status === 307) {
  response.headers["Location"] =
    "https://a.com/bbb.jpg?image=fail";
}

// Snippet: fallback 요청을 원본 요청으로 전환
if (query.image === "fail") {
  request.headers["image-optimization"] = "remove";
  removeQueryParameter("image");
  return fetch(request);
}

운영 중 확인한 추가 규칙

  • ✓ 이미지 변환 Rewrite는 상위에 두었을 때 기대대로 실행되지 않는 사례가 있었으므로, Rewrite 목록의 마지막 룰로 배치했습니다.
  • ✓ Rewrite는 요청 생명 주기 동안 한 번만 실행된다고 가정하면 안 됩니다. 특히 Snippet 또는 Worker의 fetch()는 새로운 내부 요청 경로를 만들 수 있습니다.
  • ✓ fallback 요청을 식별하는 URL 표식과 내부 처리 단계를 식별하는 헤더 표식은 서로 다른 역할으로 분리하는 것이 안전합니다.

6. 검증과 운영 체크리스트

문제 해결 후에는 “404가 사라졌는가”만 확인해서는 부족합니다. 정상 변환, fallback, 실제 원본 누락, 서브리퀘스트 재진입까지 각각 분리해 검증해야 합니다.

테스트 시나리오기대 결과확인 포인트
정상 JPG/JPEG/PNG 요청변환 이미지가 200으로 응답format=auto, quality=high 적용 여부
의도적으로 변환 실패를 유도한 요청307 → fallback 요청 → 원본 이미지 200Location에 ?image=fail이 추가되는지
?image=fail을 가진 fallback 요청Rewrite 1을 건너뛰고 Snippet으로 진입이미지 변환 경로가 다시 생성되지 않는지
Snippet의 fetch() 서브리퀘스트원본 이미지 요청으로 종료image-optimization: remove 헤더로 Rewrite 1이 차단되는지
실제로 존재하지 않는 원본 이미지정상적으로 404fallback이 없는 원본 파일을 복구하는 것처럼 보이지 않는지
원본 URL에 기존 쿼리스트링이 있는 경우기존 쿼리를 보존한 채 fallback 표식 추가?와 & 처리, 캐시 키 정책

마무리: 404는 파일이 없어서만 발생하지 않는다

이번 장애에서 원본 이미지 URL은 정상적이었습니다. 하지만 Edge에서 동작하는 여러 기능이 Rewrite → Snippet → subrequest → Rewrite 순서로 연결되면서, fallback 상태가 사라진 요청이 이미지 변환 룰에 재진입했습니다.

결국 해결의 핵심은 변환 기능을 바꾸는 것이 아니라, 요청이 어느 단계에 있는지 식별할 수 있는 상태를 명확하게 전달하는 것이었습니다. URL 쿼리는 외부 fallback 상태를, 요청 헤더는 내부 서브리퀘스트 상태를 맡게 했고, 이미지 변환 Rewrite를 마지막 룰로 배치해 재진입을 차단했습니다.

이미지 최적화는 겉으로 보기에는 단순한 URL 변환처럼 보이지만, fallback과 서브리퀘스트가 더해지는 순간 요청 흐름 자체를 설계해야 하는 문제로 바뀝니다. 정상 URL이 404가 되는 상황을 막기 위해서는, 규칙 하나보다 전체 Edge 요청 생명 주기를 먼저 그려보는 것이 중요합니다.

참고 자료

  1. Cloudflare Developers, Transform via URL — onerror=redirect의 동작과 적용 제약을 확인할 수 있습니다. 공식 문서의 내용은 라이선스 준수를 위해 요약·재서술했습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다