크롤링을 하다 보면 브라우저에서는 분명 값이 보이는데, requests로 가져오면 빈 결과만 나오는 순간이 있습니다. 저도 처음에는 선택자를 잘못 적은 줄 알았지만, 실제 원인은 페이지가 처음부터 HTML에 데이터를 담고 있지 않고 JavaScript 실행 이후에 값을 만들어 붙이는 구조였습니다. 이번 글에서는 제가 직접 겪은 흐름대로, 왜 브라우저에서는 보이는데 코드에서는 안 보였는지 확인하고, 언제 requests를 유지하고 언제 Selenium으로 바꿔야 하는지 정리하겠습니다.
브라우저에서는 보이는데 requests에서는 안 보였던 이유를 찾았습니다
처음 문제는 단순했습니다. 브라우저로 페이지를 열면 제목, 가격, 목록 데이터가 분명히 보였는데, 파이썬에서 requests.get()으로 받아 response.text를 확인해 보니 정작 필요한 값이 없었습니다. 처음에는 선택자를 잘못 적었거나 인코딩 문제라고 생각했습니다. 그래서 BeautifulSoup로 여러 번 find()와 select()를 바꿔 봤지만 결과는 같았습니다. 눈으로는 보이는데 코드에서는 안 보이는 상황이 반복되니, 어느 순간부터는 내가 잘못 찾는 것이 아니라 아예 HTML 안에 값이 없는 것일 수 있다는 생각이 들었습니다.
그때 확인한 것이 브라우저의 개발자도구였습니다. 페이지 소스를 대충 훑어보는 것만으로는 부족했고, 실제로 네트워크 요청과 Elements 구조를 같이 봐야 했습니다. 확인해 보니 처음 내려오는 HTML 문서에는 제가 찾던 데이터가 없었고, 페이지가 열린 뒤 JavaScript가 별도 요청을 보내 데이터를 받아 화면에 그리는 구조였습니다. 즉, 브라우저가 보여주는 최종 화면과 requests가 받는 초기 HTML이 서로 다른 상태였던 것입니다. 이 차이를 모르고 있으면 브라우저에서 보이는 내용을 곧바로 크롤링할 수 있을 것이라고 착각하기 쉽습니다.
이 문제를 겪고 나서 정적 페이지와 동적 페이지를 구분하는 기준이 훨씬 분명해졌습니다. 정적 페이지는 서버가 HTML 안에 필요한 값을 이미 넣어서 보내기 때문에 requests와 BeautifulSoup만으로도 충분합니다. 반대로 동적 페이지는 처음 문서에는 뼈대만 있고, 실제 데이터는 JavaScript가 나중에 API를 호출해 채워 넣습니다. 이런 경우 response.text에 값이 없다고 해서 무조건 실패가 아니라, 접근 방식이 달라야 한다는 뜻입니다. 결국 핵심은 “선택자를 바꿀까”가 아니라 “이 페이지가 처음부터 값을 주는 구조인가”를 먼저 보는 것이었습니다.

정적과 동적 차이를 구분하고 API와 Selenium 기준을 나눴습니다
문제를 해결하려고 가장 먼저 한 일은 무작정 Selenium으로 넘어가는 것이 아니라, 이 페이지가 정말 브라우저 자동화가 필요한지부터 판단하는 것이었습니다. 많은 초보자가 동적 페이지를 만나면 바로 Selenium부터 쓰지만, 실제로는 그 전에 더 가벼운 방법이 남아 있는 경우가 많습니다. 개발자도구의 Network 탭을 열고 XHR 또는 Fetch 요청을 보면, 화면에 표시되는 데이터가 어떤 주소에서 JSON 형태로 내려오는지 확인할 수 있습니다. 만약 원하는 값이 API 응답으로 따로 존재한다면, 굳이 브라우저를 띄우지 않고 그 주소를 직접 호출하는 편이 더 빠르고 안정적입니다.
제가 확인한 페이지도 비슷했습니다. 처음에는 requests로 HTML을 받아 파싱하려 했지만 실패했습니다. 그런데 Network를 열어 보니 목록 데이터가 별도 API로 내려오고 있었습니다. 그래서 HTML을 긁는 대신 API 응답을 직접 요청하는 방식으로 바꾸니 훨씬 간단하게 해결됐습니다.
import requests
api_url = "https://example.com/api/items"
headers = {"User-Agent": "Mozilla/5.0"}
response = requests.get(api_url, headers=headers)
data = response.json()
for item in data["items"]:
print(item["name"], item["price"])
이 방식의 장점은 분명합니다. 속도가 빠르고, HTML 구조 변경에 덜 흔들리며, 불필요하게 브라우저를 띄울 필요가 없습니다. 다만 모든 페이지가 이렇게 친절하지는 않습니다. 어떤 사이트는 API 주소를 숨기거나, 토큰과 쿠키가 함께 필요하거나, 클릭과 스크롤 이후에야 값이 생성되기도 합니다. 이런 경우에는 Selenium으로 실제 브라우저 동작을 재현하는 편이 더 현실적입니다.
Selenium으로 넘어가야 하는 기준도 정리할 수 있었습니다. 첫째, Network를 확인해도 원하는 데이터를 직접 받는 간단한 API가 보이지 않을 때입니다. 둘째, 로그인, 클릭, 스크롤, 탭 전환 같은 사용자 동작 이후에만 값이 나타날 때입니다. 셋째, JavaScript 실행을 거친 최종 DOM 자체를 읽어야 할 때입니다. 그때는 아래처럼 접근하면 됩니다.
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.options import Options
import time
options = Options()
options.add_argument("--headless")
driver = webdriver.Chrome(options=options)
driver.get("https://example.com")
time.sleep(3)
items = driver.find_elements(By.CSS_SELECTOR, ".item")
for item in items:
print(item.text)
driver.quit()
여기서 중요한 것은 Selenium이 만능 해결책이 아니라는 점입니다. 렌더링이 끝나기 전에 찾으면 또 빈 값이 나올 수 있고, 속도도 느립니다. 그래서 저는 지금은 기준을 이렇게 잡습니다. API가 보이면 requests, 사용자 동작과 렌더링이 핵심이면 Selenium입니다. 이 기준을 세운 뒤부터는 동적 페이지를 만나도 덜 헤매게 됐습니다.
결국 핵심은 안 긁히는 이유를 먼저 구분하는 일이었습니다
이번 문제를 겪고 나서 가장 크게 달라진 점은, requests가 안 되면 바로 코드부터 뜯어고치지 않게 되었다는 점입니다. 예전에는 선택자를 바꾸고, 태그를 다시 찾고, NoneType 오류만 의심했는데, 이제는 먼저 이 페이지가 정적인지 동적인지부터 확인합니다. 브라우저에 보인다고 해서 서버가 처음부터 그 값을 HTML에 담아 보낸 것은 아닙니다. 브라우저는 HTML을 받은 뒤 JavaScript를 실행하고, 추가 요청을 보내고, 화면을 다시 그려서 최종 결과를 보여줍니다. 그래서 눈에 보이는 값과 response.text 안의 값이 다를 수 있습니다.
제가 실제로 해결했던 흐름도 단순했습니다. 먼저 requests 결과를 확인했는데 값이 없었습니다. 그다음 개발자도구 Network를 열어 데이터가 어디서 오는지 봤습니다. API가 있으면 그 응답을 직접 받았고, API로 해결되지 않는 구조는 Selenium으로 전환했습니다. 이 흐름으로 바꾸고 나니, 전에는 막연하게 “동적 페이지는 어려운 것”이라고 느꼈던 문제가 조금 정리됐습니다. 결국 중요한 것은 도구 자체보다 구분 기준입니다. requests로 충분한 페이지에 Selenium을 쓰면 불필요하게 무거워지고, 반대로 JavaScript 렌더링 페이지를 정적 HTML처럼 다루면 아무리 선택자를 바꿔도 값이 나오지 않습니다.
이 글의 결론은 분명합니다. 브라우저에서는 보이는데 코드에서는 안 보일 때, 가장 먼저 해야 할 일은 선택자 수정이 아니라 구조 확인입니다. 정적 페이지인지, JavaScript 렌더링 페이지인지, API를 직접 칠 수 있는지, 실제 브라우저 동작이 필요한지를 구분하면 해결 방향이 바로 보입니다. 저도 이 기준을 익힌 뒤부터는 “왜 안 긁히지”에서 멈추지 않고, “이건 API로 갈까, Selenium으로 갈까”를 먼저 판단하게 되었습니다. 결국 동적 페이지 문제는 복잡한 기술보다도, 데이터를 만드는 흐름을 읽는 눈이 생기면 훨씬 쉽게 풀립니다.