Cloudflare Workers 11월 17일(2026-11-17) 4대 기본값 전환 해부: 신규 모듈 레지스트리·request.signal·Cache-Control 보존과 pipeTo 역방향 에러 전파 점검하기
11월 17일(compatibility_date 2026-11-17)부터 Cloudflare Workers에 기본 적용되는 4대 런타임 변경(new_module_registry, enable_request_signal, cache_option_preserves_cache_control, pipe_to_backward_propagation)을 정리한다. Top-Level await require() 거부, 오리진 Cache-Control 보존, pipeTo() 역방향 Reject 전파와 로컬 Python 6대 검증을 살펴본다.
Cloudflare Workers 2026-11-17 4대 기본 호환성 플래그 전환 아키텍처 요약 다이어그램 (개념 일러스트 SVG)
2026년 11월 17일(compatibility_date = "2026-11-17")부터 Cloudflare Workers 런타임의 4대 호환성 플래그가 동시에 기본값으로 전환된다 (Workers will use the new module registry by default on November 17, Request.signal will be enabled by default in Workers on November 17, Compatibility flags 공식 문서). 기존 배포된 Worker는 compatibility_date를 올리지 않는 한 이전 동작을 유지하지만, 11월 17일 이후 신규 프로젝트를 생성하거나 wrangler.jsonc·wrangler.toml·cloudflare.config.ts의 호환성 날짜를 2026-11-17 이상으로 갱신하면 **① 신규 모듈 레지스트리(new_module_registry), ② 클라이언트 연결 해제 시그널(enable_request_signal), ③ 오리진 Cache-Control 보존(cache_option_preserves_cache_control), ④ 스트림 역방향 에러 전파(pipe_to_backward_propagation)**가 별도 플래그 없이 즉시 적용된다.
핵심 판단 기준을 먼저 요약하면, 2026-11-17 이상으로 호환성 날짜를 올릴 때 가장 주의해야 할 파괴적 변경은 new_module_registry에서 Top-Level await를 포함한 모듈 그래프를 require()로 로드하면 이미 평가가 끝난 모듈이라도 즉시 실패한다는 점과, fetch(url, { cache: "no-cache" })가 더 이상 오리진의 Cache-Control 헤더를 덮어쓰지 않으므로 강제 캐시 정책 재정의가 필요하면 cf: { cacheControl: "no-cache" }로 이관해야 한다는 점이다. 또한 request.signal이 기본 활성화되더라도 하위 fetch() 요청으로 시그널이 자동 전달(request_signal_passthrough)되지는 않으므로 명시적으로 { signal: request.signal }을 넘겨야 하며, pipeTo() 대상 스트림이 닫히거나 에러를 낼 때 기존에는 Promise가 조용히 이행(Resolve)되거나 무한 대기했지만 이제는 Reject되므로 try/catch 예외 처리가 필수다. 이 글은 Cloudflare 공식 체인지로그 2건과 Workers Compatibility flags 레퍼런스를 교차 검증해 정리한 글이며, 실제 프로덕션 엣지 트래픽을 대상으로 측정한 지연 시간 벤치마크가 아니라 공식 런타임 명세와 로컬 Python 6대 동작 시뮬레이션 검증을 기준으로 설명한다.
1. 2026년 11월 17일 기본 활성화되는 4대 호환성 플래그 한눈에 보기
Cloudflare Workers는 날짜 기반 호환성 체계(compatibility_date)를 통해 런타임의 하위 호환성 파괴 변경(Breaking Changes)을 관리한다. 2026-11-17 날짜에 묶여 있는 4개 플래그의 활성화·비활성화(Opt-out) 이름과 핵심 동작 변화는 아래 표와 같다 (Compatibility flags — Flags history).
| 영역 및 기본 전환일 | 활성화 플래그 / 비활성화(롤백) 플래그 | 2026-11-17 이전 기본 동작 (Legacy) | 2026-11-17 이후 기본 동작 (New Default) |
| 1. 모듈 레지스트리 (JS/TS Workers 전용) | new_module_registry /legacy_module_registry | 레거시 레지스트리 사용, 잘못된 import attributes 무시 가능, 구형 require() 플래그 적용 | URL 기반 모듈 해석( ?query·#hash별 별도 인스턴스), import.meta.* 제공, Top-Level await 포함 그래프 require() 전면 거부 |
| 2. 클라이언트 연결 해제 감지 ( Request.signal) | enable_request_signal /disable_request_signal | 플래그 미지정 시 클라이언트 조기 종료가 request.signal에 전파되지 않음 | 클라이언트 연결 해제 시 request.signal에서 abort 이벤트 기본 발생 (단, request_signal_passthrough는 별개) |
| 3. 서브요청 캐시 헤더 보존 ( Request.cache) | cache_option_preserves_cache_control /cache_option_overrides_cache_control | cache: "no-cache" 지정 시 오리진이 Cache-Control: no-store를 보내도 no-cache로 덮어써 캐싱 | cache: "no-cache"는 오리진 재검증만 강제하고 오리진의 Cache-Control 헤더를 그대로 존중 (덮어쓰기는 cf.cacheControl 사용) |
| 4. 스트림 역방향 에러 전파 ( ReadableStream.pipeTo) | pipe_to_backward_propagation /no_pipe_to_backward_propagation | 대상 WritableStream이 닫히거나 에러가 나도 소스 취소 후 Promise가 Resolve되거나(preventCancel: true 시) 영구 미결 | WHATWG 표준에 따라 소스 취소 후 대상 에러(또는 닫힘 시 TypeError, 취소 실패 시 취소 에러)로 Promise Reject |
2. 혼동하기 쉬운 5가지 기술 경계와 마이그레이션 함정
compatibility_date를 "2026-11-17" 이상으로 올릴 때 기존 코드에서 예기치 않은 런타임 예외가 발생하는 지점은 다음 다섯 가지 경계에 집중되어 있다:
- JavaScript/TypeScript Worker와 Python Worker의 모듈 레지스트리 적용 범위는 별개다:
new_module_registry는 JavaScript 및 TypeScript Worker에만 적용되며, Python Worker는2026-11-17이후에도 계속 레거시 레지스트리를 사용한다 (Workers will use the new module registry by default on November 17). 또한 새 레지스트리가 활성화되면 기존의 레거시 제어 플래그 4종(export_commonjs_default,require_returns_default_export,throw_on_unrecognized_import_assertion,disable_top_level_await_in_require)은 더 이상 동작에 영향을 주지 않는다 (Compatibility flags — New module registry).
new_module_registry에서 URL 쿼리·프래그먼트 차이는 별도의 모듈 인스턴스를 생성한다: 새 레지스트리는 모듈 지정자(Specifier)를 URL로 해석한다. 따라서 동일한 소스 파일을 가리키더라도./plugin.js?tenant=a와./plugin.js?tenant=b처럼 쿼리 스트링이나 해시(#)가 다르면 런타임 메모리에 별개의 모듈 인스턴스로 로드되어 모듈 스코프 싱글턴 상태가 공유되지 않는다. 또한import.meta.resolve("./helper.js")는 번들 내 URL 문자열만 계산할 뿐 해당 모듈이 실제로 존재하는지 확인하거나 네트워크로 로드하지 않는다 (Workers will use the new module registry by default on November 17).
require(ESM)의 반환 형태와 Top-Levelawait거부는 이미 평가된 모듈에도 적용된다: Node.js 호환 모드(nodejs_compat,2026-08-04부터 기본 활성화)에서require()로 ES 모듈을 불러올 때, 해당 모듈이나 하위 의존성 중 하나라도 Top-Levelawait를 사용하면 모듈 그래프가 이미 평가를 마친 상태라 하더라도require()호출이 실패한다(이 경우 동적import()를 써야 한다). 또한 일반 ES 모듈을require()하면"module.exports"라는 이름의 export가 없는 한 **모듈 네임스페이스 객체(Namespace)**를 반환하며,node:내장 모듈과 기존 번들의__cjsUnwrapDefault마커가 붙은 모듈만defaultexport를 반환한다 (Compatibility flags — New module registry).
enable_request_signal과request_signal_passthrough는 별개다:2026-11-17부터request.signal이 기본 활성화되어 클라이언트가 연결을 끊으면request.signal에서"abort"이벤트가 발생한다. 하지만 이 설정이 하위fetch()요청에 시그널을 자동 전달하는request_signal_passthrough를 켜주지는 않는다. 업스트림 API나 LLM 추론 서버로 나가는 하위fetch()까지 함께 취소하려면 반드시fetch(upstreamUrl, { signal: request.signal })처럼 명시적으로 시그널을 전달해야 한다 (Request.signal will be enabled by default in Workers on November 17).
- Fetch 표준의
cache: "no-cache"(재검증 제어)와cf: { cacheControl: ... }(응답 캐시 헤더 덮어쓰기)은 별개다: 기존에는 Worker에서fetch(url, { cache: "no-cache" })를 호출하면 오리진 서버가Cache-Control: no-store를 반환했더라도 Cloudflare 캐시가 이를Cache-Control: no-cache로 덮어써 저장하는 비표준 동작이 있었다.2026-11-17부터 기본 적용되는cache_option_preserves_cache_control에서는cache: "no-cache"가 오리진 조건부 재검증만 수행하고 캐싱 여부는 오리진의Cache-Control헤더를 그대로 따른다. 만약 오리진 헤더를 무시하고 강제로 캐시 정책을 덮어써야 한다면cache옵션을 빼고cf: { cacheControl: "no-cache" }를 지정해야 한다 (Compatibility flags — cache option does not override the origin's Cache-Control).
3. 실무 적용 코드: TypeScript Worker 핸들러와 Wrangler 설정
3.1 2026-11-17 표준 패턴을 적용한 TypeScript Worker 예제
아래 코드는 compatibility_date: "2026-11-17" 환경에서 ① import.meta 메타데이터 활용, ② request.signal을 통한 클라이언트 조기 이탈 감지 및 하위 fetch() 명시적 취소 전달, ③ 오리진 Cache-Control 보존 재검증과 cf.cacheControl 강제 재정의 구분, ④ pipeTo()의 역방향 에러 전파(TypeError) 예외 처리를 결합한 예제다.
3.2 wrangler.jsonc 호환성 날짜 전환 및 개별 플래그 롤백 설정
11월 17일 이후 compatibility_date를 "2026-11-17"로 올리면서 특정 서드파티 패키지의 Top-Level await require() 문제나 레거시 스트림 동작 때문에 일부 기능만 이전 동작으로 고정해야 한다면 아래와 같이 개별 비활성화 플래그를 지정한다 (Compatibility flags 공식 문서).
4. 로컬 Python 6대 런타임 변경 검증 시뮬레이션
아래 Python 스크립트는 공식 문서에 정의된 2026-11-17 4대 플래그의 6가지 핵심 불변식(쿼리 스트링별 모듈 인스턴스 분리, 이미 평가된 Top-Level await 모듈 그래프의 require() 거부, require(ESM)의 네임스페이스 vs "module.exports" vs node: 기본 반환 규칙, cache: "no-cache"의 오리진 no-store 보존, pipeTo() 역방향 TypeError Reject 및 소스 취소 에러 우선순위, request.signal 명시적 전달 경계)을 검증한다.
5. 실무 마이그레이션 및 배포 전 점검 체크리스트
번들 및 의존성의 Top-Level await + require() 조합 점검: new_module_registry에서는 하위 의존성 중 단 하나라도 Top-Level await를 쓰면 require()가 즉시 실패한다. 해당 모듈은 await import(...)로 전환하거나 번들러 단계에서 정적 ESM import로 통일한다.
Import Attributes(with { type: ... }) 문법 유효성 확인: 레거시 레지스트리에서 무시되던 알 수 없는 속성 키나 모듈 타입 불일치가 새 레지스트리에서는 즉시 런타임 에러를 발생시킨다.
구형 레지스트리 제어 플래그 4종 정리: export_commonjs_default, require_returns_default_export, throw_on_unrecognized_import_assertion, disable_top_level_await_in_require는 new_module_registry에서 효력이 없으므로 설정 파일에서 제거하고, require(esm)이 default 대신 네임스페이스 객체를 반환하는지 확인한다.
하위 fetch() 호출에 { signal: request.signal } 명시적 전달: enable_request_signal이 기본 켜져도 request_signal_passthrough는 켜지지 않으므로, 클라이언트가 연결을 끊었을 때 비용이 큰 업스트림 API·DB·AI 추론 요청을 함께 중단하려면 signal 옵션을 직접 전달해야 한다.
fetch(url, { cache: "no-cache" })와 pipeTo() 예외 처리 점검: 오리진의 Cache-Control을 강제로 덮어쓰던 코드는 cf: { cacheControl: "no-cache" }로 변경하고, readable.pipeTo(writable) 호출부에는 대상 스트림 조기 종료(TypeError)를 처리할 .catch() 또는 try/catch 블록을 추가한다.
