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-session-abc1:pass@gate.proxyhat.com:8080",
"http://user-country-DE-session-abc2:pass@gate.proxyhat.com:8080",
"http://user-country-DE-session-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-session-%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의 사용자명 포맷에서 -session-abc123 접미사를 바꾸면 매 요청마다 다른 IP가 할당됩니다. -country-DE로 국가를 지정하고 -city-berlin으로 도시까지 타겟팅할 수 있습니다.
ProxyHat 주거 프록시와 Colly 통합하기
ProxyHat은 게이트웨이 호스트 gate.proxyhat.com을 통해 HTTP(포트 8080)와 SOCKS5(포트 1080)를 모두 지원합니다. 사용자명에 지오타겟팅과 세션 플래그를 포함하는 방식이 핵심입니다:
| 사용자명 포맷 | 효과 |
|---|---|
user-country-US:pass |
미국 IP로 라우팅, 매 요청마다 새 IP |
user-country-DE-city-berlin:pass |
베를린 도시 단위 타겟팅 |
user-country-DE-session-abc123:pass |
동일 세션 ID = 동일 IP 유지 (스티키 세션) |
user-session-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-session-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-city-berlin-session-abc123형식으로 플래그를 추가하면 매 요청마다 IP·지역·세션을 제어할 수 있습니다. Limit()으로 Parallelism과 RandomDelay를 설정하지 않으면 타겟 서버가 봇으로 간주합니다. 동시성 20~50, 지연 1~3초가 권장값입니다.- 재시도는
c.Clone()으로 새 콜렉터를 만들어 새 IP로 재방문하는 것이 Colly의 관용적 패턴입니다. - 분산 환경에서는 Redis 큐 백엔드를 사용해 여러 컨테이너가 동일한 작업 큐를 공유하세요.
- JS 렌더링이 필요한 SPA는 Colly 대신 chromedp나 Playwright를 사용하고, 그 앞에 ProxyHat 프록시를 설정하세요.
FAQ
Colly에서 로테이션 프록시를 설정하는 방법은?
proxy.RoundRobinProxySwitcher에 프록시 URL 목록을 전달하고 c.SetProxyFunc(rp)로 등록하면 됩니다. 더 유연한 제어가 필요하면 c.SetProxyFunc(func(*http.Request) (*url.URL, error))로 커스텀 함수를 작성해 요청마다 ProxyHat 게이트웨이 주소와 세션 ID를 동적으로 생성하세요.
Colly에서 어떤 프록시 유형이 가장 적합한가요?
봇 방지 시스템이 있는 하드 타겟(SERP, 이커머스, 소셜 미디어)에서는 주거(residential) 프록시가 가장 적합합니다. 데이터센터 IP는 AS 정보로 쉽게 식별됩니다. ProxyHat 주거 프록시를 사용하면 실제 ISP IP로 요청이 전달되어 차단률이 크게 낮아집니다. SOCKS5(포트 1080)는 HTTP 프록시가 감지되는 사이트에서 대안이 됩니다.
Colly에서 차단을 피하려면 어떻게 해야 하나요?
세 가지를 결합하세요: (1) ProxyHat 주거 프록시로 매 요청마다 IP 로테이션, (2) Limit()으로 Parallelism을 20~50, RandomDelay를 1~3초로 설정, (3) OnError에서 429/503 응답 시 c.Clone()으로 재시도. User-Agent 회전과 robots.txt 준수도 필수입니다.
Colly의 비동기 모드에서 프록시 로테이션이 안전한가요?
네, c.Async = true와 SetProxyFunc는 함께 작동합니다. 단, atomic.AddUint64 같은 스레드 안전 카운터를 사용해 세션 ID를 생성해야 합니다. 일반 변수를 사용하면 고루틴 경쟁 조건으로 인해 동일 IP가 중복 할당될 수 있습니다.
분산 환경에서 Colly 프록시 로테이션을 어떻게 관리하나요?
Colly의 redisstorage 백엔드를 사용해 방문한 URL을 추적하고, 각 컨테이너가 독립적으로 ProxyHat 프록시를 로테이션하게 하세요. 중앙에서 프록시 풀을 관리할 필요 없이, 각 노드가 자체적으로 세션 ID를 생성하면 됩니다. 큐 스레드 수는 노드당 10~20이 적당합니다.






