Go로 웹 스크래핑을 하다 보면 단일 IP로는 수백 페이지를 수집하기 전에 차단당합니다. Colly 로테이션 프록시를 설정하면 매 요청마다 IP를 바꿔 429 Too Many Requests와 403 Forbidden을 회피할 수 있습니다. 이 글에서는 Colly의 콜렉터 모델, proxy.RoundRobinProxySwitcher와 c.SetProxyFunc를 활용한 프록시 교체, ProxyHat 주거 프록시 연동, 그리고 프로덕션 환경에서의 재시도·동시성 제어·분산 큐 패턴까지 단계별로 설명합니다.
Colly 로테이션 프록시: 기본 개념과 필요성
Colly는 Go 생태계에서 가장 널리 쓰이는 웹 스크래핑 프레임워크입니다. Colly 공식 문서에 따르면, Colly는 콜백 기반의 콜렉터(Collector) 모델을 사용하며 goquery로 DOM을 순회합니다. 핵심 콜백은 다음과 같습니다:
c.OnRequest(func(r *colly.Request))— 요청 전 헤더·쿠키·프록시 설정c.OnHTML(selector, func(e *colly.HTMLElement))— DOM 노드 추출c.OnError(func(r *colly.Response, err error))— 에러 처리 및 재시도c.OnScraped(func(r *colly.Response))— 페이지 완료 후 후처리
Colly는 기본적으로 동기 모드로 동작하지만, c.Async = true로 설정하면 고루틴 기반 비동기 모드로 전환됩니다. 이때 c.Parallelism으로 동시 요청 수를 제어할 수 있으며, 프록시 로테이션과 함께 사용하면 대량 수집 시 차단을 크게 줄일 수 있습니다.
왜 주거 프록시인가?
데이터센터 IP는 AS(Autonomous System) 정보만 봐도 프록시로 식별되기 쉽습니다. 반면 주거(residential) IP는 실제 ISP가 할당한 가정용 주소이므로, Cloudflare·Akamai·PerimeterX 같은 봇 방지 시스템도 합법 사용자와 구분하기 어렵습니다. SERP 스크래핑, 이커머스 가격 모니터링, 소셜 미디어 데이터 수집 등 하드 타겟에서는 주거 프록시가 사실상 필수입니다.
주의: 공개 데이터만 수집하세요. robots.txt와 웹사이트 이용약관을 준수해야 하며, 미국에서는 CFAA(Computer Fraud and Abuse Act) 위반, EU에서는 GDPR 위반의 법적 리스크가 있습니다. 인증이 필요한 페이지를 무단으로 스크래핑하지 마세요.
Colly 프록시 교체 방식: RoundRobinProxySwitcher vs SetProxyFunc
Colly는 프록시 설정을 위한 두 가지 방법을 제공합니다. 하나는 빌트인 라운드로빈 스위처이고, 다른 하나는 커스텀 함수 기반 방식입니다.
1. proxy.RoundRobinProxySwitcher (빌트인)
colly/proxy 패키지의 RoundRobinProxySwitcher는 문자열 URL 목록을 받아 매 요청마다 순차적으로 프록시를 교체합니다. 가장 간단한 colly proxy switcher 패턴입니다:
package main
import (
"fmt"
"log"
"github.com/gocolly/colly/v2"
"github.com/gocolly/colly/v2/proxy"
)
func main() {
// ProxyHat 주거 프록시 — http/https/socks5 모두 지원
proxies := []string{
"http://user-country-DE-sid-abc1:pass@gate.proxyhat.com:8080",
"http://user-country-DE-sid-abc2:pass@gate.proxyhat.com:8080",
"http://user-country-DE-sid-abc3:pass@gate.proxyhat.com:8080",
}
rp, err := proxy.RoundRobinProxySwitcher(proxies...)
if err != nil {
log.Fatal(err)
}
c := colly.NewCollector()
c.SetProxyFunc(rp)
c.OnHTML("title", func(e *colly.HTMLElement) {
fmt.Println("Title:", e.Text)
})
c.OnError(func(r *colly.Response, err error) {
log.Printf("Error on %s: %v", r.Request.URL, err)
})
c.Visit("https://httpbin.org/ip")
}
이 방식은 소규모 프록시 풀(3~10개)에는 적합하지만, 수백 개의 세션을 관리해야 하는 프로덕션 환경에서는 유연성이 부족합니다. 세션 스티키니스, 국가별 라우팅, 실패한 프록시 제외 등의 로직이 필요하면 커스텀 함수를 작성해야 합니다.
2. 커스텀 SetProxyFunc (유연한 제어)
c.SetProxyFunc(func(*http.Request) (*url.URL, error))를 사용하면 요청 컨텍스트, URL 패턴, 재시도 카운트 등에 따라 프록시를 동적으로 선택할 수 있습니다. 이것이 진정한 golang colly proxy 패턴입니다:
package main
import (
"math/rand"
"net/http"
"net/url"
"sync/atomic"
"time"
"github.com/gocolly/colly/v2"
)
var counter uint64
func proxyHatSwitcher(req *http.Request) (*url.URL, error) {
countries := []string{"US", "DE", "GB", "FR", "JP"}
n := atomic.AddUint64(&counter, 1)
country := countries[n % uint64(len(countries))]
sessionID := fmt.Sprintf("sess-%d", n)
proxyURL := fmt.Sprintf(
"http://user-country-%s-sid-%s:YOUR_PASSWORD@gate.proxyhat.com:8080",
country, sessionID,
)
return url.Parse(proxyURL)
}
func main() {
c := colly.NewCollector(
colly.Async(true),
)
c.SetProxyFunc(proxyHatSwitcher)
c.Limit(&colly.LimitRule{
DomainGlob: "*",
Parallelism: 20,
Delay: 2 * time.Second,
RandomDelay: 1 * time.Second,
})
c.OnHTML("title", func(e *colly.HTMLElement) {
fmt.Printf("[%s] %s\n", e.Request.AbsoluteURL(e.Request.URL.String()), e.Text)
})
urls := []string{
"https://httpbin.org/ip",
"https://httpbin.org/headers",
"https://httpbin.org/user-agent",
}
for _, u := range urls {
c.Visit(u)
}
c.Wait()
}
위 예제에서는 atomic 카운터로 매 요청마다 새 세션 ID를 생성합니다. ProxyHat의 사용자명 포맷에서 -sid-abc123 접미사를 바꾸면 매 요청마다 다른 IP가 할당됩니다. -country-DE로 국가를 지정하고 -region-state_of_berlin-city-berlin으로 도시까지 타겟팅할 수 있습니다.
ProxyHat 주거 프록시와 Colly 통합하기
ProxyHat은 게이트웨이 호스트 gate.proxyhat.com을 통해 HTTP(포트 8080)와 SOCKS5(포트 1080)를 모두 지원합니다. 사용자명에 지오타겟팅과 세션 플래그를 포함하는 방식이 핵심입니다:
| 사용자명 포맷 | 효과 |
|---|---|
user-country-US:pass |
미국 IP로 라우팅, 매 요청마다 새 IP |
user-country-DE-region-state_of_berlin-city-berlin:pass |
베를린 도시 단위 타겟팅 |
user-country-DE-sid-abc123:pass |
동일 세션 ID = 동일 IP 유지 (스티키 세션) |
user-sid-xyz789:pass |
국가 무관, 세션만 고정 |
SOCKS5 사용 예제
일부 타겟 사이트는 HTTP CONNECT 프록시를 감지하지만 SOCKS5는 통과시키는 경우가 있습니다. SOCKS5를 사용하려면 포트를 1080으로 변경하세요:
func socks5Switcher(req *http.Request) (*url.URL, error) {
n := atomic.AddUint64(&counter, 1)
proxyURL := fmt.Sprintf(
"socks5://user-country-JP-sid-s%d:YOUR_PASSWORD@gate.proxyhat.com:1080",
n,
)
return url.Parse(proxyURL)
}
ProxyHat의 전체 연결 옵션은 공식 문서를 참조하세요. 가격 정보는 ProxyHat 가격 페이지에서 확인할 수 있으며, 지원 국가 목록은 위치 페이지에 있습니다.
프로덕션 패턴: 재시도, 동시성, 분산 큐
재시도 with c.Clone()
Colly에서 재시도를 구현하는 관용적 방법은 OnError 콜백에서 c.Clone()으로 새 콜렉터를 만들어 재방문하는 것입니다. 이때 프록시 함수도 복제되므로, 새 IP로 재시도됩니다:
func setupCollector() *colly.Collector {
c := colly.NewCollector(
colly.Async(true),
colly.MaxDepth(2),
)
c.SetProxyFunc(proxyHatSwitcher)
c.OnError(func(r *colly.Response, err error) {
if r.StatusCode == 429 || r.StatusCode == 503 {
fmt.Printf("Retrying %s (status %d)\n", r.Request.URL, r.StatusCode)
// 짧은 대기 후 재시도
time.Sleep(5 * time.Second)
clone := r.Request.C.Clone()
clone.SetProxyFunc(proxyHatSwitcher)
clone.OnHTML("title", func(e *colly.HTMLElement) {
fmt.Println("Retry title:", e.Text)
})
clone.Visit(r.Request.URL.String())
}
})
return c
}
RandomDelay와 Parallelism 제어
대량 스크래핑에서 가장 흔한 실수는 Limit() 규칙 없이 비동기 모드를 사용하는 것입니다. Parallelism을 20~50으로 설정하고 RandomDelay를 1~3초로 주면, 타겟 서버에 부담을 주지 않으면서도 시간당 수천 페이지를 수집할 수 있습니다. ProxyHat 주거 프록시를 사용하면 동시 세션 100개까지도 안정적으로 유지할 수 있습니다.
분산 스크래핑: Redis 큐 백엔드
여러 머신에서 동시에 스크래핑하려면 Colly의 큐(Queue) 시스템에 Redis 백엔드를 사용하세요. Colly v2는 storage.RedisStorage를 제공합니다:
import (
"github.com/gocolly/colly/v2/storage"
"github.com/gocolly/redisstorage"
)
func setupDistributedQueue() *colly.Collector {
c := colly.NewCollector()
c.SetProxyFunc(proxyHatSwitcher)
// Redis 백엔드로 방문한 URL 추적
redisStorage := &redisstorage.Storage{
Address: "redis:6379",
Password: "",
DB: 0,
Prefix: "proxyhat_scraper",
}
c.SetStorage(redisStorage)
q, _ := colly.NewQueue(
20, // threads
colly.NewCookieJarStorage(redisStorage),
)
// URL 추가
q.AddURL("https://example.com/page1")
q.AddURL("https://example.com/page2")
q.Run(c)
return c
}
이 패턴을 사용하면 여러 컨테이너가 동일한 Redis 큐에서 URL을 가져와 처리합니다. 각 컨테이너는 독립적으로 ProxyHat 프록시를 로테이션하므로 IP 풀이 고갈되지 않습니다.
커스텀 Transport TLS 설정
일부 사이트는 TLS 지문(fingerprint)으로 봇을 탐지합니다. Colly의 http.Transport를 커스터마이징하면 User-Agent와 TLS 설정을 미세 조정할 수 있습니다:
import (
"crypto/tls"
"net/http"
"github.com/gocolly/colly/v2"
)
func setupTLS() *colly.Collector {
c := colly.NewCollector()
c.SetProxyFunc(proxyHatSwitcher)
c.WithTransport(&http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: false,
MinVersion: tls.VersionTLS12,
},
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
})
return c
}
Colly를 사용하면 안 되는 경우
Colly는 HTTP 요청과 HTML 파싱에 최적화되어 있습니다. React·Vue·Angular로 구축된 SPA(Single Page Application)는 JavaScript 실행이 필요하므로 Colly만으로는 렌더링된 콘텐츠를 얻을 수 없습니다. 이런 경우 chromedp나 Playwright for Go를 사용해 헤드리스 브라우저를 구동하고, 그 앞에 ProxyHat 프록시를 설정해야 합니다.
또한 Colly는 파일 다운로드, 바이너리 처리, WebSocket 통신에는 적합하지 않습니다. 이런 작업이 필요하면 표준 net/http 클라이언트에 프록시를 직접 설정하는 것이 더 적합합니다.
윤리와 법적 고려사항
웹 스크래핑은 강력한 도구이지만 책임감 있게 사용해야 합니다. CFAA는 미국에서 인증 없이 접근해서는 안 되는 시스템에 대한 접근을 금지하며, EU의 GDPR은 개인 데이터 수집에 엄격한 제한을 둡니다. 다음 원칙을 지키세요:
- robots.txt 준수: Colly는
c.IgnoreRobotsTxt = false가 기본값이지만, 명시적으로 확인하세요. - Rate limit 존중:
Limit()규칙으로 초당 요청 수를 제한하세요. - 공개 데이터만 수집: 로그인이 필요한 페이지는 이용약관 위반일 수 있습니다.
- 개인정보 최소화: 이메일·전화번호 등 PII를 수집하지 않거나 즉시 익명화하세요.
SERP 추적과 웹 스크래핑 유스케이스에 대한 자세한 가이드는 SERP 추적 페이지와 웹 스크래핑 페이지를 참조하세요.
핵심 요약: Key Takeaways
- RoundRobinProxySwitcher는 소규모 프록시 풀에 적합하고, SetProxyFunc 커스텀 함수는 동적 지오타겟팅·세션 관리에 적합합니다.
- ProxyHat 사용자명에
-country-DE-region-state_of_berlin-city-berlin-sid-abc123형식으로 플래그를 추가하면 매 요청마다 IP·지역·세션을 제어할 수 있습니다. Limit()으로 Parallelism과 RandomDelay를 설정하지 않으면 타겟 서버가 봇으로 간주합니다. 동시성 20~50, 지연 1~3초가 권장값입니다.- 재시도는
c.Clone()으로 새 콜렉터를 만들어 새 IP로 재방문하는 것이 Colly의 관용적 패턴입니다. - 분산 환경에서는 Redis 큐 백엔드를 사용해 여러 컨테이너가 동일한 작업 큐를 공유하세요.
- JS 렌더링이 필요한 SPA는 Colly 대신 chromedp나 Playwright를 사용하고, 그 앞에 ProxyHat 프록시를 설정하세요.






