Cloudflare Worker 프록시 배포 가이드

도핑 뉴스 모니터 — 네이버·구글 수집 안정화를 위한 커스텀 프록시

무료 (월 10만 요청) 설정 10분 봇 차단 우회 효과적 전 세계 엣지 서버
왜 Cloudflare Worker가 필요한가요?
방법 구글 RSS 네이버 안정성 비용
공개 프록시 (현재) 불안정 자주 차단 낮음 무료
rss2json.com 안정적 미지원 높음 무료(분당10회)
Cloudflare Worker ★ 안정적 안정적 매우 높음 무료(월10만)
현재 이 앱은 구글 RSS → rss2json.com을 우선 사용하고, 나머지(네이버·전문사이트)는 공개 프록시를 시도합니다. Cloudflare Worker를 추가하면 네이버 수집 성공률이 크게 올라갑니다.
1 Cloudflare 계정 만들기
무료 계정 가입

https://dash.cloudflare.com/sign-up 에서 이메일·비밀번호만으로 가입합니다.
신용카드 불필요 — 무료 플랜으로 충분합니다.

2 Workers & Pages → 새 Worker 만들기
대시보드에서 Worker 생성

1. 로그인 후 좌측 메뉴 Workers & Pages 클릭

2. Create applicationCreate Worker 클릭

3. Worker 이름 입력 (예: doping-proxy)

4. Deploy 클릭 (기본 코드로 먼저 배포)

3 Worker 코드 교체 (복사 → 붙여넣기)

배포 후 Edit code 버튼을 클릭해 아래 코드 전체를 붙여넣고 Save and Deploy합니다.

JavaScript — Worker 코드
/**
 * Doping News Monitor — CORS Proxy Worker  v3.0
 * 배포 후 URL: https://doping-proxy.YOUR-SUBDOMAIN.workers.dev
 *
 * ■ 라우트
 *   GET /?url=<enc>
 *       — 일반 CORS 프록시 (Naver HTML, WADA, ITA, iNADO, ITG 크롤링)
 *
 *   GET /naver_api?query=<q>&id=<id>&secret=<sec>[&start=1][&display=100][&sort=date]
 *       — 네이버 Open API 전용 포워딩
 *         Worker가 서버→서버로 호출 → CORS/봇차단 완전 우회
 *
 * ■ 봇 차단 우회 원리
 *   브라우저 → Worker (CORS 허용) → openapi.naver.com (서버간 호출, 봇차단 없음)
 *   Worker가 X-Naver-Client-Id / X-Naver-Client-Secret 헤더를 대신 전달
 *
 * ■ v3.0 변경사항
 *   - /naver_api: start 파라미터 지원 → 페이징(최대 300건) 가능
 *   - /naver_api: 타임아웃 30초, 429/401/403 에러 코드 그대로 전달
 *   - handleProxy: 최신 Chrome UA, Sec-Fetch 헤더 추가로 봇 차단율 감소
 *   - 공통: X-Worker-Version 헤더로 버전 확인 가능
 */
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    // ── CORS preflight ──────────────────────────────────────────
    if (request.method === "OPTIONS") {
      return new Response(null, { status: 204, headers: corsHeaders() });
    }

    // GET 이외 차단
    if (request.method !== "GET") {
      return jsonError(405, "GET 메서드만 허용됩니다.");
    }

    // ── 라우트: /naver_api ──────────────────────────────────────
    if (url.pathname === "/naver_api" || url.pathname === "/naver_api/") {
      return handleNaverAPI(url.searchParams);
    }

    // ── 라우트: / — 일반 CORS 프록시 ────────────────────────────
    return handleProxy(url.searchParams);
  },
};

// ============================================================
// 네이버 Open API 포워딩 핸들러 (서버→서버, CORS 우회)
// ============================================================
async function handleNaverAPI(params) {
  const query   = params.get("query")   || "";
  const id      = params.get("id")      || "";
  const secret  = params.get("secret")  || "";
  const display = params.get("display") || "100";
  const sort    = params.get("sort")    || "date";
  const start   = params.get("start")   || "1";

  if (!query || !id || !secret) {
    return jsonError(400, "query, id, secret 파라미터가 필요합니다.");
  }
  // display 범위 보정 (1~100)
  const safeDisplay = Math.min(100, Math.max(1, parseInt(display, 10) || 100));
  // start 범위 보정 (1~1000)
  const safeStart   = Math.min(1000, Math.max(1, parseInt(start, 10) || 1));

  const apiUrl = "https://openapi.naver.com/v1/search/news.json?" +
    new URLSearchParams({
      query,
      display: String(safeDisplay),
      sort,
      start:   String(safeStart),
    });

  try {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 30000); // 30초 타임아웃

    const resp = await fetch(apiUrl, {
      signal: controller.signal,
      headers: {
        "X-Naver-Client-Id":     id,
        "X-Naver-Client-Secret": secret,
        "Accept":                "application/json",
        "User-Agent":            "DoingNewsMonitor/3.0 (Cloudflare Worker; server-to-server)",
      },
    });
    clearTimeout(timer);

    const body = await resp.text();

    // 401/403/429는 상태 코드를 그대로 전달 → 브라우저에서 정확한 오류 처리 가능
    return new Response(body, {
      status: resp.status,
      headers: {
        ...corsHeaders(),
        "Content-Type":     "application/json; charset=utf-8",
        "X-Proxied-Status": String(resp.status),
        "X-Via":            "cf-worker-naver-api",
        "X-Worker-Version": "3.0",
      },
    });
  } catch (err) {
    const isTimeout = err.name === "AbortError";
    return jsonError(
      isTimeout ? 504 : 502,
      isTimeout ? "네이버 API 타임아웃 (30초 초과)" : "네이버 API 요청 실패: " + err.message
    );
  }
}

// ============================================================
// 일반 CORS 프록시 핸들러 (HTML/XML 크롤링)
// ============================================================
async function handleProxy(params) {
  const rawUrl = params.get("url");

  if (!rawUrl) {
    return jsonError(400, "url 파라미터가 필요합니다.");
  }

  let parsedUrl;
  try {
    parsedUrl = new URL(decodeURIComponent(rawUrl));
  } catch (e) {
    return jsonError(400, "유효하지 않은 URL: " + e.message);
  }

  // https 전용 (http 차단)
  if (parsedUrl.protocol !== "https:") {
    return jsonError(403, "https URL만 허용됩니다.");
  }

  // 허용 도메인 목록
  const ALLOWED_HOSTS = [
    "search.naver.com",
    "m.search.naver.com",
    "news.naver.com",
    "n.news.naver.com",
    "openapi.naver.com",
    "news.google.com",
    "www.wada-ama.org",
    "ita.sport",
    "www.ita.sport",
    "inado.org",
    "www.inado.org",
    "www.insidethegames.biz",
    "api.rss2json.com",
  ];
  const isAllowed = ALLOWED_HOSTS.some(
    h => parsedUrl.hostname === h || parsedUrl.hostname.endsWith("." + h)
  );
  if (!isAllowed) {
    return jsonError(403, "허용되지 않은 호스트: " + parsedUrl.hostname);
  }

  try {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), 25000); // 25초 타임아웃

    const resp = await fetch(parsedUrl.href, {
      signal:   controller.signal,
      redirect: "follow",
      headers: {
        // 최신 Chrome 135 UA — 봇 차단율 감소
        "User-Agent":
          "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
          "AppleWebKit/537.36 (KHTML, like Gecko) " +
          "Chrome/135.0.0.0 Safari/537.36",
        "Accept":
          "text/html,application/xhtml+xml,application/xml;q=0.9," +
          "image/avif,image/webp,*/*;q=0.8",
        "Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
        "Accept-Encoding": "gzip, deflate, br",
        "Cache-Control":   "no-cache",
        "Pragma":          "no-cache",
        "Referer":         "https://www.naver.com/",
        // Sec-Fetch 헤더: 일반 브라우저 요청처럼 보이게
        "Sec-Fetch-Dest":  "document",
        "Sec-Fetch-Mode":  "navigate",
        "Sec-Fetch-Site":  "none",
        "Sec-Fetch-User":  "?1",
        "Upgrade-Insecure-Requests": "1",
      },
    });
    clearTimeout(timer);

    const contentType = resp.headers.get("content-type") || "text/plain; charset=utf-8";
    const body = await resp.text();

    return new Response(body, {
      status: resp.status,
      headers: {
        ...corsHeaders(),
        "Content-Type":     contentType,
        "X-Proxied-Status": String(resp.status),
        "X-Via":            "cf-worker-proxy",
        "X-Worker-Version": "3.0",
      },
    });
  } catch (err) {
    const isTimeout = err.name === "AbortError";
    return jsonError(
      isTimeout ? 504 : 502,
      isTimeout ? "프록시 타임아웃 (25초 초과)" : "프록시 요청 실패: " + err.message
    );
  }
}

// ============================================================
// 공통 유틸
// ============================================================
function corsHeaders() {
  return {
    "Access-Control-Allow-Origin":  "*",
    "Access-Control-Allow-Methods": "GET, OPTIONS",
    "Access-Control-Allow-Headers":
      "Content-Type, Accept, X-Naver-Client-Id, X-Naver-Client-Secret",
    "Access-Control-Max-Age": "86400",
  };
}

function jsonError(status, message) {
  return new Response(
    JSON.stringify({ error: message, status }),
    {
      status,
      headers: {
        ...corsHeaders(),
        "Content-Type":     "application/json; charset=utf-8",
        "X-Worker-Version": "3.0",
      },
    }
  );
}
ALLOWED_HOSTS 목록에 없는 도메인은 403으로 차단됩니다. 필요 시 배열에 추가하세요.
4 Worker URL 확인 및 앱에 등록
Worker URL 찾기

배포 완료 후 Worker 상세 페이지에 URL이 표시됩니다:

https://doping-proxy.YOUR-SUBDOMAIN.workers.dev

이 URL을 복사합니다.

여기에 Worker URL을 입력하고 저장하면 앱에 바로 적용됩니다
5 동작 확인
브라우저에서 직접 테스트

아래 URL을 브라우저에서 열어 네이버 검색 결과 HTML이 표시되면 성공입니다:

https://doping-proxy.YOUR-SUBDOMAIN.workers.dev/?url=https%3A%2F%2Fsearch.naver.com%2Fsearch.naver%3Fwhere%3Dnews%26query%3D%EB%8F%84%ED%95%91

앱 수집 화면에서 수집 로그에 "✅ [네이버] 프록시1 성공" 등이 뜨면 정상입니다.

무료 플랜 한도: 월 100,000 요청 / 하루 약 3,300회. 키워드 5개 × 포털 2개 × 페이지 3개 = 1회 수집 시 약 30 요청. 하루 100회 수집해도 3,000 요청 — 충분합니다.
자주 묻는 질문
개인 정보가 노출되나요?

Worker는 URL만 중계합니다. 로그인 정보나 개인 정보는 전송되지 않습니다. Worker 코드도 직접 관리하므로 안전합니다.

네이버가 여전히 차단되면?

네이버는 주기적으로 크롤러 대응을 강화합니다. 이 경우 Worker의 User-AgentReferer 헤더를 최신 Chrome 버전으로 업데이트해 보세요.

Worker를 삭제하고 싶으면?

Cloudflare 대시보드 → Workers & Pages → 해당 Worker → Settings → Delete에서 삭제할 수 있습니다. 앱 설정에서도 Worker URL을 초기화하세요.

메인 페이지로 돌아가기