봇 감지 테스트 사이트들

  1. https://bot-detector.rebrowser.net/
  2. https://bot.sannysoft.com/
  3. https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html
  4. https://arh.antoinevastel.com/bots/areyouheadless

LLM 추천 옵션들

args: [
  "--no-sandbox",
  "--disable-setuid-sandbox",
  "--disable-dev-shm-usage",
  "--disable-accelerated-2d-canvas",
  "--no-first-run",
  "--no-zygote",
  "--disable-gpu",
  "--disable-web-security",
  "--disable-features=VizDisplayCompositor",
  "--disable-blink-features=AutomationControlled",
  "--exclude-switches=enable-automation",
  "--disable-plugins-discovery",
  "--disable-default-apps",
  "--disable-infobars",
  "--disable-extensions",
  "--disable-component-extensions-with-background-pages",
  "--disable-background-timer-throttling",
  "--disable-backgrounding-occluded-windows",
  "--disable-renderer-backgrounding",
  "--disable-field-trial-config",
  "--disable-back-forward-cache",
  "--disable-ipc-flooding-protection",
  "--password-store=basic",
  "--use-mock-keychain",
  "--no-default-browser-check",
  "--no-pings",
  "--disable-sync",
  "--disable-translate",
  "--disable-background-networking",
  "--disable-client-side-phishing-detection",
  "--disable-component-update",
  "--disable-domain-reliability",
  "--disable-features=TranslateUI",
  "--disable-hang-monitor",
  "--disable-prompt-on-repost",
  "--metrics-recording-only",
  "--safebrowsing-disable-auto-update",
  "--enable-automation=false",
],

뷰포트 설정

await this.page.setViewport({
  width: 1920,
  height: 1080,
  deviceScaleFactor: 1,
  hasTouch: false,
  isLandscape: true,
  isMobile: false,
});

기타 한국 특화 설정

// 한국 사용자 User-Agent 설정
await this.page.setUserAgent(
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
);

// 고급 봇 감지 우회 스크립트
await this.page.evaluateOnNewDocument(() => {
  // webdriver 속성 완전 제거
  Object.defineProperty(navigator, "webdriver", {
    get: () => undefined,
    configurable: true,
  });

  // plugins 배열을 실제 브라우저처럼 설정
  Object.defineProperty(navigator, "plugins", {
    get: () => ({
      0: {
        0: {
          description: "Portable Document Format",
          enabledPlugin: {},
          suffixes: "pdf",
          type: "application/pdf",
        },
        description: "Portable Document Format",
        filename: "internal-pdf-viewer",
        length: 1,
        name: "Chrome PDF Plugin",
      },
      length: 1,
    }),
    configurable: true,
  });

  // languages 설정 (한국어 우선)
  Object.defineProperty(navigator, "languages", {
    get: () => ["ko-KR", "ko", "en-US", "en"],
    configurable: true,
  });

  // language 설정
  Object.defineProperty(navigator, "language", {
    get: () => "ko-KR",
    configurable: true,
  });

  // platform 설정
  Object.defineProperty(navigator, "platform", {
    get: () => "Win32",
    configurable: true,
  });

  // hardwareConcurrency 설정
  Object.defineProperty(navigator, "hardwareConcurrency", {
    get: () => 8,
    configurable: true,
  });

  // deviceMemory 설정
  Object.defineProperty(navigator, "deviceMemory", {
    get: () => 8,
    configurable: true,
  });

  // permissions API 우회
  const originalQuery = window.navigator.permissions.query;
  window.navigator.permissions.query = (parameters) =>
    parameters.name === "notifications"
      ? Promise.resolve({ state: "granted" })
      : originalQuery(parameters);

  // Chrome runtime 제거
  if (window.chrome && window.chrome.runtime) {
    delete window.chrome.runtime.onConnect;
    delete window.chrome.runtime.onMessage;
  }

  // WebGL 정보 설정
  const getParameter = WebGLRenderingContext.getParameter;
  WebGLRenderingContext.prototype.getParameter = function (parameter) {
    if (parameter === 37445) {
      return "Intel Inc.";
    }
    if (parameter === 37446) {
      return "Intel(R) Iris(TM) Graphics 6100";
    }
    return getParameter(parameter);
  };

  // 마우스 이벤트 시뮬레이션을 위한 설정
  let mouseX = 0;
  let mouseY = 0;
  document.addEventListener("mousemove", (e) => {
    mouseX = e.clientX;
    mouseY = e.clientY;
  });

  // 스크롤 이벤트 시뮬레이션
  let scrollY = 0;
  window.addEventListener("scroll", () => {
    scrollY = window.scrollY;
  });
});

// 한국 사용자 헤더 설정
await this.page.setExtraHTTPHeaders({
  "Accept-Language": "ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7",
  "Accept-Encoding": "gzip, deflate, br",
  Accept:
    "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
  "Upgrade-Insecure-Requests": "1",
  "Cache-Control": "max-age=0",
  "sec-ch-ua":
    '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
  "sec-ch-ua-mobile": "?0",
  "sec-ch-ua-platform": '"Windows"',
  "Sec-Fetch-Dest": "document",
  "Sec-Fetch-Mode": "navigate",
  "Sec-Fetch-Site": "none",
  "Sec-Fetch-User": "?1",
});

// 한국 시간대 설정
await this.page.emulateTimezone("Asia/Seoul");

사람 같이 행동하게?

async simulateHumanBehavior() {
  // 1. 자연스러운 마우스 움직임 (베지어 곡선 사용)
  await this.simulateNaturalMouseMovement();

  // 2. 페이지 읽기 시뮬레이션 (위에서 아래로 천천히 스크롤)
  await this.simulateReading();

  // 3. 랜덤한 일시정지 (사람이 생각하는 시간)
  await this.simulateThinking();

  // 4. 가끔 뒤로 스크롤 (다시 읽기)
  if (Math.random() < 0.3) {
    await this.simulateReReading();
  }
}

async simulateNaturalMouseMovement() {
  const viewport = await this.page.viewport();
  const startX = Math.random() * viewport.width;
  const startY = Math.random() * viewport.height;

  // 여러 단계로 나누어 자연스럽게 마우스 이동
  const steps = 3 + Math.floor(Math.random() * 5); // 3-7단계

  for (let i = 0; i < steps; i++) {
    const targetX = Math.random() * viewport.width;
    const targetY = Math.random() * viewport.height;

    // 베지어 곡선처럼 중간 지점들을 거쳐 이동
    const currentX = startX + (targetX - startX) * (i / steps);
    const currentY = startY + (targetY - startY) * (i / steps);

    // 약간의 랜덤 노이즈 추가 (손떨림 효과)
    const noiseX = (Math.random() - 0.5) * 20;
    const noiseY = (Math.random() - 0.5) * 20;

    await this.page.mouse.move(currentX + noiseX, currentY + noiseY);

    // 각 이동 사이에 자연스러운 지연
    await new Promise((resolve) =>
      setTimeout(resolve, 50 + Math.random() * 100)
    );
  }
}

async simulateReading() {
  // 페이지 높이 확인
  const pageHeight = await this.page.evaluate(
    () => document.body.scrollHeight
  );
  const viewportHeight = await this.page.evaluate(() => window.innerHeight);

  if (pageHeight <= viewportHeight) return; // 스크롤할 필요 없음

  // 읽기 속도 시뮬레이션 (한국인 평균 읽기 속도 고려)
  const readingSpeed = 200 + Math.random() * 100; // 200-300 글자/분
  const scrollSteps = 5 + Math.floor(Math.random() * 8); // 5-12단계로 나누어 스크롤

  for (let i = 0; i < scrollSteps; i++) {
    // 각 스크롤 단계마다 다른 속도로 스크롤
    const scrollAmount =
      (pageHeight / scrollSteps) * (0.8 + Math.random() * 0.4);

    await this.page.evaluate((amount) => {
      window.scrollBy({
        top: amount,
        behavior: "smooth",
      });
    }, scrollAmount);

    // 읽는 시간 시뮬레이션 (내용을 읽는 시간)
    const readingTime = 1000 + Math.random() * 2000; // 1-3초
    await new Promise((resolve) => setTimeout(resolve, readingTime));

    // 가끔 마우스도 함께 움직임
    if (Math.random() < 0.4) {
      const viewport = await this.page.viewport();
      const mouseX = Math.random() * viewport.width;
      const mouseY = Math.random() * viewport.height;
      await this.page.mouse.move(mouseX, mouseY);
    }
  }
}

async simulateThinking() {
  // 사람이 생각하거나 망설이는 시간
  const thinkingTime = 500 + Math.random() * 2000; // 0.5-2.5초
  await new Promise((resolve) => setTimeout(resolve, thinkingTime));

  // 생각하는 동안 가끔 마우스를 작게 움직임
  if (Math.random() < 0.6) {
    const viewport = await this.page.viewport();
    const smallMoveX = (Math.random() - 0.5) * 50;
    const smallMoveY = (Math.random() - 0.5) * 50;

    const currentX = Math.random() * viewport.width;
    const currentY = Math.random() * viewport.height;

    await this.page.mouse.move(
      Math.max(0, Math.min(viewport.width, currentX + smallMoveX)),
      Math.max(0, Math.min(viewport.height, currentY + smallMoveY))
    );
  }
}

async simulateReReading() {
  // 가끔 위로 다시 스크롤해서 다시 읽기
  const currentScroll = await this.page.evaluate(() => window.pageYOffset);

  if (currentScroll > 200) {
    const backScrollAmount = 100 + Math.random() * 300;

    await this.page.evaluate((amount) => {
      window.scrollBy({
        top: -amount,
        behavior: "smooth",
      });
    }, backScrollAmount);

    // 다시 읽는 시간
    await new Promise((resolve) =>
      setTimeout(resolve, 800 + Math.random() * 1200)
    );

    // 다시 아래로 스크롤
    await this.page.evaluate((amount) => {
      window.scrollBy({
        top: amount * 0.7, // 조금 덜 내려감
        behavior: "smooth",
      });
    }, backScrollAmount);
  }
}