R에서 프록시 사용하기: httr2와 rvest로 웹 스크래핑 완벽 가이드

httr2의 req_proxy와 rvest를 활용해 R에서 프록시 기반 웹 스크래핑을 구현하는 방법을 코드 예제와 함께 설명합니다. 지역 타겟팅, 세션 로테이션, JS 렌더링 페이지 처리까지 다룹니다.

Using Proxies in R: A Code-First Guide with httr2 and rvest
이 글의 목차

R에서 프록시 사용: 왜 필요한가?

R 데이터 분석가와 데이터 엔지니어에게 웹 데이터 수집은 일상적인 작업입니다. 하지만 많은 웹사이트가 데이터센터 IP를 차단하거나 특정 지역에서만 접근을 허용합니다. R에서 프록시 사용은 이러한 제한을 우회하고 안정적으로 공개 데이터를 수집하기 위한 핵심 기술입니다.

전통적으로 R 사용자들은 httr 패키지와 read_html()을 조합해 스크래핑을 수행했습니다. 하지만 최신 httr2 패키지는 파이프 기반 API, 재시도 로직, 쓰로틀링 등 프로덕션 수준의 기능을 제공하며, rvest와 결합하면 강력한 스크래핑 파이프라인을 구축할 수 있습니다.

이 가이드에서는 ProxyHat residential proxy를 기준으로 httr2의 req_proxy(), rvest의 파싱 함수, 지역 타겟팅, 세션 관리, JS 렌더링 페이지 처리까지 실전 코드와 함께 다룹니다.

프록시 유형 비교: Residential vs Datacenter vs Mobile

프록시를 선택하기 전에 각 유형의 차이를 이해해야 합니다. 대상 사이트의 anti-bot 시스템에 따라 적합한 유형이 달라집니다.

특성 Residential Datacenter Mobile
IP 출처 실제 ISP 가입자 IP 데이터센터 서버 IP 4G/5G 모바일 통신사 IP
차단 확률 낮음 높음 매우 낮음
속도 중간 (약 200ms~500ms) 빠름 (50ms~100ms) 변동적
가격 중간 저렴 높음
적합 용도 SERP 스크래핑, 이커머스 대량 단순 수집 소셜 미디어, 고도 차단 환경

데이터센터 IP는 AWS, GCP, Azure 등의 IP 대역으로 등록되어 있어 Cloudflare, Akamai, PerimeterX 같은 anti-bot 서비스가 쉽게 식별합니다. 반면 residential 프록시는 실제 ISP 고객에게 할당된 IP이므로 일반 사용자 트래픽과 구분하기 어렵습니다. 이것이 R proxy 설정 시 residential를 선택하는 주요 이유입니다.

httr2와 rvest 기본 스택: req_proxy로 요청 보내기

httr2의 req_proxy() 함수는 HTTP/HTTPS 요청에 프록시를 적용하는 가장 직관적인 방법입니다. ProxyHat 게이트웨이를 통해 residential IP로 요청을 보내는 기본 예제부터 시작합니다.

기본 HTTP 프록시 요청

# httr2와 rvest 로드
library(httr2)
library(rvest)

# ProxyHat residential 프록시로 IP 확인
req <- request("https://httpbin.org/ip") |>
  req_proxy(
    url = "http://gate.proxyhat.com:8080",
    username = "user",
    password = "pass"
  ) |>
  req_user_agent(
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
  )

resp <- req |> req_perform()
body <- resp |> resp_body_json()
cat("프록시 IP:", body$origin, "\n")

req_proxy()의 첫 번째 인자는 프록시 URL입니다. ProxyHat의 경우 http://gate.proxyhat.com:8080을 사용합니다. username과 password는 ProxyHat 대시보드에서 발급받은 인증 정보입니다.

rvest로 HTML 파싱하기

응답 본문을 rvest로 파싱하려면 resp_body_string()으로 문자열을 추출한 후 read_html()에 전달합니다.

library(httr2)
library(rvest)

req <- request("https://books.toscrape.com/catalogue/page-1.html") |>
  req_proxy(
    url = "http://gate.proxyhat.com:8080",
    username = "user",
    password = "pass"
  ) |>
  req_user_agent("Mozilla/5.0 (compatible; R-bot/1.0)")

resp <- req |> req_perform()

# 응답을 rvest로 파싱
html <- resp |> resp_body_string() |> read_html()

# 도서 제목 추출
titles <- html |>
  html_elements("article.product_pod h3 a") |>
  html_attr("title")

print(head(titles))

html_elements()는 CSS 셀렉터로 노드를 선택하고, html_attr()은 속성 값을 추출합니다. html_table()을 사용하면 HTML 테이블을 데이터프레임으로 변환할 수 있습니다.

지역 타겟팅과 세션 관리: username에 플래그 인코딩

ProxyHat의 핵심 기능 중 하나는 username 필드에 지역 및 세션 플래그를 인코딩하는 것입니다. 별도의 헤더나 파라미터 없이 username 문자열만으로 제어할 수 있습니다.

국가 및 도시 타겟팅

# 영국 런던 IP로 요청
req_uk <- request("https://ipinfo.io/json") |>
  req_proxy(
    url = "http://gate.proxyhat.com:8080",
    username = "user-country-GB-city-london",
    password = "pass"
  )

resp_uk <- req_uk |> req_perform()
info_uk <- resp_uk |> resp_body_json()
cat("국가:", info_uk$country, "도시:", info_uk$city, "\n")

# 독일 베를린 IP로 요청
req_de <- request("https://ipinfo.io/json") |>
  req_proxy(
    url = "http://gate.proxyhat.com:8080",
    username = "user-country-DE-city-berlin",
    password = "pass"
  )

resp_de <- req_de |> req_perform()
info_de <- resp_de |> resp_body_json()
cat("국가:", info_de$country, "도시:", info_de$city, "\n")

지역 타겟팅은 SERP 추적, 지역별 가격 비교, 현지화된 콘텐츠 수집에 필수적입니다. 예를 들어 구글 검색 결과는 IP 기반으로 지역 결과를 반환하므로, 정확한 SERP 추적을 위해서는 해당 국가의 residential IP가 필요합니다.

Sticky 세션으로 IP 유지하기

기본적으로 ProxyHat은 요청마다 새로운 IP를 할당합니다. 하지만 로그인 세션이나 페이지 간 상태 유지가 필요한 경우 sticky session을 사용할 수 있습니다.

# 동일한 세션 ID로 IP 유지
session_id <- "my-session-001"

req <- request("https://example.com/dashboard") |>
  req_proxy(
    url = "http://gate.proxyhat.com:8080",
    username = paste0("user-session-", session_id),
    password = "pass"
  )

# 같은 session_id를 사용하면 동일한 IP가 유지됨
# 여러 페이지를 순회할 때 인증 상태가 유지됨

SOCKS5 프록시 사용

일부 환경에서는 SOCKS5가 더 안정적일 수 있습니다. ProxyHat은 포트 1080에서 SOCKS5를 지원합니다.

# SOCKS5 프록시 (포트 1080)
req <- request("https://httpbin.org/ip") |>
  req_proxy(
    url = "socks5://gate.proxyhat.com:1080",
    username = "user",
    password = "pass"
  )

resp <- req |> req_perform()
body <- resp |> resp_body_json()
cat("SOCKS5 IP:", body$origin, "\n")

실전 예제: 페이지네이션 테이블 스크래핑

이제 지금까지의 기법을 조합하여 실전 예제를 구축합니다. 여러 페이지에 걸친 HTML 테이블을 수집하여 하나의 tidy 데이터프레임으로 결합하는 파이프라인입니다. purrr::map_dfr()로 페이지를 순회하고, 각 페이지마다 새로운 세션 ID를 할당하여 IP를 로테이션합니다.

library(httr2)
library(rvest)
library(purrr)
library(dplyr)

# 단일 페이지 수집 함수
fetch_page <- function(page_num) {
  # 페이지마다 고유 세션 ID 생성하여 IP 로테이션
  session_id <- paste0("page-", page_num, "-", as.integer(Sys.time()))
  url <- paste0("https://books.toscrape.com/catalogue/page-", page_num, ".html")

  req <- request(url) |>
    req_proxy(
      url = "http://gate.proxyhat.com:8080",
      username = paste0("user-session-", session_id),
      password = "pass"
    ) |>
    req_user_agent(
      "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
    ) |>
    req_headers("Accept-Language" = "en-US,en;q=0.9") |>
    req_retry(
      max_tries = 3,
      max_seconds = 30,
      is_transient = function(resp) {
        status <- resp_status(resp)
        status == 429 || status >= 500
      }
    ) |>
    req_throttle(0.5)  # 초당 2회 요청 (2초 간격)

  tryCatch({
    resp <- req |> req_perform()
    html <- resp |> resp_body_string() |> read_html()

    # 도서 정보 추출
    books <- tibble(
      title = html |>
        html_elements("article.product_pod h3 a") |>
        html_attr("title"),
      price = html |>
        html_elements(".product_price .price_color") |>
        html_text2(),
      availability = html |>
        html_elements(".availability") |>
        html_text2() |>
        trimws(),
      page = page_num
    )

    message("페이지 ", page_num, " 완료: ", nrow(books), "건")
    books
  }, error = function(e) {
    warning("페이지 ", page_num, " 실패: ", e$message)
    tibble()
  })
}

# 1~10페이지 수집 (병렬 처리 시 furrr::future_map_dfr 사용 권장)
all_books <- 1:10 |> map_dfr(fetch_page)

cat("총 수집 도서 수:", nrow(all_books), "\n")
print(head(all_books))

이 예제에서 주목할 점은 다음과 같습니다:

  • 세션 로테이션: 각 페이지마다 새로운 session_id를 생성하여 IP가 자동으로 변경됩니다. 단일 IP로 대량 요청을 보내는 것을 방지합니다.
  • req_retry(): 429(Too Many Requests) 및 5xx 오류 시 최대 3회, 30초 제한으로 재시도합니다. is_transient 함수로 재시도 조건을 세밀하게 제어할 수 있습니다.
  • req_throttle(): 초당 0.5회(2초 간격)로 요청 속도를 제한합니다. 대상 서버에 부하를 주지 않는 핵심 설정입니다.
  • tryCatch: 실패한 페이지가 전체 파이프라인을 중단시키지 않도록 예외 처리합니다.

병렬 처리로 속도 향상

수집 페이지가 많은 경우 furrr 패키지로 병렬 처리할 수 있습니다. 100개의 동시 세션을 사용하면 직렬 처리 대비 수집 시간을 크게 단축할 수 있습니다.

library(furrr)

# 4개 워커로 병렬 수집
plan(multisession, workers = 4)

all_books <- 1:50 |> future_map_dfr(
  fetch_page,
  .options = furrr_options(
    seed = TRUE,
    packages = c("httr2", "rvest", "dplyr")
  )
)

JS 렌더링 페이지 처리: read_html_live() 활용

React, Vue, Angular로 구축된 SPA(Single Page Application)는 서버에서 HTML을 직접 반환하지 않습니다. 이 경우 rvest의 read_html_live()를 사용하면 실제 Chrome 브라우저로 페이지를 렌더링한 후 파싱할 수 있습니다. read_html_live()는 내부적으로 chromote 패키지를 사용합니다.

library(rvest)

# Chrome에 프록시 설정 (환경 변수 사용)
# ProxyHat 게이트웨이를 Chrome 프록시로 지정
Sys.setenv(
  CHROMOTE_CHROME = paste(
    "google-chrome",
    "--proxy-server=http://gate.proxyhat.com:8080",
    "--no-sandbox",
    "--disable-gpu"
  )
)

# JS 렌더링 페이지 로드
page <- read_html_live("https://quotes.toscrape.com/js/")

# 동적 콘텐츠 추출
quotes <- page |>
  html_elements(".quote .text") |>
  html_text2()

authors <- page |>
  html_elements(".quote .author") |>
  html_text2()

df <- tibble(quote = quotes, author = authors)
print(head(df))

주의: Chrome의 --proxy-server 플래그는 HTTP 프록시 인증을 직접 지원하지 않습니다. ProxyHat의 username/password 인증을 사용하려면 로컬 프록시 릴레이(예: mitmproxy 또는 squid)를 설정하거나, IP 화이트리스트 인증 방식을 ProxyHat 대시보드에서 활성화해야 합니다. 자세한 내용은 ProxyHat 문서를 참조하세요.

실제적인 User-Agent와 헤더 설정

anti-bot 시스템은 User-Agent와 헤더 패턴으로 봇을 식별합니다. req_user_agent()req_headers()로 실제 브라우저와 유사한 요청을 만들어야 합니다.

req <- request("https://example.com/data") |>
  req_proxy(
    url = "http://gate.proxyhat.com:8080",
    username = "user-country-US",
    password = "pass"
  ) |>
  req_user_agent(
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/120.0.0.0 Safari/537.36"
  ) |>
  req_headers(
    "Accept" = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language" = "en-US,en;q=0.9",
    "Accept-Encoding" = "gzip, deflate, br",
    "Connection" = "keep-alive",
    "Upgrade-Insecure-Requests" = "1"
  )

윤리와 컴플라이언스: 공정한 데이터 수집

프록시를 사용한 웹 스크래핑은 강력한 도구이지만, 윤리적이고 법적인 가이드라인을 준수해야 합니다. 다음 원칙을 권장합니다:

  1. robots.txt 확인: 대상 사이트의 robots.txt를 먼저 확인하여 수집이 허용되는 경로인지 확인합니다. R에서는 robotstxt 패키지를 사용할 수 있습니다.
  2. 공식 API 우선: 웹사이트에서 공식 API를 제공한다면 스크래핑보다 API를 우선 사용해야 합니다. API가 더 안정적이고 법적 리스크가 적습니다.
  3. GDPR 및 개인정보 보호: EU 데이터 대상자의 개인정보를 수집하는 경우 GDPR을 준수해야 합니다. 공개된 데이터라도 개인정보에 해당하면 동의 기반 또는 익명화가 필요합니다.
  4. 요청 속도 제한: req_throttle()로 대상 서버에 부하를 주지 않는 속도를 유지합니다. 일반적으로 초당 1~2회 이하를 권장합니다.
  5. 이용약관 검토: 대상 사이트의 ToS(Terms of Service)를 확인하여 자동화 수집이 금지되어 있는지 확인합니다.

ProxyHat은 웹 스크래핑을 위한 인프라를 제공하지만, 수집의 목적과 방법은 사용자의 책임입니다. 항상 합법적이고 윤리적인 데이터 수집을 실천하세요.

ProxyHat 설정과 혼합 파이프라인

ProxyHat의 모든 프록시 유형은 동일한 게이트웨이 gate.proxyhat.com을 사용합니다. HTTP는 포트 8080, SOCKS5는 포트 1080입니다. 지원 위치는 90개국 이상이며, 국가 및 도시 수준 타겟팅이 가능합니다.

R 개발자가 Python이나 Node.js 팀과 협업하는 경우, ProxyHat SDK가 동일한 username 플래그 패턴을 사용하므로 일관된 설정을 유지할 수 있습니다. 예를 들어 Python의 requests 라이브러리에서도 동일하게 http://user-country-GB:pass@gate.proxyhat.com:8080 형식을 사용합니다.

# R에서의 프록시 URL (httr2)
proxy_url <- "http://gate.proxyhat.com:8080"
proxy_user <- "user-country-GB-session-r001"
proxy_pass <- "pass"

# 동일한 설정을 Python requests에서도 사용 가능
# proxies = {"http": "http://user-country-GB-session-r001:pass@gate.proxyhat.com:8080"}

혼합 파이프라인에서 R은 데이터 분석과 시각화를 담당하고 Python/Node.js는 수집을 담당하는 패턴이 흔합니다. ProxyHat의 통합 게이트웨이는 이러한 다국어 환경에서 설정 일관성을 보장합니다. ProxyHat 가격 페이지에서 residential, datacenter, mobile 프록시의 요금제를 확인할 수 있습니다.

주요 실수와 엣지 케이스

1. 세션 ID 재사용으로 인한 IP 고정

sticky session은 의도적으로 IP를 유지하는 기능이지만, 실수로 동일한 session_id를 재사용하면 한 IP로 모든 요청이 집중됩니다. 페이지나 배치마다 새로운 session_id를 생성하세요.

2. 타임아웃 설정 누락

residential 프록시는 데이터센터 프록시보다 레이턴시가 높을 수 있습니다(200ms~500ms). req_options()로 타임아웃을 명시적으로 설정하세요.

req <- request(url) |>
  req_proxy("http://gate.proxyhat.com:8080", "user", "pass") |>
  req_options(timeout_ms = 30000)  # 30초 타임아웃

3. 인코딩 문제

한국어, 일본어 등 비-ASCII 콘텐츠를 수집할 때 문자 인코딩이 깨질 수 있습니다. resp_body_string()은 자동으로 인코딩을 감지하지만, 문제가 발생하면 resp_body_raw()stringi 패키지로 수동 처리하세요.

4. 메모리 부족

수천 페이지를 수집할 때 모든 응답을 메모리에 보관하면 R의 메모리 한계에 도달합니다. 페이지마다 데이터를 추출하고 응답 객체를 즉시 해제하세요. 필요시 readr::write_csv()로 중간 저장하세요.

Key Takeaways

  • httr2의 req_proxy()는 R에서 프록시를 설정하는 가장 간결한 방법입니다. gate.proxyhat.com:8080을 사용하세요.
  • 지역 타겟팅은 username에 user-country-XX-city-yyy 형식으로 인코딩합니다. 별도 헤더가 필요 없습니다.
  • 세션 로테이션은 각 요청마다 고유한 session-XXX를 사용하여 IP를 자동 변경합니다.
  • req_retry()와 req_throttle()을 조합하여 429/5xx 오류에 대한 재시도와 요청 속도 제한을 구현하세요.
  • read_html_live()로 JS 렌더링 페이지를 처리할 수 있지만, 프록시 인증은 추가 설정이 필요합니다.
  • 윤리적 수집: robots.txt 확인, 공식 API 우선, GDPR 준수, 적절한 속도 제한을 항상 실천하세요.

FAQ

R에서 프록시 사용이란 무엇인가요?

R에서 프록시 사용은 httr2의 req_proxy() 함수나 환경 변수를 통해 HTTP 요청을 중간 서버(프록시)로 라우팅하는 것을 의미합니다. 이를 통해 실제 IP를 숨기고, 지역 제한을 우회하며, 대상 서버의 차단을 방지할 수 있습니다. residential 프록시는 실제 ISP IP를 사용하므로 데이터센터 IP보다 차단 확률이 현저히 낮습니다.

R에서 프록시 사용이 프록시 사용자에게 왜 중요한가요?

R로 웹 데이터를 수집할 때 데이터센터 IP는 Cloudflare, Akamai 등의 anti-bot 시스템에 의해 자주 차단됩니다. residential 프록시를 사용하면 실제 사용자 트래픽과 구분되지 않아 차단 확률이 크게 낮아집니다. 또한 국가별로 IP를 선택할 수 있어 지역별 검색 결과, 가격, 콘텐츠를 정확하게 수집할 수 있습니다. 이는 SERP 추적, 이커머스 가격 모니터링, 시장 조사에 필수적입니다.

R에서 프록시 사용에 어떤 프록시 유형이 가장 적합한가요?

대부분의 웹 스크래핑 시나리오에서 residential 프록시가 가장 적합합니다. 데이터센터 IP 대역이 아니므로 anti-bot 시스템에 탐지될 확률이 낮습니다. 단순한 대량 수집이나 차단이 없는 사이트에서는 datacenter 프록시로 비용을 절감할 수 있습니다. 소셜 미디어나 고도로 보호된 사이트에서는 mobile 프록시가 가장 안정적입니다. ProxyHat은 세 유형을 모두 gate.proxyhat.com 게이트웨이에서 제공합니다.

R에서 프록시 사용 시 차단을 어떻게 피하나요?

차단을 피하기 위해서는 여러 전략을 조합해야 합니다. 첫째, 각 요청마다 새로운 세션 ID를 사용하여 IP를 로테이션합니다. 둘째, req_throttle()로 요청 속도를 초당 1~2회 이하로 제한합니다. 셋째, 실제 브라우저와 유사한 User-Agent와 Accept 헤더를 설정합니다. 넷째, req_retry()로 429 오류 시 자동 재시도합니다. 마지막으로 robots.txt를 준수하고 공식 API가 있다면 우선 사용하는 것이 장기적으로 가장 안정적인 방법입니다.

httr2 req_proxy와 rvest를 함께 사용하려면 어떻게 하나요?

httr2로 프록시가 적용된 요청을 보낸 후, resp_body_string()으로 응답 본문을 문자열로 추출합니다. 그 문자열을 rvest의 read_html()에 전달하면 HTML 파싱이 가능합니다. 이후 html_elements()로 CSS 셀렉터 기반 노드 선택을, html_table()로 테이블 추출을, html_text2()로 텍스트 추출을 수행할 수 있습니다. 이 조합은 httr2의 재시도/쓰로틀링 기능과 rvest의 파싱 기능을 모두 활용할 수 있어 프로덕션 스크래핑에 적합합니다.

자주 묻는 질문

R에서 프록시 사용이란 무엇인가요?

R에서 프록시 사용은 httr2의 req_proxy() 함수나 환경 변수를 통해 HTTP 요청을 중간 서버(프록시)로 라우팅하는 것을 의미합니다. 이를 통해 실제 IP를 숨기고, 지역 제한을 우회하며, 대상 서버의 차단을 방지할 수 있습니다. residential 프록시는 실제 ISP IP를 사용하므로 데이터센터 IP보다 차단 확률이 현저히 낮습니다.

R에서 프록시 사용이 프록시 사용자에게 왜 중요한가요?

R로 웹 데이터를 수집할 때 데이터센터 IP는 Cloudflare, Akamai 등의 anti-bot 시스템에 의해 자주 차단됩니다. residential 프록시를 사용하면 실제 사용자 트래픽과 구분되지 않아 차단 확률이 크게 낮아집니다. 또한 국가별로 IP를 선택할 수 있어 지역별 검색 결과, 가격, 콘텐츠를 정확하게 수집할 수 있습니다.

R에서 프록시 사용에 어떤 프록시 유형이 가장 적합한가요?

대부분의 웹 스크래핑 시나리오에서 residential 프록시가 가장 적합합니다. 데이터센터 IP 대역이 아니므로 anti-bot 시스템에 탐지될 확률이 낮습니다. 단순한 대량 수집에서는 datacenter 프록시로 비용을 절감할 수 있고, 소셜 미디어나 고도로 보호된 사이트에서는 mobile 프록시가 가장 안정적입니다.

R에서 프록시 사용 시 차단을 어떻게 피하나요?

차단을 피하기 위해 각 요청마다 새로운 세션 ID로 IP를 로테이션하고, req_throttle()로 요청 속도를 초당 1~2회 이하로 제한하며, 실제 브라우저와 유사한 User-Agent와 헤더를 설정하고, req_retry()로 429 오류 시 자동 재시도해야 합니다. robots.txt를 준수하고 공식 API를 우선 사용하는 것이 장기적으로 가장 안정적입니다.

httr2 req_proxy와 rvest를 함께 사용하려면 어떻게 하나요?

httr2로 프록시가 적용된 요청을 보낸 후, resp_body_string()으로 응답 본문을 문자열로 추출합니다. 그 문자열을 rvest의 read_html()에 전달하면 HTML 파싱이 가능합니다. 이후 html_elements()로 CSS 셀렉터 기반 노드 선택을, html_table()로 테이블 추출을 수행할 수 있습니다.

시작할 준비가 되셨나요?

148개국 이상의 주거용, ISP, 모바일 프록시. 무료 계정을 만드세요.

무료 계정 만들기
← 블로그로 돌아가기