스크래핑

페이지 뒤의 API 찾기: HTML 대신 JSON 엔드포인트에서 수집하기

많은 페이지가 데이터를 JSON 형태로 로드합니다. 이를 전달하는 엔드포인트를 찾는 방법, 그것이 페이지의 세션과 연결되어 있는 경우가 많은 이유, 그리고 책임감 있게 수집하는 방법을 소개합니다.

Chris Collins

Chris Collins

2026년 9월 30일 · 6 분 소요

웹 페이지를 열고 네트워크 활동을 지켜보면, 원하는 데이터가 페이지에 렌더링되기 전에 JSON 형태로 도착하는 것을 자주 볼 수 있습니다. 스크래퍼가 HTML에서 힘들게 추출해야 할 가격, 목록 또는 결과가 페이지가 스스로 가져온 깔끔하고 구조화된 응답 안에 이미 들어 있는 것입니다.

렌더링된 페이지 대신 이 응답에서 수집하면 크기가 더 작고, 더 안정적이며, 파싱하기도 훨씬 쉬울 수 있습니다. 하지만 URL을 복사하는 것만큼 단순하지는 않으며, 대부분의 팀이 두 가지 사실에 놀라게 됩니다. 페이지의 JSON 중 실제로 데이터인 부분이 얼마나 적은지, 그리고 데이터 엔드포인트가 호출한 페이지 밖에서는 작동을 거부하는 경우가 얼마나 많은지입니다. 이 가이드는 올바른 엔드포인트를 찾는 방법, 실제 페이지에서 측정한 결과, 그리고 선을 넘지 않으면서 엔드포인트에서 수집하는 방법을 보여줍니다.

핵심 요약

  • 페이지가 로드하는 JSON 대부분은 데이터가 아닙니다. 한 항공사의 항공편 검색 페이지에서는 요금이 44.9 KB짜리 응답 하나에 담겨 있었는데, 이는 페이지 JSON의 약 8%, 페이지 전체 전송량 3.6 MB의 약 1.2%에 불과했습니다.
  • 어떤 페이지는 데이터 API가 아예 없습니다. 한 국가 기상청의 예보 페이지에서는 모든 JSON 응답이 쿠키 동의 도구에 속했고, 예보 자체는 HTML 안에 있었습니다.
  • 엔드포인트는 URL을 추측하는 것이 아니라 페이지에서 눈으로 확인할 수 있는 값을 응답 본문에서 검색해서 찾습니다.
  • 페이지 API는 흔히 해당 페이지의 세션에 묶여 있습니다. 그 항공사의 요금 엔드포인트는 페이지 안에서는 작동했지만 직접 호출하면 HTTP 409를 반환했습니다.
  • 페이지가 익명 방문자를 위해 호출하는 공개 엔드포인트만, 사람이 직접 탐색하는 속도로 사용하고, 공식 API가 있다면 그쪽을 우선하십시오.

우리가 측정한 것

2026년 9월 30일, 실제 브라우저에서 공개 페이지 두 곳을 로드하여 모든 응답을 기록했습니다.

항공사 항공편 검색날씨 예보
요청 수6146
총 전송량3.62 MB2.57 MB
JSON 응답15개, 534 KB3개, 1.04 MB
데이터를 담은 JSON 응답1개, 44.9 KB0개
데이터가 있던 위치요금 엔드포인트서버 렌더링된 HTML

항공사 페이지에서 가장 큰 JSON 응답들은 전혀 요금이 아니었습니다. 한 서드파티 기능 플래그 서비스가 같은 97 KB 응답을 네 번 반환했고, 번역 번들이 92 KB를 추가했습니다. 요금은 항공사 자체 예약 API에서 온 단일 응답에 담겨 있었습니다.

날씨 페이지에서는 세 개의 JSON 응답 모두 동의 관리 도구에서 온 것이었고, 그중 하나는 860 KB짜리 벤더 목록이었으며, 예보 자체는 서버에서 HTML로 렌더링되어 있었습니다. “JSON에서 수집한다”는 방식은 아무 쓸모 있는 것도 수집하지 못했을 것입니다.

데이터를 담은 엔드포인트 찾기

신뢰할 수 있는 방법은 추측이 아니라 검색입니다. 가격, 상품명, 식별자처럼 페이지에서 눈으로 확인할 수 있는 값을 하나 골라, 실제 브라우저에서 페이지를 로드한 다음, 어떤 JSON 응답이 그 값을 담고 있는지 찾습니다.

수동으로는 브라우저 개발자 도구를 사용합니다. Network 탭을 열고 Fetch/XHR로 필터링한 뒤 새로고침하고, 응답 본문에서 값을 검색합니다. 자동화하면 짧은 스크립트로 처리할 수 있습니다.

import { chromium } from 'playwright';

// Load a page once and report which JSON responses contain a value you can see on it.
export async function findEndpoint(url, needle, waitMs = 20000) {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  const hits = [];
  page.on('response', async (response) => {
    const type = response.request().resourceType();
    const contentType = response.headers()['content-type'] || '';
    if (!['xhr', 'fetch'].includes(type) || !contentType.includes('json')) return;
    try {
      const body = await response.text();
      if (body.includes(needle)) {
        hits.push({
          method: response.request().method(),
          status: response.status(),
          bytes: Buffer.byteLength(body),
          url: response.url(),
        });
      }
    } catch {
      // Some responses (redirects, aborted requests) have no readable body.
    }
  });
  // Analytics beacons keep many pages from ever going network-idle,
  // so wait for the DOM, then until a match appears or the time runs out.
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 90000 });
  for (let waited = 0; waited < waitMs && hits.length === 0; waited += 500) {
    await page.waitForTimeout(500);
  }
  await page.waitForTimeout(1000);
  await browser.close();
  return hits;
}

항공사의 항공편 페이지와 그 위에서 눈에 보이는 요금 하나를 넣어 실행하자, 스크립트는 61건의 요청 중 정확히 하나의 일치, 즉 예약 API의 좌석 가용성 응답을 반환했습니다. 대기 전략에 주목하십시오. 첫 번째 버전은 페이지가 네트워크 유휴 상태가 될 때까지 기다렸지만, 분석용 비콘이 계속 발생하는 바람에 그런 상태는 오지 않았고, 90초 후 타임아웃되었습니다. DOM을 기다린 다음 일치 항목을 기다리는 방식이 더 안정적입니다.

직접 호출할 수 있는가?

대개는 불가능합니다. 페이지 없이 같은 항공사 가용성 엔드포인트를 직접 요청했을 때, 서로 다른 여섯 개 국가에서 보낸 요청 모두 HTTP 409를 반환했지만, 페이지 자체는 아무 문제 없이 이를 로드했습니다. 페이지 API는 흔히 페이지가 먼저 설정해 놓은 것들, 즉 세션 쿠키, HTML에 심어진 토큰, 요청 헤더, 이전 호출들의 순서에 의존합니다.

이렇게 되면 두 가지 접근법이 남습니다.

  • 페이지가 직접 호출하게 둔다. 브라우저에서 페이지를 로드하고, 위 스크립트처럼 도착하는 JSON 응답을 읽습니다. 세션을 재구성할 필요 없이 깔끔한 데이터를 얻을 수 있지만, 브라우저를 구동해야 하는 비용이 듭니다.
  • 단순하고 공개된 엔드포인트만 재현한다. 어떤 엔드포인트는 URL 하나만 있으면 됩니다. 같은 항공사는 평범한 요청에도 응답하는 공개 요금 엔드포인트도 함께 제공하는데, 이는 출국 국가에 따라 가격이 달라지는지를 별도로 테스트할 때 사용한 것입니다. 이렇게 단순하게 작동하는 엔드포인트가 있다면, 이는 단연 가장 저렴한 선택지입니다.

해서는 안 되는 일은 세션을 우회하는 것입니다. 토큰을 채취하거나, 헤더를 위조하거나, 사이트가 익명 방문자에게 제공하지 않은 데이터에 접근하려고 인증된 호출을 재현하는 것입니다. 그것은 관찰이 아니라 침해가 시작되는 지점입니다.

JSON이 있을 때 그것이 가치 있는 이유

데이터가 실제로 JSON으로 도착할 때, 얻는 이득은 실질적입니다.

  • 크기. 요금 응답은 44.9 KB였던 반면 전체 페이지는 3.6 MB였습니다. 대역폭 기준으로 과금되는 수집에서 이 차이는 청구서의 대부분을 차지하며, 이는 클린 레코드당 비용에서 보여주는 바와 같습니다.
  • 구조. 필드는 이름과 타입이 정해진 채로 도착하며, 작성하거나 유지 관리할 셀렉터가 없습니다.
  • 완전성. 응답은 흔히 페이지에 표시되는 것보다 더 많은 정보, 예를 들어 식별자, 모든 요금 등급, 재고 플래그 등을 담고 있습니다.

하지만 그만큼 실질적인 트레이드오프도 있습니다. 문서화되지 않은 엔드포인트는 예고 없이 변경되며, 형태가 바뀐 응답은 파서를 조용히 망가뜨립니다. 이는 정확히 스키마 드리프트 모니터링이 존재하는 이유이기도 합니다. 항공사 예약 API의 경로에 있는 v4처럼 경로에 버전 번호가 있는 것은 안정성을 어느 정도 암시할 뿐, 보장하는 것은 아닙니다.

대안들 사이에서 이 방법의 위치

소스안정성노력사용 시점
공식 문서화된 API최고최저항상 먼저 확인
JSON-LD 또는 내장된 페이지 상태높음낮음페이지가 이를 공개하는 경우, HTML 파싱을 멈추라 참고
페이지 자체의 JSON 엔드포인트중간중간데이터가 페이지 로드 이후에 로드되고, 엔드포인트가 공개된 경우
렌더링된 HTML과 셀렉터최저최고다른 방법으로는 데이터를 담을 수 없는 경우
페이지를 읽는 모델다양함사이트당 낮음, 페이지당 높음LLM 추출 대 셀렉터에서처럼 템플릿이 많은 경우

책임감 있게 수집하기

페이지가 호출하는 엔드포인트도 사이트의 일부이며, 사이트의 규칙은 여전히 적용됩니다. 페이지가 익명 방문자를 위해 호출하는 엔드포인트만 사용하고, 사람이 직접 탐색하는 속도로 요청 속도를 조절하고, robots.txt와 사이트 이용 약관을 존중하고, 허가가 없다면 로그인이나 토큰 뒤에 있는 것은 건드리지 마십시오. 사이트가 공식 API를 제공한다면 그것을 사용하십시오. 그쪽이 더 안정적이며, 사이트가 지원하기로 동의한 방식이기도 합니다. 더 폭넓은 원칙은 robots.txt, AI 옵트아웃과 예약 신호에 있습니다.

결론

현대적인 페이지 뒤에 있는 데이터는 흔히 JSON 형태로 도착하며, 그럴 경우 그곳에서 수집하는 것이 HTML을 파싱하는 것보다 더 작고, 더 깔끔하고, 유지하기도 더 쉽습니다. 하지만 그것을 찾으려면 추측이 아니라 검색이 필요합니다. 우리가 측정한 페이지들에서, 유용한 JSON은 수많은 응답 중 하나였고, 어떤 페이지에서는 아예 존재하지 않았습니다.

눈으로 확인할 수 있는 값을 응답 본문에서 검색하고, 엔드포인트가 단독으로 작동하는지 확인하고, 그렇지 않다면 페이지가 직접 호출하게 두고, 사이트가 익명 방문자 누구에게나 제공하는 범위 안에 머무르십시오.

출처 및 참고자료

시작할 준비가 되셨나요?

205M개 이상의 IP, 195개 이상의 국가를 지원하는 Shifter의 레지덴셜 프록시를 $0.10/GB부터 이용해보세요.

시작하기