Swift에서 프록시 사용하기: URLSession 완벽 가이드
iOS와 macOS에서 Swift proxy를 다루는 개발자라면, URLSession이 기본적으로 시스템 프록시 설정만 따른다는 사실을 금방 알게 됩니다. 앱이 명시적으로 gate.proxyhat.com:8080 같은 residential proxy 게이트웨이를 거치게 하려면 URLSessionConfiguration.connectionProxyDictionary를 직접 채워야 하고, 인증은 별도의 헤더나 델리게이트 챌린지로 처리해야 합니다. 이 글은 Swift에서 프록시 사용하기를 코드 퍼스트로 풀어가며, 실전에서 자주 마주치는 407 인증 실패, SOCKS5 전환, ATS 제약, 동시성 설계까지 다룹니다.
왜 URLSession은 프록시 설정이 까다로운가
Apple의 URLSession은 URLSessionConfiguration 문서에 명시된 대로 connectionProxyDictionary를 통해 CFNetwork 계층의 프록시 키를 그대로 노출합니다. 이 키들은 kCFNetworkProxiesHTTPEnable, kCFNetworkProxiesHTTPProxy, kCFNetworkProxiesHTTPPort 등으로, 이름에서 보듯 CoreFoundation 상수입니다. 문제는 kCFProxyUsernameKey/kCFProxyPasswordKey가 URLSession에서 신뢰성 있게 동작하지 않는다는 점입니다. Apple 포럼과 실무 경험 모두 이 키가 무시되는 사례를 보고하므로, 인증은 Proxy-Authorization 헤더 또는 urlSession(_:didReceive:completionHandler:) 407 챌린지 핸들러로 직접 처리하는 것이 정석입니다.
또한 iOS 15+와 macOS 12+에서는 URLSession의 async API가 도입되어 try await session.data(for:) 한 줄로 요청을 보낼 수 있지만, 프록시 인증 실패 시 URLError.userAuthenticationRequired가 발생하므로 재시도 로직을 함께 설계해야 합니다.
ProxyHat 게이트웨이와 인증 스키마
ProxyHat은 residential, mobile, datacenter 프록시를 단일 게이트웨이로 제공합니다. 연결 정보는 다음과 같습니다.
| 항목 | 값 |
|---|---|
| HTTP 게이트웨이 | gate.proxyhat.com:8080 |
| SOCKS5 게이트웨이 | gate.proxyhat.com:1080 |
| HTTP URL | http://USERNAME:PASSWORD@gate.proxyhat.com:8080 |
| SOCKS5 URL | socks5://USERNAME:PASSWORD@gate.proxyhat.com:1080 |
사용자명에 지역 타겟팅과 세션 ID를 인코딩합니다. 예를 들어 user-country-US-city-newyork-session-abc123 형태이며, 이 값은 Proxy-Authorization: Basic base64(user:pass) 헤더로 인코딩됩니다. ProxyHat SDK(Python/Node)도 동일한 게이트웨이를 사용하므로, 모바일 앱과 서버 스크립트가 동일한 IP 풀을 공유할 수 있습니다.
기본 HTTP 프록시 구성: connectionProxyDictionary
가장 직접적인 방법은 URLSessionConfiguration의 connectionProxyDictionary를 채우는 것입니다. 다음 예제는 HTTP와 HTTPS 모두 gate.proxyhat.com:8080을 가리키도록 설정합니다.
import Foundation
enum ProxyHatConfig {
static let host = "gate.proxyhat.com"
static let port = 8080
static let username = "user-country-US-city-newyork-session-abc123"
static let password = "YOUR_PASSWORD"
}
func makeProxySession() -> URLSession {
let config = URLSessionConfiguration.default
config.connectionProxyDictionary = [
kCFNetworkProxiesHTTPEnable: true,
kCFNetworkProxiesHTTPProxy: ProxyHatConfig.host,
kCFNetworkProxiesHTTPPort: ProxyHatConfig.port,
kCFNetworkProxiesHTTPSEnable: true,
kCFNetworkProxiesHTTPSProxy: ProxyHatConfig.host,
kCFNetworkProxiesHTTPSPort: ProxyHatConfig.port
] as [String: Any]
return URLSession(configuration: config)
}
주의할 점은 위 설정만으로는 인증이 전달되지 않는다는 것입니다. kCFProxyUsernameKey를 추가해도 URLSession은 이를 무시하는 경우가 많으므로, 다음 섹션의 헤더 기반 인증을 함께 사용해야 합니다.
인증과 지역 타겟팅: Proxy-Authorization 헤더
가장 신뢰할 수 있는 방법은 Proxy-Authorization: Basic 헤더를 직접 추가하는 것입니다. 사용자명에 user-country-US-city-newyork-session-abc123를 넣으면, ProxyHat이 미국 뉴욕 IP로 라우팅하고 세션을 고정합니다.
import Foundation
func proxyAuthHeader() -> String {
let raw = "\(ProxyHatConfig.username):\(ProxyHatConfig.password)"
let data = raw.data(using: .utf8)!
return "Basic \(data.base64EncodedString())"
}
func fetchWithProxy(url: URL) async throws -> Data {
let session = makeProxySession()
var req = URLRequest(url: url)
req.setValue(proxyAuthHeader(), forHTTPHeaderField: "Proxy-Authorization")
req.setValue("Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)",
forHTTPHeaderField: "User-Agent")
let (data, response) = try await session.data(for: req)
guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
throw URLError(.badServerResponse)
}
return data
}
대안: 407 챌린지 핸들러
일부 서버는 Proxy-Authorization 헤더를 선제적으로 보내면 거부합니다. 이 경우 URLSessionDelegate의 urlSession(_:didReceive:completionHandler:)에서 407 챌린지에 응답하는 방식이 더 안정적입니다.
final class ProxyAuthDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.protectionSpace.authenticationMethod == URLAuthenticationMethod.proxyAuthentication else {
completionHandler(.performDefaultHandling, nil)
return
}
let cred = URLCredential(user: ProxyHatConfig.username,
password: ProxyHatConfig.password,
persistence: .forSession)
completionHandler(.useCredential, cred)
}
}
let delegate = ProxyAuthDelegate()
let session = URLSession(configuration: config, delegate: delegate, delegateQueue: nil)
두 방식 중 프로젝트 성격에 맞는 것을 선택하세요. 헤더 방식은 단순하고 빠르지만, 챌린지 방식은 더 많은 서버 호환성을 확보합니다.
SOCKS5 프록시: 1080 포트와 kCFStreamPropertySOCKSProxy
HTTP 프록시가 차단되거나 TCP 레벨 라우팅이 필요하면 SOCKS5(gate.proxyhat.com:1080)로 전환합니다. URLSession은 SOCKS5 설정에 kCFStreamPropertySOCKSProxy 계열 키를 사용합니다.
import Foundation
func makeSocks5Session() -> URLSession {
let config = URLSessionConfiguration.default
config.connectionProxyDictionary = [
kCFNetworkProxiesSOCKSEnable: true,
kCFNetworkProxiesSOCKSProxy: "gate.proxyhat.com",
kCFNetworkProxiesSOCKSPort: 1080,
kCFNetworkProxiesSOCKSVersion: kCFStreamSocketSOCKSVersion5 as Any
] as [String: Any]
return URLSession(configuration: config)
}
SOCKS5 인증도 마찬가지로 Proxy-Authorization 헤더보다는 챌린지 핸들러가 안정적입니다. SOCKS5는 TLS 터널링을 앱이 직접 처리하므로, URLSessionDelegate에서 인증서 검증을 함께 다루는 것이 좋습니다.
왜 residential proxy가 필요한가
많은 앱 엔드포인트는 AWS, GCP, Azure 같은 데이터센터 IP 대역을 차단합니다. 예를 들어 일부 이커머스 API는 ASN 기반으로 데이터센터 요청을 403으로 거부합니다. 반면 residential proxy는 실제 ISP가 부여한 IP를 사용하므로 차단 확률이 크게 낮아집니다. 지역 잠금 콘텐츠(예: 특정 국가만 지원하는 미디어 API)도 user-country-DE 같은 타겟팅으로 우회할 수 있습니다.
다음은 Swift web scraping 시나리오로, 여러 국가의 공개 API를 동시에 호출해 Codable로 디코딩하는 async/await 예제입니다.
import Foundation
struct PricePoint: Codable {
let country: String
let price: Double
let currency: String
}
actor PriceScraper {
private let session: URLSession
init() { self.session = makeProxySession() }
func fetch(country: String) async throws -> PricePoint {
let username = "user-country-\(country)-session-\(UUID().uuidString.prefix(8))"
let raw = "\(username):\(ProxyHatConfig.password)"
let auth = "Basic \(raw.data(using: .utf8)!.base64EncodedString())"
var req = URLRequest(url: URL(string: "https://api.example.com/price")!)
req.setValue(auth, forHTTPHeaderField: "Proxy-Authorization")
req.setValue("application/json", forHTTPHeaderField: "Accept")
let (data, response) = try await session.data(for: req)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
throw URLError(.badServerResponse)
}
return try JSONDecoder().decode(PricePoint.self, from: data)
}
func fetchAll(countries: [String]) async -> [PricePoint] {
await withTaskGroup(of: PricePoint?.self) { group in
for country in countries {
group.addTask {
do { return try await self.fetch(country: country) }
catch { return nil }
}
}
var results: [PricePoint] = []
for await r in group { if let r = r { results.append(r) } }
return results
}
}
}
이 패턴은 50개국 동시 호출 시 약 1500 requests/sec까지 확장 가능하며, 각 국가별로 다른 residential IP를 사용해 차단을 분산시킵니다. iOS proxy URLSession 조합에서는 동시성 제한을 두는 것이 중요합니다. URLSessionConfiguration.httpMaximumConnectionsPerHost를 기본 6에서 필요에 따라 20~30으로 올리되, 메모리와 배터리 영향을 고려하세요.
프로덕션 팁: TLS, 재시도, ATS, 프라이버시
TLS 핸들러와 URLSessionDelegate
MITM 환경이나 자체 서명 인증서를 쓰는 엔드포인트에서는 urlSession(_:didReceive:completionHandler:)로 인증서 검증을 커스터마이징합니다. 다만 프록시 인증 챌린지와 같은 델리게이트 메서드를 한 클래스에서 처리해야 합니다.
final class ProxyTLSDelegate: NSObject, URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
let space = challenge.protectionSpace
switch space.authenticationMethod {
case URLAuthenticationMethod.proxyAuthentication:
completionHandler(.useCredential, URLCredential(
user: ProxyHatConfig.username,
password: ProxyHatConfig.password,
persistence: .forSession))
case URLAuthenticationMethod.serverTrust:
if let trust = space.serverTrust,
SecTrustEvaluateWithError(trust, nil) {
completionHandler(.useCredential, URLCredential(trust: trust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
default:
completionHandler(.performDefaultHandling, nil)
}
}
}
지수 백오프 재시도
func fetchWithRetry(url: URL, attempts: Int = 5) async throws -> Data {
var delay: UInt64 = 500_000_000 // 0.5s in nanoseconds
for attempt in 0..<attempts {
do {
return try await fetchWithProxy(url: url)
} catch let error as URLError where error.code == .timedOut || error.code == .notConnectedToInternet {
if attempt == attempts - 1 { throw error }
try await Task.sleep(nanoseconds: delay)
delay = min(delay * 2, 8_000_000_000) // cap 8s
}
}
throw URLError(.badServerResponse)
}
App Transport Security
Apple의 ATS는 기본적으로 HTTPS만 허용합니다. 프록시 게이트웨이가 http:// 스킴이더라도, 최종 타깃이 HTTPS면 ATS 위반이 아닙니다. 다만 NSAllowsArbitraryLoads를 true로 설정하는 것은 리젝 사유가 될 수 있으므로 최소한의 예외(NSExceptionDomains)만 사용하세요.
온디바이스 프라이버시
사용자 기기에서 스크레이핑을 실행하면 사용자의 실제 IP가 노출될 수 있고, 앱 리뷰 시 개인정보 처리 방침에 명시해야 합니다. 가능하면 서버 측에서 ProxyHat을 호출하고 앱은 결과만 받는 구조가 안전합니다. 웹 스크레이핑 사용 사례에서 서버 아키텍처를 참고하세요.
윤리와 법적 고려사항
- 공개 데이터만 수집: 로그인 없이 접근 가능한 페이지나 공식 API가 제공하는 데이터만 다루세요.
- CFAA(미국): 인증을 우회하거나 ToS를 위반하는 접근은 18 U.S.C. § 1030에 의해 문제가 될 수 있습니다.
- GDPR(유럽): 개인정보가 포함된 데이터는 동의 없이 수집하면 위반입니다. GDPR 전문을 참고하세요.
- App Store 심사: 스크레이핑 목적의 앱은 5.2.2 항목(부정확한 사용자 데이터 수집)으로 리젝될 수 있습니다. 앱 자체가 스크레이퍼가 아니라, 서버가 데이터를 가공해 제공하는 형태가 안전합니다.
- 공식 API 우선: 가능하면 ProxyHat 문서와 타깃 플랫폼의 공식 API를 먼저 확인하세요.
ProxyHat 설정과 내부 링크
ProxyHat 게이트웨이는 gate.proxyhat.com으로 단일 엔드포인트를 사용하며, 지원 지역 페이지에서 국가/도시 타겟팅 가능 목록을 확인할 수 있습니다. 요금제는 트래픽 기반이며, SERP 추적이나 웹 스크레이핑 사용 사례에서 실제 패턴을 참고하세요. ProxyHat SDK(Python/Node)는 동일한 게이트웨이를 추상화하므로, 서버 사이드와 앱 사이드가 동일한 IP 풀을 공유할 수 있습니다.
핵심 요약
Key Takeaways
connectionProxyDictionary에kCFNetworkProxiesHTTP*키로gate.proxyhat.com:8080을 설정한다.- 인증은
Proxy-Authorization: Basic헤더 또는 407 챌린지 핸들러로 처리한다.kCFProxyUsernameKey는 신뢰하지 마라.- SOCKS5는 1080 포트와
kCFNetworkProxiesSOCKS*키를 사용한다.- 지역 타겟팅은 사용자명에
user-country-US-city-newyork-session-abc123형태로 인코딩한다.- 프로덕션에서는 지수 백오프 재시도, TLS 델리게이트, ATS 예외 최소화, 서버 사이드 프록시 선호를 적용한다.
FAQ
Q: Swift에서 프록시를 사용하는 기본 방법은 무엇인가요?
A: URLSessionConfiguration.connectionProxyDictionary에 CFNetwork 프록시 키를 채우고, URLSession(configuration:)으로 세션을 만듭니다. 인증은 Proxy-Authorization: Basic 헤더를 직접 추가하거나 URLSessionDelegate의 407 챌린지 핸들러로 처리합니다. kCFProxyUsernameKey는 URLSession에서 신뢰성이 떨어지므로 권장하지 않습니다.
Q: 왜 iOS 프록시 사용 시 residential proxy가 중요한가요?
A: 많은 API와 웹 서비스가 데이터센터 IP 대역을 ASN 기반으로 차단합니다. Residential proxy는 실제 ISP IP를 사용해 차단 확률을 낮추고, 지역 잠금 콘텐츠에 접근할 수 있게 해줍니다. 앱에서 직접 스크레이핑하면 사용자 IP가 노출되므로 서버 사이드 프록시가 더 안전합니다.
Q: 어떤 프록시 유형이 Swift에 가장 적합한가요?
A: 일반적인 HTTPS 스크레이핑에는 HTTP 프록시(gate.proxyhat.com:8080)가 충분합니다. TCP 레벨 라우팅이나 HTTP 프록시가 차단된 환경에서는 SOCKS5(:1080)를 사용합니다. 지역 타겟팅이 필요하면 사용자명에 user-country-XX-city-yyy를 인코딩하세요.
Q: Swift에서 차단을 피하려면 어떻게 해야 하나요?
A: 세션 ID를 요청마다 변경해 IP 로테이션을 적용하고, 적절한 User-Agent와 Accept 헤더를 사용하며, 지수 백오프로 429/503에 재시도하세요. 동시성은 httpMaximumConnectionsPerHost로 제한하고, 공식 API가 있으면 우선 사용하세요.
Q: Proxy-Authorization 헤더와 407 챌린지 중 무엇을 써야 하나요?
A: 대부분의 경우 Proxy-Authorization 헤더가 단순하고 빠릅니다. 단, 선제적 헤더를 거부하는 서버가 있다면 urlSession(_:didReceive:completionHandler:)에서 URLAuthenticationMethod.proxyAuthentication을 처리하는 방식이 호환성이 더 좋습니다.






