Indeed에서 채용 공고 제목을 스프레드시트에 복사·붙여넣기만 하던 횟수가 쉰 번쯤 됐을 무렵, 저는 제 커리어 선택을 진지하게 의심하기 시작했습니다. Indeed에서 구조화된 데이터를 프로그램으로 가져오려 해본 적이 있다면, 결말은 이미 아실 겁니다. 403 오류는 버그가 아니라 Indeed의 방어 시스템이 제대로 작동하고 있다는 신호입니다.
Indeed는 전 세계 최대의 구인 플랫폼으로, , 한 시점에 를 보유하고 있으며, 에서 운영됩니다. 즉, 전 세계에서 가장 풍부한 채용 시장 데이터 소스 중 하나이자, 동시에 가장 크롤링하기 어려운 대상 중 하나라는 뜻입니다. 오픈소스 크롤러 JobFunnel(깃허브 스타 수천 개)은 반봇 방어와의 싸움에서 계속 밀린 끝에 2025년 12월 됐습니다. 관리자는 이렇게 말했습니다. "모든 사용자가 일부 공고는 수집할 수 있지만, 곧바로 캡차에 걸리고 크롤링은 실패하며 결국 공고를 얻지 못한다." 또 다른 기여자는 첫 요청부터 CAPTCHA가 떴다고 했습니다. 그러니 네, 이건 전혀 만만한 크롤링 대상이 아닙니다. 이 가이드에서는 Python으로 Indeed를 크롤링하는 실용적인 모든 방법을 하나씩 살펴보고, 403 방어막을 실제로 뚫는 방법을 보여드리겠습니다. 그리고 디버깅을 아예 건너뛰고 싶은 분들을 위해서는 을 활용한 노코드 대안도 소개합니다.
Python으로 Indeed를 크롤링한다는 것은 무슨 뜻일까?
웹 크롤링의 핵심은 웹페이지에서 구조화된 데이터를 자동으로 추출하는 것입니다. Python으로 Indeed를 크롤링한다는 말은, Indeed의 검색 결과 페이지와 채용 공고 상세 페이지를 방문하는 스크립트를 작성하고, 그 안의 HTML(또는 삽입된 데이터)을 읽어 직무명, 회사명, 지역, 연봉, 설명 같은 필드를 CSV나 데이터베이스, Google Sheets 등에서 쓸 수 있는 형태로 가져오는 것을 뜻합니다.
일반적으로 쓰이는 Python 라이브러리는 Requests(HTTP 요청용), BeautifulSoup(HTML 파싱용), Selenium 또는 Playwright(브라우저 자동화용)입니다. 하지만 Indeed는 단순한 정적 사이트가 아닙니다. 서버 렌더링 HTML에 JSON 상태 블롭이 함께 들어 있고, 앞단은 Cloudflare Bot Management가 막고 있는 하이브리드 구조입니다. 즉, 스크레이퍼는 JavaScript 렌더링 콘텐츠, 자주 바뀌는 CSS 클래스명, 강력한 반봇 보호를 모두 상대해야 하며, 그 전에 HTML에서 직무명 하나를 제대로 파싱할 수 있어야 합니다.
게다가 2026년 기준으로 Indeed에는 공식적인 무료 읽기 전용 API도 없습니다. 예전의 Publisher Jobs API는 2020년 무렵 폐기됐고, 현재 남아 있는 것은 고용주 전용 기능(Job Sync, Sponsored Jobs)뿐입니다. 결국 현실적인 선택지는 크롤링하거나, 제3자 데이터 제공업체에 비용을 지불하는 것뿐입니다.
왜 Indeed 채용 데이터를 크롤링할까?
Indeed를 크롤링할 이유는 분명합니다. 수천 개의 공고를 일일이 살펴보는 건 비현실적이고, 그 안에 들어 있는 데이터는 실제로 매우 가치가 높기 때문입니다.

| 활용 사례 | 이점을 보는 대상 | 예시 |
|---|---|---|
| 리드 발굴 | 영업팀 & 채용팀 | 채용 중인 기업 리스트와 연락처 구축 |
| 채용 시장 조사 | 분석가, HR팀 | 떠오르는 기술과 지역별 연봉 벤치마크 파악 |
| 경쟁사 인텔리전스 | 고용주, 인력소개소 | 경쟁사의 채용 패턴과 연봉 제안 모니터링 |
| 개인 구직 자동화 | 구직자 | 조건에 맞는 공고를 지역별로 한 번에 수집 |
| 머신러닝 모델 학습 데이터 | 데이터 과학자 | 과거 데이터를 활용한 연봉 예측 모델 구축 |
Indeed Hiring Lab의 자체 연구도 하고 있듯, 공고 데이터는 BLS JOLTS와 높은 상관관계를 보이며 미국 노동시장 상황을 거의 실시간으로 가늠하는 지표로 쓸 수 있습니다. 헤지펀드는 채용 공고 증가 속도를 대체 데이터 신호로 사용합니다. HR팀은 크롤링한 연봉 구간을 기준으로 보상 수준을 비교합니다. 채용 담당자는 적극적으로 채용 중인 기업을 바탕으로 잠재 고객 리스트를 만듭니다.
실무적으로 한 가지 짚고 넘어갈 점이 있습니다. Indeed의 연봉 데이터는 점점 나아지고 있지만 여전히 완전하지 않습니다. 2025년 중반 기준으로 미국 공고의 약 에 연봉 정보가 포함되지만, 그중 정확한 숫자를 제시하는 비율은 약 에 불과하고 나머지는 범위 형태입니다. Indeed 데이터를 기반으로 연봉을 분석할 때는 이런 결측을 반드시 고려해야 합니다.
Python으로 Indeed를 크롤링하는 방법 선택하기
Indeed를 크롤링하는 정답은 하나가 아닙니다. 가장 좋은 방식은 실력, 필요한 데이터 양, 유지보수에 얼마나 시간을 쓸 수 있는지에 따라 달라집니다. 저는 주요한 네 가지 방법을 모두 테스트해봤고, 비교해보면 다음과 같습니다.
This paragraph contains content that cannot be parsed and has been skipped.
이 가이드는 각 방법을 순서대로 설명합니다. Python 개발자라면 BS4, 숨겨진 JSON, Selenium 섹션을 중심으로 보시면 되고, 비개발자이거나 403 디버깅에 지쳤다면 Thunderbit 섹션으로 바로 넘어가셔도 됩니다.
시작하기 전에
- 난이도: 초급~중급(Python 섹션); 없음(Thunderbit 섹션)
- 소요 시간: Python 환경 설정 및 첫 크롤링까지 약 20~60분, Thunderbit은 약 2분
- 준비물: Python 3.9+, 코드 에디터, Chrome 브라우저, 그리고 노코드 경로를 원한다면
Indeed 크롤링을 위한 Python 환경 설정
크롤링 코드를 작성하기 전에 먼저 환경부터 준비합시다.
필요한 라이브러리 설치
가상환경을 만들고 필요한 패키지를 설치하세요.
1python -m venv indeed_env
2source indeed_env/bin/activate # Windows: indeed_env\Scripts\activate
3# HTTP + 파싱 방식용
4pip install requests beautifulsoup4 lxml httpx
5# 숨겨진 JSON 방식용(권장)
6pip install curl_cffi parsel tenacity
7# 브라우저 자동화 방식용
8pip install selenium
몇 가지 참고사항:
- **
curl_cffi**는 2026년 기준 Cloudflare 보호 사이트를 크롤링할 때 사실상의 표준입니다. 일반requests나httpx로는 흉내 낼 수 없는, 실제 브라우저 TLS 지문을 가장 잘 모방합니다. 왜 중요한지는 아래 반봇 섹션에서 더 설명하겠습니다. - **Selenium 4.6+**에는 Selenium Manager가 포함되어 있어 ChromeDriver를 따로 내려받을 필요가 없습니다. 브라우저 바이너리를 자동으로 관리합니다.
- BeautifulSoup의 파서로는 **
lxml**을 사용하는 것이 좋습니다. 표준 라이브러리html.parser보다 대략 .
프로젝트 구조 만들기
복잡하게 갈 필요 없습니다.
1indeed_scraper/
2├── scraper.py
3├── requirements.txt
4└── output/
아래의 모든 코드 예시는 scraper.py를 기준으로 설명합니다.
BeautifulSoup으로 Python에서 Indeed 크롤링하는 방법
가장 입문자 친화적인 방식입니다. requests로 페이지를 가져오고 BeautifulSoup으로 HTML을 파싱합니다. 설정은 가장 쉽지만, Indeed에서는 가장 쉽게 깨지기도 합니다.
1단계: Indeed 검색 URL 만들기
Indeed의 검색 URL은 다음과 같은 패턴을 따릅니다.
1https://www.indeed.com/jobs?q=<query>&l=<location>&start=<offset>
예를 들어 Austin, TX에서 "data analyst"를 검색해 첫 페이지를 가져오려면:
1from urllib.parse import urlencode
2> This paragraph contains content that cannot be parsed and has been skipped.
3Indeed는 10개 단위로 페이지를 나누며, 결과 수는 1,000개가 상한입니다(`start <= 990`). 990보다 큰 오프셋을 주면 조용히 같은 페이지가 다시 반환됩니다.
4### 2단계: 적절한 헤더로 HTTP 요청 보내기
5기본 Python User-Agent 문자열로는 Indeed가 바로 막습니다. 현실적인 헤더를 사용해야 합니다.
6```python
7import requests
8> This paragraph contains content that cannot be parsed and has been skipped.
9response = requests.get(url, headers=headers, timeout=30)
10print(response.status_code)
200이 나오면 일단 성공입니다. 하지만 403이 나오면 Cloudflare에 걸린 것입니다. 이 문제를 피하는 방법은 아래에서 더 설명하겠습니다.
3단계: HTML에서 채용 공고 파싱하기
BeautifulSoup으로 채용 카드 요소를 선택합니다. Indeed의 랜덤 CSS 클래스보다 data-testid 속성을 타깃으로 잡는 편이 더 안정적입니다.
1from bs4 import BeautifulSoup
2> This paragraph contains content that cannot be parsed and has been skipped.
3> This paragraph contains content that cannot be parsed and has been skipped.
4> This paragraph contains content that cannot be parsed and has been skipped.
5print(f"발견된 공고 수: {len(jobs)}")
4단계: 페이지네이션 처리하기
start 파라미터를 증가시키면서 페이지를 반복하면 됩니다.
1import time, random
2all_jobs = []
3for page in range(0, 50, 10): # 처음 5페이지
4 params["start"] = page
5 url = f"https://www.indeed.com/jobs?{urlencode(params)}"
6 response = requests.get(url, headers=headers, timeout=30)
7 # ... 위와 같이 파싱 ...
8 all_jobs.extend(jobs)
9 time.sleep(random.uniform(3, 6))
이 방식의 한계
솔직히 말해, BS4 + Requests는 2026년 기준 Indeed에서 가장 약한 방법입니다. 일반 requests는 Python 표준 TLS 라이브러리를 사용하기 때문에, Cloudflare가 즉시 "브라우저가 아닌 요청"으로 인식하는 을 남깁니다. 또한 Indeed가 사용하는 HTTP/2도 제대로 지원하지 못합니다. 결과적으로 몇 페이지 넘기지 못하고 막힐 가능성이 큽니다. 그리고 CSS 셀렉터는요? Indeed는 css-1m4cuuf, jobsearch-JobComponent-embeddedBody-1n0gh5s 같은 클래스명을 바꿉니다. 이런 셀렉터는 시한폭탄과 다를 바 없습니다.
이 방법은 한 페이지짜리 빠른 프로토타입에만 쓰세요. 규모 있게 처리하려면 숨겨진 JSON 방식을 사용해야 합니다.
숨겨진 JSON 데이터를 사용해 Python으로 Indeed 크롤링하기
대부분의 Python 개발자에게 제가 가장 추천하는 방법입니다. 깨지기 쉬운 HTML 요소를 파싱하는 대신, Indeed 페이지 소스에 포함된 JavaScript 변수 window.mosaic.providerData["mosaic-provider-jobcards"]에서 구조화 데이터를 꺼내오는 방식입니다.
직무명, 회사, 지역, 연봉, job key, 게시일, 원격 여부 등 관심 있는 거의 모든 필드가 이 JSON 블롭 안에 이미 들어 있습니다. JavaScript 실행도 필요 없습니다. 스키마는 이어서 DOM 셀렉터보다 훨씬 견고합니다.
1단계: 페이지 HTML 가져오기
requests 대신 curl_cffi를 사용하세요. 실제 브라우저 TLS 지문을 모방하므로 Cloudflare를 견디는 데 매우 중요합니다.
1from curl_cffi import requests as cffi_requests
2> This paragraph contains content that cannot be parsed and has been skipped.
3왜 `curl_cffi`일까요? 이 라이브러리는 curl-impersonate 위에 만들어진 Python 바인딩으로, 실제 브라우저와 동일한 TLS ClientHello, HTTP/2 SETTINGS 프레임, 헤더 순서를 재현합니다. [JA3/JA4와 Akamai H2 지문을 한 번의 호출로 우회할 수 있는](https://github.com/lexiforest/curl_cffi) 현재 유지보수되는 유일한 Python HTTP 클라이언트입니다. 지원하는 impersonation 대상은 `chrome120`, `chrome124`, `chrome131`, Safari, Edge 변형 등이 있습니다.
4### 2단계: 정규식으로 JSON 추출하기
5이 JSON 블롭은 `<script>` 태그 안에 들어 있습니다. 정규식으로 꺼내면 됩니다.
6```python
7import re, json
8MOSAIC_RE = re.compile(
9 r'window\.mosaic\.providerData\["mosaic-provider-jobcards"\]=(\{.+?\});',
10 re.DOTALL,
11)
12match = MOSAIC_RE.search(response.text)
13if match:
14 data = json.loads(match.group(1))
15 results = data["metaData"]["mosaicProviderJobCardsModel"]["results"]
16 print(f"숨겨진 JSON에서 발견된 공고 수: {len(results)}")
17else:
18 print("숨겨진 JSON을 찾지 못했습니다. 차단됐거나 페이지 구조가 바뀌었을 수 있습니다.")
3단계: JSON에서 공고 필드 파싱하기
results의 각 항목에는 페이지에 보이는 것보다 더 많은 데이터가 들어 있습니다.
1jobs = []
2for job in results:
3 jobs.append({
4 "jobkey": job["jobkey"],
5 "title": job["title"],
6 "company": job.get("company"),
7 "location": job.get("formattedLocation"),
8 "remote": job.get("remoteLocation"),
9 "salary": (job.get("salarySnippet") or {}).get("text"),
10 "posted": job.get("formattedRelativeTime"),
11 "job_type": job.get("jobTypes"),
12 "easy_apply": job.get("indeedApplyEnabled"),
13 "url": f"https://www.indeed.com/viewjob?jk={job['jobkey']}",
14 })
이 JSON에는 페이지에 항상 보이지 않는 연봉 추정치, taxonomy 속성(기술 태그), 회사 평점까지 포함되는 경우가 많습니다.
4단계: 여러 페이지 크롤링하기
JSON 안의 tierSummaries를 활용해 전체 결과 수를 파악한 뒤 반복하면 됩니다.
1import time, random
2> This paragraph contains content that cannot be parsed and has been skipped.
3print(f"총 수집한 공고 수: {len(all_jobs)}")
숨겨진 JSON 방식이 더 견고한 이유
window.mosaic.providerData 구조는 CSS 클래스명보다 훨씬 덜 자주 바뀝니다. 지저분한 HTML을 해석하지 않고도 깔끔한 구조화 데이터를 얻을 수 있습니다. 다만 헤더, 딜레이, 프록시 같은 반봇 대응은 여전히 필요합니다. 이는 다음 섹션에서 다룹니다.
Selenium으로 Python에서 Indeed 크롤링하는 방법
Selenium은 브라우저 자동화 방식입니다. 상세 페이지 패널을 클릭해서 들어가야 하거나, 로그인 상태가 필요한 콘텐츠를 처리해야 하거나, 초기 HTML에 없는 동적 설명을 수집해야 할 때 유용합니다.
HTTP 클라이언트 대신 Selenium을 써야 하는 경우
- Indeed가 일부 콘텐츠를 동적으로 로딩하는 경우(오른쪽 패널의 전체 설명 등)
- 세션 상태나 로그인이 필요한 페이지를 크롤링해야 하는 경우
- 속도가 중요하지 않은 소규모 수집 작업인 경우
간단한 실행 예시
1from selenium import webdriver
2from selenium.webdriver.common.by import By
3from selenium.webdriver.chrome.options import Options
4import time
5options = Options()
6options.add_argument("--disable-blink-features=AutomationControlled")
7# options.add_argument("--headless=new") # 헤드리스는 더 쉽게 탐지됩니다 — 주의해서 사용하세요
8driver = webdriver.Chrome(options=options)
9driver.get("https://www.indeed.com/jobs?q=data+engineer&l=New+York")
10time.sleep(3)
11cards = driver.find_elements(By.CSS_SELECTOR, "[data-testid='slider_item']")
12for card in cards:
13 try:
14 title = card.find_element(By.CSS_SELECTOR, "h2.jobTitle").text
15 company = card.find_element(By.CSS_SELECTOR, "[data-testid='company-name']").text
16 location = card.find_element(By.CSS_SELECTOR, "[data-testid='text-location']").text
17 print(f"{title} | {company} | {location}")
18 except Exception:
19 continue
20driver.quit()
한계
Selenium은 느립니다. 페이지마다 브라우저 전체를 렌더링해야 하기 때문입니다. 헤드리스 Chrome은 (Cloudflare는 navigator.webdriver, WebGL 벤더 문자열, 플러그인 개수 등을 확인합니다). undetected-chromedriver를 써도 탐지를 잠시 늦출 뿐, 영구적으로 막아주지는 않습니다. 그리고 BS4와 마찬가지로 Indeed가 UI를 바꾸면 셀렉터도 깨집니다.
대부분의 경우 숨겨진 JSON 방식이 같은 데이터를 더 빠르고 유지보수 부담 적게 가져옵니다. 진짜로 브라우저가 필요한 특수 상황에서만 Selenium을 쓰세요.
Python으로 Indeed를 크롤링할 때 403 오류를 피하는 방법
이 섹션이 가장 중요합니다. 구글 검색을 헤매다 여기까지 오셨다면 잘 찾아오셨습니다.

Indeed가 스크레이퍼를 막는 이유
Indeed는 을 사용합니다. DataDome도 아니고, PerimeterX도 아닙니다. 응답 헤더만 봐도 확인할 수 있습니다. server: cloudflare, cf-ray, 그리고 봇 관리 쿠키 __cf_bm이 보입니다. Cloudflare는 TLS 지문(JA3/JA4), HTTP/2 헤더 순서, 요청 패턴, 브라우저 행동 신호를 검사합니다. 이 중 하나라도 사람 같지 않으면 403, 429, 503이 뜨거나, 더 교묘하게는 실제 공고 대신 Turnstile 챌린지 페이지가 포함된 200 OK가 돌아올 수도 있습니다.
User-Agent와 요청 헤더를 회전시키기
고정된 하나의 User-Agent를 쓰는 건 가장 빠르게 차단되는 방법입니다. 현실적인 최신 문자열 풀에서 랜덤으로 바꾸세요. 중요한 점 하나: Chrome의 마이너 버전 필드는 User-Agent Reduction 이후 되어 있습니다. 존재하지 않는 마이너 버전을 만들어 넣으면 반봇 시스템이 바로 잡아냅니다.
1import random
2USER_AGENTS = [
3 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
4 "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
5 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
6 "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
7 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
8 "(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.3800.97",
9 "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
10 "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 "
11 "(KHTML, like Gecko) Version/17.4 Safari/605.1.15",
12]
13> This paragraph contains content that cannot be parsed and has been skipped.
14또한 `sec-ch-ua` Client Hints가 UA 버전과 일치하는지도 확인해야 합니다. User-Agent는 Chrome 145인데 `sec-ch-ua: "Chrome";v="131"`처럼 서로 안 맞으면 바로 의심 신호입니다.
15### 요청 사이에 랜덤 지연 넣기
16고정 간격은 패턴 탐지에 쉽게 걸립니다. 랜덤한 지터를 넣어야 합니다.
17```python
18import time, random
19# 각 요청 사이
20time.sleep(random.uniform(3, 6))
21# 차단 후 재시도 시
22def backoff_sleep(attempt):
23 base = 4
24 sleep_time = base * (2 ** attempt) + random.uniform(0, 2)
25 time.sleep(min(sleep_time, 60))
와 의 실무 공감대는 IP당 요청 간 3~6초 간격, 세션당 IP당 대략 100요청을 넘기기 전에 회전하는 방식입니다.
프록시 회전 사용하기
성공 여부를 좌우하는 가장 큰 요소는 이것입니다. AWS/GCP 같은 데이터센터 프록시는 Cloudflare Enterprise 대상에서 성공률이 약 5~15% 수준이라 사실상 쓸모가 없습니다. 반면, 올바른 TLS 지문과 함께 Residential 프록시를 사용하면 성공률이 80~95%까지 올라갑니다.
1PROXIES = [
2 "http://user:pass@us.residential.example:7777",
3 "http://user:pass@us.residential.example:7778",
4 "http://user:pass@us.residential.example:7779",
5]
6> This paragraph contains content that cannot be parsed and has been skipped.
72026년 기준 Residential 프록시 가격은 공급자와 약정 수준에 따라 대략 [GB당 4~8.50달러](https://aimultiple.com/proxy-pricing) 정도입니다. Indeed용으로는 작은 풀부터 시작해 필요할 때 확장하는 것이 좋습니다.
8### 403, 429, 503 상태 코드를 적절하게 처리하기
9무작정 재시도하지 마세요. 상태 코드마다 의미가 다릅니다.
10```python
11def fetch_with_retry(url, proxy_pool, max_retries=5):
12 for attempt in range(max_retries):
13 proxy = random.choice(proxy_pool)
14 headers["User-Agent"] = random.choice(USER_AGENTS)
15 try:
16 r = cffi_requests.get(
17 url,
18 impersonate=random.choice(["chrome124", "chrome120", "edge101"]),
19 headers=headers,
20 proxies={"https": proxy},
21 timeout=30,
22 )
23 # 교묘한 "200이지만 챌린지 페이지" 케이스 확인
24 if r.status_code == 200 and "cf-turnstile" not in r.text and "Just a moment" not in r.text:
25 return r
26 if r.status_code == 403:
27 print(f"403 — 차단됨. 프록시 교체 중, 시도 {attempt + 1}")
28 elif r.status_code == 429:
29 print(f"429 — 요청 제한에 걸림. 속도를 늦춥니다.")
30 elif r.status_code == 503:
31 print(f"503 — 서버 과부하 또는 JS 챌린지입니다.")
32 backoff_sleep(attempt)
33 except Exception as e:
34 print(f"요청 오류: {e}")
35 backoff_sleep(attempt)
36 raise RuntimeError(f"{max_retries}번 재시도 후 실패: {url}")
가장 까다로운 건 200이지만 실제로는 챌린지 페이지가 오는 경우입니다. 200으로 처리하기 전에 반드시 응답 본문에서 cf-turnstile 또는 Just a moment 같은 표시를 검사하세요.
더 쉬운 대안: Thunderbit이 반봇 처리를 대신하게 하기
프록시 풀, 헤더 회전, TLS 지문 모방을 직접 만들고 유지하고 싶지 않다면, 의 클라우드 크롤링이 CAPTCHA, 프록시 회전, 반봇 보호를 자동으로 처리합니다. 프록시 설정도 없고, curl_cffi 설정도 없고, CAPTCHA 해결 라이브러리도 필요 없습니다. 그냥 데이터만 필요할 때 가장 부담이 적은 방법입니다.
왜 내 Indeed 스크레이퍼는 계속 깨질까? 그리고 어떻게 고칠까?
403 방화벽은 급성 통증입니다. 만성 통증은 유지보수입니다. 오늘은 잘 작동하던 스크레이퍼가 다음 주에는 조용히 빈 데이터나 오래된 결과만 돌려줄 수 있습니다.
Indeed가 셀렉터를 깨는 방식
Indeed는 CSS 클래스명을 매우 공격적으로 바꿉니다. Bright Data 가이드도 css-1m4cuuf, css-1rqpxry 같은 클래스가 "무작위로 생성된 듯 보이며, 빌드 시점에 만들어졌을 가능성이 높다"고 합니다. A/B 테스트 때문에 서로 다른 세션이 서로 다른 레이아웃을 볼 수 있고, DOM 구조도 예고 없이 바뀝니다.
JobFunnel 사례는 이를 잘 보여줍니다. 한 기여자는 이렇게 보고했습니다. "CaptchaBuster는 캡차를 성공적으로 완화했지만, 여전히 페이지를 제대로 가져오지 못한 이유는 오래된 beautiful soup 셀렉터 때문입니다." 즉, 막힌 게 아니라 잘못된 요소를 파싱하고 있었던 겁니다.
전략: DOM 파싱보다 숨겨진 JSON을 우선하기
window.mosaic.providerData 블롭은 적어도 2023년부터 스키마가 안정적이었습니다. metaData.mosaicProviderJobCardsModel.results[] 경로는 2026년에도 입니다. DOM 셀렉터는 매달 깨질 수 있지만, JSON 추출은 많아야 1년에 한 번 정도 깨집니다.
전략: 클래스명보다 data 속성 사용하기
DOM을 반드시 써야 할 때는 기능적 속성을 타깃으로 하세요.
| 셀렉터 | 용도 |
|---|---|
[data-testid="slider_item"] | 각 채용 공고 카드 컨테이너 |
[data-testid="job-title"] 또는 h2.jobTitle > a | 직무명 링크 |
[data-testid="company-name"] | 회사명 |
[data-testid="text-location"] | 위치 텍스트 |
각 카드의 data-jk="<jobkey>" | 가장 안정적인 훅 중 하나 — 2019년 이후 거의 변하지 않음 |
셀렉터가 오래됐는지 잡아내는 assertion 추가하기
스크레이퍼가 조용히 0건으로 돌아가게 두지 마세요. 매 요청 후 확인 절차를 넣으세요.
1results = parse_hidden_json(html)
2assert len(results) > 0, (
3 f"Indeed가 start={start}에서 빈 결과를 반환했습니다 — "
4 "차단, CAPTCHA, 또는 셀렉터 변경 가능성 있음. "
5 f"응답 앞 500자: {html[:500]}"
6)
실패 시 원본 응답의 앞 500~2000자를 기록해 두면, Turnstile 챌린지인지, 로그인 차단인지, 스키마 변경인지 바로 알 수 있습니다. q=python&l=remote 같은 고정 쿼리로 매일 CI 수준의 스모크 테스트를 돌려 비정상적으로 0건이 나오는지 확인하는 것도 좋습니다.
AI 대안: 깨지지 않는 스크레이퍼
Thunderbit의 AI는 매번 페이지 구조를 새로 읽기 때문에 하드코딩된 셀렉터나 정규식에 의존하지 않습니다. Indeed가 HTML을 바꿔도 Thunderbit은 자동으로 적응합니다. 이는 커뮤니티에서 늘 가장 큰 고충으로 꼽는 유지보수 문제를 직접 해결해 줍니다. "스크레이퍼가 또 빈 행만 돌려준다"는 Slack 메시지를 받아본 적이 있다면, 유지보수 없는 도구의 가치가 얼마나 큰지 잘 아실 겁니다.
Python 없이 Indeed 크롤링하기: 노코드 대안
대부분의 경쟁 가이드는 Python 코드를 작성하는 걸 전제로 합니다. 하지만 포럼의 실제 사용자 반응은 다릅니다. 사람들은 *"버그와 오류가 계속 나서 너무 어렵다"*고 말하고, 어떤 이들은 단지 데이터를 얻기 위해 Fiverr에서 사람을 고용하자고까지 합니다. 이게 당신 이야기 같다면, 이 섹션이 탈출구입니다.
Thunderbit로 Indeed를 크롤링하는 방법(단계별)
1단계: Chrome 웹 스토어에서 을 설치합니다. 무료로 시작할 수 있습니다.
2단계: 브라우저에서 Indeed 검색 결과 페이지로 이동합니다. 예를 들어 https://www.indeed.com/jobs?q=data+analyst&l=Austin%2C+TX 같은 페이지입니다.
3단계: 브라우저 툴바에서 Thunderbit 아이콘을 클릭한 다음 **"AI Suggest Fields"**를 누릅니다. Thunderbit의 AI가 페이지를 스캔해 Job Title, Company, Location, Salary, Job URL, Posted Date 같은 컬럼을 자동으로 감지합니다. 필요 없는 컬럼은 지우고, 원하는 항목은 자연어로 설명해 추가할 수도 있습니다.
4단계: **"Scrape"**를 클릭합니다. Thunderbit이 페이지에서 데이터를 추출해 구조화된 표로 보여줍니다. 설정한 필드가 포함된 채용 공고 행들이 보일 것입니다.
하위 페이지까지 확장하기
목록 페이지를 수집한 뒤에는 **"Scrape Subpages"**를 클릭해 각 개별 채용 상세 페이지를 방문하게 할 수 있습니다. 그러면 전체 채용 설명, 자격 요건, 복리후생, 지원 링크까지 가져옵니다. /viewjob?jk=<jobkey> URL을 하나씩 방문하는 두 번째 Python 스크레이퍼를 작성한 것과 같지만, 실제로는 클릭 한 번이면 됩니다.
페이지네이션도 자동 처리
Thunderbit은 Indeed의 클릭 기반 페이지네이션을 자동으로 처리합니다. 오프셋 URL을 직접 만들거나 반복문을 작성할 필요가 없습니다. 페이지를 넘기면서 결과를 자동으로 합쳐줍니다.
즐겨 쓰는 도구로 내보내기
수집한 데이터는 CSV, Excel, Google Sheets, Airtable, Notion으로 내보낼 수 있습니다. csv.writer()나 pandas.to_csv()를 직접 쓸 필요가 없습니다.
Python과 Thunderbit, 언제 무엇을 써야 할까?
| 상황 | 가장 적합한 도구 |
|---|---|
| 사용자 맞춤 데이터 파이프라인, cron/Airflow를 활용한 예약 자동화 | Python |
| 더 큰 코드베이스와의 통합 | Python |
| 매우 세분화된 파싱 로직 | Python |
| 일회성 리서치 또는 시장 분석 | Thunderbit |
| 비기술 팀원이 데이터를 필요로 할 때 | Thunderbit |
| 403 디버깅 없이 지금 바로 데이터가 필요할 때 | Thunderbit |
| 추가 설정 없이 하위 페이지 확장이 필요할 때 | Thunderbit |
시간 비교를 해보면, Python 설정 + 반봇 디버깅은 몇 시간에서 며칠까지 걸릴 수 있습니다(특히 처음이라면 더 그렇습니다). Thunderbit은 같은 데이터를 2분 안에 끝낼 수 있습니다. Python이 틀렸다는 말이 아니라, 필요한 상황이 다르다는 뜻입니다.
Indeed 크롤링은 합법일까? 알아야 할 점
상위권 Indeed 크롤링 가이드 중 상당수는 합법성 문제를 다루지 않는데, 포럼에서 *"Indeed를 크롤링하는 게 합법인가요?"*라는 질문이 얼마나 자주 나오는지 생각하면 의외입니다. 아래 내용은 법률 자문은 아니지만, 현 상황을 정리해드리겠습니다.
Indeed의 이용약관
Indeed의 ToS()에는 포괄적인 "크롤링 금지" 조항이 없습니다. 명시적인 자동화 금지 조항은 A.3.5조뿐이며, *"Indeed Apply 프로세스를 자동화하기 위한 어떤 자동화, 스크립팅 또는 봇의 사용"*을 금지합니다. 이는 공개 채용 공고를 수동적으로 읽는 행위가 아니라 Apply 흐름에 한정된 조항입니다. Indeed의 주된 집행 수단은 법정이 아니라 기술적 차단(Cf. Cloudflare 챌린지, IP 차단, 디바이스 지문 추적)입니다.
관련 판례
미국에서 가장 자주 인용되는 사건은 hiQ Labs v. LinkedIn입니다. 9th Circuit은 2022년 4월 고 판시했습니다. 다만 hiQ는 이후 가짜 LinkedIn 프로필을 만들고 ToS에 동의한 직원들 때문에 을 졌습니다.
더 최근에는 Meta v. Bright Data(미 연방 캘리포니아 북부지법, 2024년 1월)에서 더 분명한 판단이 나왔습니다. Chen 판사는 Facebook과 Instagram의 이용약관이 "로그아웃 상태에서의 공개 데이터 크롤링을 금지하지 않는다"고 했습니다. Meta는 다음 달 나머지 주장을 자진 취하했습니다.
Indeed의 robots.txt
Indeed의 는 기본 User-agent: *에 대해 /jobs/와 /job/ 경로를 광범위하게 막지만, Googlebot과 Bingbot에게는 /viewjob?의 개별 채용 상세 페이지 접근을 허용합니다. GPTBot, CCBot, anthropic-ai 같은 AI 학습용 크롤러는 강하게 제한됩니다. robots.txt는 미국에서 법적 구속력이 있는 것은 아니지만, 이를 존중하는 것은 좋은 관행이자 선의의 증거입니다.
책임 있는 크롤링을 위한 실무 지침
- 공개적으로 접근 가능한 데이터만 수집하세요. 로그인하거나 가짜 계정을 만들지 마세요
- 속도 제한을 지키세요: IP당 3~6초에 1회 요청, 동시성은 낮게 유지
- 크롤링한 데이터를 본인만의 구인 사이트처럼 재게시하지 마세요
- 개인 또는 내부 연구 목적에만 사용하고, 허가 없이 상업적 재판매는 하지 마세요
- 필요 없는 PII는 폐기하거나 해시 처리하고, 개인 관련 데이터의 보관 기간을 제한하세요
- 대규모로 운영하거나 EU/UK에서 다룰 경우 변호사와 상담하세요. GDPR 제14조의 투명성 의무가 크롤링된 개인정보에 적용될 수 있습니다
위험도는 대체로 이렇습니다. 개인 구직 자동화는 낮은 편이고, 대규모 상업적 재판매는 높은 편입니다.
결론 및 핵심 요약
Python으로 Indeed를 크롤링하는 건 가능합니다. 다만 주말 프로젝트처럼 대충 만들어 놓고 방치할 수 있는 일은 아닙니다. Indeed의 Cloudflare 보호, 자주 바뀌는 셀렉터, 공격적인 반봇 조치는 올바른 도구와 올바른 기대치로 접근해야 한다는 뜻입니다.
이 글에서 꼭 기억할 점은 다음과 같습니다.
- Indeed는 웹에서 가장 풍부한 채용 시장 데이터 소스입니다 — 월간 방문자 3억 5천만 명, 공고 1억 3천만 개 수준이지만, 스크레이퍼와는 치열하게 싸웁니다.
- 숨겨진 JSON 추출(
window.mosaic.providerData)이 Python에서 가장 견고한 방법입니다. 스키마는 수년간 안정적이지만 CSS 셀렉터는 매달 깨집니다. curl_cffi와 브라우저 impersonation이 2026년 기준 Cloudflare 보호 사이트용 HTTP 클라이언트의 표준입니다. 일반requests와httpx는 TLS 지문만으로도 차단됩니다.- 403 오류를 피하려면 반드시 헤더 회전, 랜덤 딜레이, Residential 프록시를 사용하세요. 데이터센터 프록시는 Cloudflare Enterprise 앞에서 거의 무용지물입니다.
- assertion 체크를 넣어야 셀렉터가 깨졌는지, 아니면 챌린지 페이지를 받고 있는지 즉시 알 수 있습니다.
- 비기술 사용자이거나 그냥 빨리 결과가 필요한 사람이라면, 이 프록시도, 디버깅도, 유지보수도 필요 없는 노코드 AI 방식으로 사이트 변화에 자동 적응합니다.
노코드 경로를 시험해보고 싶다면, 을 제공하므로 부담 없이 Indeed에서 테스트할 수 있습니다. Python 경로를 선택한다면 위 코드 예시가 좋은 출발점이 될 것입니다. 다만 반봇 대응을 나중에 할 일로 미루지 말고, 처음부터 핵심 요소로 다뤄야 한다는 점만 기억하세요.
웹 크롤링 접근법과 도구에 대해 더 알고 싶다면, , , 가이드를 확인해 보세요. 에서 튜토리얼도 볼 수 있습니다.
자주 묻는 질문
Indeed 크롤링에 가장 좋은 Python 라이브러리는 무엇인가요?
HTTP 요청용으로는 2026년 기준 curl_cffi가 가장 강력합니다. 실제 브라우저 TLS 지문을 모방하므로 Cloudflare 우회에 필수적입니다. 보호가 덜한 대상에는 HTTP/2를 지원하는 httpx도 괜찮은 대안입니다. HTML 파싱에는 lxml과 함께 쓰는 BeautifulSoup4가 여전히 표준입니다. 브라우저 자동화에는 playwright-stealth를 붙인 Playwright나 undetected-chromedriver를 사용할 수 있지만, 둘 다 점점 더 탐지되기 쉬워지고 있습니다. 숨겨진 JSON 정규식 방식(window.mosaic.providerData)을 쓰면 무거운 파싱 자체를 피할 수 있습니다.
Indeed를 크롤링할 때 왜 계속 403 오류가 뜨나요?
Indeed는 Cloudflare Bot Management를 사용하며, TLS 지문(JA3/JA4), HTTP/2 헤더 순서, 요청 패턴, 브라우저 행동을 검사합니다. 일반 requests를 쓰면 TLS 지문만으로도 Python 스크립트라는 게 바로 드러나서, 헤더를 읽기도 전에 403이 뜹니다. 해결하려면 브라우저 impersonation이 가능한 curl_cffi로 바꾸고, 현실적인 User-Agent를 회전하며, 랜덤 딜레이(3~6초)를 넣고, Residential 프록시를 사용하세요. 또 cf-turnstile가 들어간 "200이지만 챌린지 페이지"도 반드시 확인해야 합니다.
코딩 없이 Indeed를 크롤링할 수 있나요?
네. 같은 도구를 쓰면 몇 번의 클릭만으로 Indeed 채용 공고를 추출할 수 있습니다. Chrome 확장 프로그램을 설치하고, Indeed 검색 페이지로 이동한 뒤, "AI Suggest Fields"를 클릭하고, 이어서 "Scrape"를 누르면 됩니다. Thunderbit의 AI가 직무명, 회사, 지역, 연봉 같은 필드를 자동 감지합니다. 페이지네이션, 하위 페이지 확장(전체 직무 설명), 반봇 보호도 자동으로 처리합니다. CSV, Google Sheets, Airtable, Notion으로 무료 내보내기도 가능합니다.
Indeed의 HTML 구조는 얼마나 자주 바뀌나요?
Indeed는 CSS 클래스명(예: css-1m4cuuf 같은 무작위 해시 문자열)을 자주 바꾸고, DOM 요소 구조도 예고 없이 재구성합니다. A/B 테스트 때문에 서로 다른 사용자에게 동시에 다른 레이아웃이 보일 수 있습니다. 숨겨진 JSON 방식(window.mosaic.providerData)은 훨씬 안정적이며, 스키마도 적어도 2023년부터 일관적이었습니다. DOM 셀렉터를 써야 한다면 CSS 클래스보다 data-testid와 data-jk(job key)를 타깃으로 하세요.
Indeed를 크롤링하는 것은 합법인가요?
미국 기준으로, 공개적으로 접근 가능한 Indeed 공고 URL을 로그아웃 상태에서 크롤링하는 행위는 2022년 9th Circuit의 hiQ v. LinkedIn 판결과 2024년 Meta v. Bright Data 결정에 비추어 CFAA 책임을 만들 가능성이 낮습니다. Indeed의 ToS는 공개 목록을 수동으로 읽는 행위가 아니라 Apply 프로세스 자동화만 명시적으로 금지합니다. 다만 항상 책임 있게 크롤링하세요. 로그인하지 말고, 가짜 계정을 만들지 말고, 속도 제한을 지키고, 데이터를 자체 구인 사이트처럼 재게시하지 말고, 이메일·이름 같은 개인 데이터는 GDPR/CCPA 관점에서 신중하게 다뤄야 합니다. 상업적 규모로 운영한다면 변호사와 상담하세요.
더 알아보기
