도핑 뉴스 모니터 — 네이버·구글 수집 안정화를 위한 커스텀 프록시
| 방법 | 구글 RSS | 네이버 | 안정성 | 비용 |
|---|---|---|---|---|
| 공개 프록시 (현재) | 불안정 | 자주 차단 | 낮음 | 무료 |
| rss2json.com | 안정적 | 미지원 | 높음 | 무료(분당10회) |
| Cloudflare Worker ★ | 안정적 | 안정적 | 매우 높음 | 무료(월10만) |
https://dash.cloudflare.com/sign-up
에서 이메일·비밀번호만으로 가입합니다.
신용카드 불필요 — 무료 플랜으로 충분합니다.
1. 로그인 후 좌측 메뉴 Workers & Pages 클릭
2. Create application → Create Worker 클릭
3. Worker 이름 입력 (예: doping-proxy)
4. Deploy 클릭 (기본 코드로 먼저 배포)
배포 후 Edit code 버튼을 클릭해 아래 코드 전체를 붙여넣고 Save and Deploy합니다.
/**
* 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",
},
}
);
}
배포 완료 후 Worker 상세 페이지에 URL이 표시됩니다:
https://doping-proxy.YOUR-SUBDOMAIN.workers.dev
이 URL을 복사합니다.
아래 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 성공" 등이 뜨면 정상입니다.
Worker는 URL만 중계합니다. 로그인 정보나 개인 정보는 전송되지 않습니다. Worker 코드도 직접 관리하므로 안전합니다.
네이버는 주기적으로 크롤러 대응을 강화합니다. 이 경우 Worker의 User-Agent나 Referer 헤더를 최신 Chrome 버전으로 업데이트해 보세요.
Cloudflare 대시보드 → Workers & Pages → 해당 Worker → Settings → Delete에서 삭제할 수 있습니다. 앱 설정에서도 Worker URL을 초기화하세요.