사용자가 서울에 있고 오리진 서버가 미국 동부 리전에 있다면, 요청 하나가 왕복하는 데만 물리적으로 200ms 이상이 소요됩니다. 광속은 협상 대상이 아니므로, 지연을 줄이는 유일한 방법은 연산과 데이터를 사용자 가까이로 옮기는 것입니다. Cloudflare Workers는 이 발상에서 출발해, 전 세계 수백 개 도시의 노드에서 코드를 실행하여 오리진까지 가지 않고도 응답을 만들어냅니다.

하지만 엣지는 “그냥 배포하면 빨라지는” 마법이 아닙니다. Workers는 컨테이너가 아닌 V8 아이솔레이트(isolate) 위에서 돌아가고, 실행 시간·메모리·API에 고유한 제약이 있습니다. 이 글에서는 실행 모델부터 라우팅, 엣지 스토리지, 캐싱, 그리고 지연시간을 계측하고 줄이는 방법까지 정리합니다.

왜 엣지가 지연을 줄이는가

일반적인 요청 경로는 사용자 → CDN → 오리진 → DB → 오리진 → CDN → 사용자입니다. CDN이 정적 자산을 캐싱해도 동적 응답은 결국 오리진까지 왕복해야 하고, 이 구간의 RTT가 전체 체감 속도를 지배합니다. 게다가 TLS 핸드셰이크가 최소 1회 왕복을 더하므로, 거리가 멀수록 TTFB가 급격히 늘어납니다.

Workers는 이 체인에서 연산 자체를 CDN 노드로 끌어옵니다. 가장 가까운 엣지 데이터센터에서 요청을 받아 인증·리다이렉트·A/B 분기·응답 조립 같은 로직을 즉시 처리하며, 오리진 호출이 필요 없는 요청은 왕복 자체가 사라지고 TLS 핸드셰이크도 사용자 근처에서 종단됩니다.

실행 모델: 아이솔레이트 vs 컨테이너

Workers가 컨테이너 기반 서버리스(FaaS)와 근본적으로 다른 지점은 격리 방식입니다. 전통적 서버리스는 요청마다 컨테이너나 마이크로VM을 띄워 콜드 스타트에 수백 ms에서 수 초가 걸립니다. Workers는 대신 V8 아이솔레이트를 씁니다. 아이솔레이트는 하나의 V8 프로세스 안에서 메모리만 격리된 경량 실행 컨텍스트로, 생성 비용이 밀리초 단위입니다.

덕분에 Workers의 콜드 스타트는 실질적으로 무시할 수 있는 수준(대개 5ms 미만)입니다. 다만 대가가 있습니다. 파일시스템 접근이 없고 네이티브 바이너리를 실행할 수 없으며, Node.js 전체 API 대신 웹 표준(Fetch, Streams, Web Crypto) 위에서 동작해야 합니다.

// 가장 기본적인 Worker — 모든 요청을 엣지에서 처리
export default {
  async fetch(request, env, ctx) {
    // 지역 정보는 요청 객체에 이미 붙어 있음 (엣지가 알고 있음)
    const country = request.cf?.country ?? "unknown";
    const colo = request.cf?.colo ?? "unknown"; // 처리한 데이터센터 코드

    // 오리진 왕복 없이 엣지에서 즉시 응답
    return Response.json({ country, colo });
  },
};

request.cf 객체는 엣지가 이미 알고 있는 지리·네트워크 정보를 담습니다. 사용자의 국가·도시·ASN을 오리진에 물어보지 않고도 알 수 있어 지역별 분기를 지연 없이 처리합니다.

라우팅과 배포 설정

Workers는 wrangler CLI와 wrangler.toml로 배포하며, 어떤 경로에서 트리거되고 어떤 리소스에 바인딩되는지를 선언합니다.

# wrangler.toml — Worker 배포 설정
name = "edge-api"
main = "src/index.js"
compatibility_date = "2026-01-01"

# 특정 라우트에서만 Worker 실행 (나머지는 오리진 직행)
routes = [{ pattern = "api.example.com/*", zone_name = "example.com" }]

[[kv_namespaces]]              # env.SETTINGS 로 접근
binding = "SETTINGS"
id = "a1b2c3d4e5f6"

[[d1_databases]]              # SQLite — env.DB
binding = "DB"
database_id = "9f8e7d6c"

[observability]
enabled = true               # 요청 로그·트레이스 수집

라우트 패턴을 좁게 잡는 것이 중요합니다. 모든 요청을 Worker로 통과시키면 정적 자산까지 불필요하게 아이솔레이트를 거칩니다. 정적 파일은 Cloudflare 캐시나 R2/Pages에 맡기고, Worker는 동적 로직이 필요한 경로에만 붙이세요.

엣지 캐싱으로 오리진 왕복 제거

지연을 줄이는 가장 직접적인 방법은 오리진에 아예 가지 않는 것입니다. Workers는 Cache API로 엣지 로컬 캐시를 직접 제어할 수 있어, CDN 기본 캐싱보다 세밀하게 요청별 캐시 키와 TTL을 다룰 수 있습니다.

export default {
  async fetch(request, env, ctx) {
    const cache = caches.default;

    // 추적 파라미터를 제거한 정규화된 캐시 키 (적중률 향상)
    const url = new URL(request.url);
    url.searchParams.delete("utm_source");
    const cacheKey = new Request(url.toString(), request);

    // 1) 엣지 로컬 캐시 조회 — 히트 시 오리진 왕복 0, 수 ms 내 응답
    let response = await cache.match(cacheKey);
    if (response) return response;

    // 2) 미스 시에만 오리진 호출 후, 성공 응답을 60초간 엣지에 저장
    response = await fetch(request);
    if (response.status === 200) {
      response = new Response(response.body, response);
      response.headers.set("Cache-Control", "s-maxage=60");
      ctx.waitUntil(cache.put(cacheKey, response.clone())); // 백그라운드 기록
    }
    return response;
  },
};

ctx.waitUntil()은 응답을 사용자에게 먼저 돌려준 뒤 캐시 쓰기 같은 후속 작업을 백그라운드에서 이어가게 하므로, 캐시 저장 지연이 응답 시간에 포함되지 않습니다. 캐시 키에서 추적용 쿼리 파라미터를 제거하는 정규화는 적중률을 크게 높이는 실전 기법입니다.

엣지 스토리지 선택: KV, D1, Durable Objects

엣지에서 데이터를 다루려면 각 스토리지의 일관성 모델을 이해해야 합니다.

  • KV: 전 세계에 복제되는 최종 일관성(eventually consistent) 키-값 저장소. 읽기는 엣지에서 매우 빠르지만 쓰기가 전 지역에 전파되기까지 최대 수십 초 지연이 있어, 설정값·피처 플래그처럼 읽기 많고 쓰기 드문 데이터에 적합합니다.
  • D1: 엣지에서 접근하는 SQLite 기반 관계형 DB. 강한 일관성을 제공하지만 프라이머리 인스턴스가 특정 리전에 있어 원거리 쓰기는 왕복 지연이 있습니다.
  • Durable Objects: 단일 인스턴스로 강한 일관성과 직렬화된 접근을 보장하는 상태 객체. 카운터, 채팅방처럼 순서와 원자성이 중요한 곳에 씁니다.
// KV: 피처 플래그 조회 — 엣지 로컬 읽기라 매우 빠름 (오리진 왕복 없음)
const flags = await env.SETTINGS.get("feature-flags", { type: "json" });
if (flags?.newCheckout) { /* 신규 결제 플로우로 분기 */ }

// KV 쓰기는 최종 일관성 — 즉시 전역 반영을 기대하지 말 것
await env.SETTINGS.put("feature-flags", body, { expirationTtl: 3600 });

흔한 실수는 KV를 강한 일관성이 필요한 곳에 쓰는 것입니다. “방금 저장한 값을 다른 리전에서 즉시 읽어야 한다”면 KV는 부적합하며, Durable Objects나 D1의 트랜잭션을 선택해야 합니다.

오리진 호출 최적화와 병렬화

엣지에서도 결국 오리진이나 외부 API를 호출해야 하는 경우가 많습니다. 호출을 직렬로 이어 붙이면 왕복 지연이 누적되므로, 서로 의존하지 않는 요청은 Promise.all로 병렬화해 전체 지연을 가장 느린 하나의 왕복으로 압축해야 합니다.

// 나쁨: 직렬 호출은 지연이 합산됨 (a + b + c)
//   const user = await getUser(); const orders = await getOrders(); ...

// 좋음: 독립 호출은 병렬 — 전체 지연이 max(a, b, c) 로 수렴
const [user, orders, recs] = await Promise.all([
  fetch(env.ORIGIN + "/user",   { cf: { cacheTtl: 30 } }),
  fetch(env.ORIGIN + "/orders", { cf: { cacheTtl: 10 } }),
  fetch(env.ORIGIN + "/recs",   { cf: { cacheTtl: 300 } }), // 신선도 낮으면 TTL 길게
]);

return Response.json({
  user: await user.json(),
  orders: await orders.json(),
  recs: await recs.json(),
});

fetch의 cf.cacheTtl 옵션은 Cloudflare 백본 수준에서 응답을 캐싱하므로, 신선도 요구가 낮은 응답일수록 TTL을 길게 잡아 오리진 부하와 지연을 함께 줄일 수 있습니다.

제약과 함정

Workers의 강점은 제약에서 나오지만, 그 제약을 모르면 배포 후에야 벽에 부딪힙니다. 설계 단계에서 고려해야 할 지점을 정리합니다.

  • CPU 시간 제한: 요청당 CPU 실행 시간에 상한이 있습니다(플랜에 따라 다름). 무거운 이미지 처리나 대규모 암호 연산은 부적합하며 별도 서비스로 분리해야 합니다. 단, I/O 대기 시간은 CPU 시간에 포함되지 않습니다.
  • 서브리퀘스트 수 제한: 한 요청에서 나갈 수 있는 외부 fetch 호출 수에 상한이 있어, 팬아웃이 많은 오케스트레이션은 주의가 필요합니다.
  • 메모리 제한: 아이솔레이트당 메모리가 제한적이니 응답 전체를 버퍼링하지 말고 스트리밍하세요.
  • 파일시스템·일부 Node API 부재: 로컬 디스크 캐시 같은 가정은 성립하지 않으니, 상태는 KV·D1·Durable Objects·R2로 외부화해야 합니다.
  • 전역 상태의 함정: 모듈 최상위 변수는 같은 아이솔레이트를 재사용하는 동안만 유지되며 언제든 사라질 수 있으니, 캐시로는 쓰되 영속성을 기대하면 안 됩니다.

지연 계측과 검증

“빨라졌다”는 체감이 아니라 데이터로 확인해야 합니다. 엣지 처리가 어느 데이터센터에서 이뤄졌는지, 오리진 왕복이 얼마나 걸렸는지를 응답 헤더나 로그로 노출해 계측하세요.

# 여러 지역에서 TTFB 계측 — x-edge-colo 헤더로 어느 엣지가 처리했는지 확인
# (Worker에서 request.cf.colo, cf-cache-status 를 응답 헤더로 노출해 둔다)
curl -sw "connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}n" 
  -o /dev/null -D - https://api.example.com/data | grep -i "x-edge"

# 실시간 로그 스트리밍으로 CPU 시간·예외 관찰
wrangler tail --format pretty --status error

colo 값이 사용자와 가까운 데이터센터를 가리키는지, x-cache가 기대만큼 HIT인지 확인하는 것이 핵심입니다. 적중률이 낮다면 캐시 키 정규화를, 응답 시간이 크다면 오리진 호출 병렬화를 점검하세요.

마무리

Cloudflare Workers로 지연을 줄이는 핵심은 단순합니다. 오리진 왕복을 없애거나(엣지 캐싱) 짧게 만드는 것(백본·병렬화)입니다. 아이솔레이트 모델 덕분에 콜드 스타트 걱정 없이 이 로직을 사용자 옆에서 실행할 수 있지만, CPU·메모리·API 제약과 스토리지의 일관성 모델을 이해하지 못하면 오히려 새로운 병목을 만듭니다.

실전 접근은 계측 우선입니다. colo·x-cache·TTFB를 먼저 눈에 보이게 만든 뒤 캐시 키 정규화 → 오리진 호출 병렬화 순으로 개선하면 가장 큰 지연 구간부터 줄일 수 있습니다. 엣지는 만능이 아니라 오리진과 역할을 나눠 갖는 새로운 레이어로 이해할 때 제 값을 합니다.

자주 묻는 질문

Q. Workers는 콜드 스타트가 정말 없나요?

A. 컨테이너 기반 서버리스에 비하면 사실상 없다고 봐도 됩니다. V8 아이솔레이트는 생성 비용이 밀리초 단위라 콜드 스타트가 대개 5ms 미만입니다. 다만 코드 번들이 매우 크거나 모듈 최상위에서 무거운 초기화를 하면 첫 실행이 느려지니, 초기화는 최소화하고 지연 로딩하세요.

Q. KV와 D1 중 무엇을 써야 하나요?

A. 데이터의 일관성 요구로 판단합니다. 읽기가 압도적으로 많고 쓰기 후 전역 반영에 수십 초 지연을 허용할 수 있다면(설정, 피처 플래그, 정적 캐시) KV가 빠르고 저렴합니다. 관계형 쿼리나 방금 쓴 값을 즉시 정확히 읽어야 한다면 D1을 쓰되 먼 리전 쓰기의 지연을 감안하세요. 순서와 원자성이 핵심이면 Durable Objects가 정답입니다.

Q. 무거운 연산을 엣지에서 하면 안 되는 이유는 무엇인가요?

A. 요청당 CPU 시간에 상한이 있기 때문입니다. 대용량 이미지 변환이나 대규모 반복 암호 연산은 이 한도를 넘겨 요청이 종료될 수 있습니다. 엣지는 라우팅·인증·캐시 판단 같은 가벼운 로직에 두고, CPU 집약적 작업은 오리진이나 전용 서비스로 분리하세요. 단, I/O 대기는 CPU 시간에 포함되지 않으니 외부 호출을 많이 기다리는 것 자체는 문제되지 않습니다.