Swift URLSession에서 프록시 사용하기: iOS/macOS 실전 가이드

Swift URLSession으로 residential, datacenter, SOCKS5 프록시를 설정하는 방법부터 인증, 지역 타겟팅, 동시 요청, TLS, 재시도 전략까지 코드 중심으로 설명합니다.

Using Proxies in Swift: A Code-First URLSession Guide for iOS & macOS

iOS와 macOS 앱에서 네트워크 요청을 보낼 때, 데이터센터 IP가 차단되거나 지역 제한 콘텐츠에 접근해야 하는 상황은 흔합니다. Swift에서 프록시 사용하기(Using Proxies in Swift)의 핵심은 URLSessionConfiguration.connectionProxyDictionary를 올바르게 구성하고, 인증 헤더를 직접 제어하며, 비동기 패턴으로 안정성을 확보하는 것입니다. 이 글은 실제 코드 예제와 함께 그 모든 과정을 다룹니다.

Swift proxy 설정은 URLSession 레벨에서 처리하는 것이 가장 안전하고 유연합니다. 시스템 전체 프록시를 변경하면 앱 외부의 트래픽까지 영향을 받기 때문입니다. iOS proxy URLSession 조합은 앱 샌드박스 내에서만 동작하며, 이는 App Store 심사 가이드라인과도 정렬됩니다.

Swift에서 프록시 사용하기: 왜 필요한가

많은 API 엔드포인트와 웹 서비스가 데이터센터 IP 대역을 자동으로 차단합니다. AWS, GCP, Azure 등 클라우드 제공업체의 IP 범위는 공개되어 있으며, IPv4 주소 고갈로 인해 데이터센터 대역과 일반 사용자 대역의 구분이 더욱 명확해졌습니다. residential 프록시는 실제 ISP가 부여한 주소를 사용하므로, 일반 사용자의 요청과 구분하기 어렵습니다.

지역 제한 콘텐츠도 중요한 이유입니다. 특정 국가에서만 제공되는 미디어, 가격 정보, SERP 결과를 수집하려면 해당 국가의 IP가 필요합니다. Swift web scraping 작업에서는 국가, 도시 단위의 지역 타겟팅이 필수적입니다.

ProxyHat은 residential, mobile, datacenter 프록시를 제공하며, 게이트웨이 주소 gate.proxyhat.com의 HTTP 포트 8080, SOCKS5 포트 1080을 통해 접근합니다. 자세한 내용은 웹 스크래핑 사용 사례프록시 위치 페이지를 참조하세요.

URLSession 프록시 설정 기본: connectionProxyDictionary

URLSession에 프록시를 적용하려면 URLSessionConfigurationconnectionProxyDictionary 속성을 설정합니다. 이 딕셔너리는 CFNetwork 프레임워크의 kCFNetworkProxies* 키를 사용합니다.

HTTP 및 HTTPS 프록시 기본 설정

import Foundation

func makeProxySession(username: String, password: String) -> URLSession {
    let config = URLSessionConfiguration.ephemeral
    
    config.connectionProxyDictionary = [
        // HTTP 프록시 활성화
        kCFNetworkProxiesHTTPEnable as String: 1,
        kCFNetworkProxiesHTTPProxy as String: "gate.proxyhat.com",
        kCFNetworkProxiesHTTPPort as String: 8080,
        
        // HTTPS 프록시 활성화 (CONNECT 터널링)
        kCFNetworkProxiesHTTPSEnable as String: 1,
        kCFNetworkProxiesHTTPSProxy as String: "gate.proxyhat.com",
        kCFNetworkProxiesHTTPSPort as String: 8080
    ]
    
    // 인증은 헤더로 직접 처리 (아래 참조)
    let token = \(username):\(password)\n    let encoded = Data(token.utf8).base64EncodedString()
    
    config.httpAdditionalHeaders = [
        "Proxy-Authorization": "Basic \(encoded)"
    ]
    
    return URLSession(configuration: config)
}

kCFNetworkProxiesHTTPProxykCFNetworkProxiesHTTPSProxy는 호스트 문자열, 포트 키는 정수값을 받습니다. HTTPS 요청의 경우 URLSession은 프록시에 CONNECT 메서드로 터널을 맺고 그 위에서 TLS를 수행합니다.

핵심: kCFProxyUsernameKey / kCFProxyPasswordKeyconnectionProxyDictionary에 넣을 수 있지만, URLSession에서는 이 키들이 신뢰할 수 없게 동작합니다. 인증은 Proxy-Authorization 헤더 또는 URLSessionDelegate의 407 챌린지 처리로 직접 구현해야 합니다.

인증과 지역 타겟팅: 사용자 이름에 플래그 인코딩

ProxyHat은 사용자 이름에 국가, 도시, 세션 플래그를 인코딩하는 방식을 사용합니다. 예를 들어 user-country-US-city-newyork-session-abc123 형식입니다. 이를 Base64로 인코딩하여 Proxy-Authorization: Basic 헤더로 전송합니다.

import Foundation

struct ProxyConfig {
    let baseUsername: String
    let password: String
    let country: String?
    let city: String?
    let session: String?
    
    var fullUsername: String {
        var u = baseUsername
        if let country = country {
            u += "-country-\(country)"
        }
        if let city = city {
            u += "-city-\(city)"
        }
        if let session = session {
            u += "-session-\(session)"
        }
        return u
    }
    
    var basicAuthHeader: String {
        let token = "\(fullUsername):\(password)"
        let encoded = Data(token.utf8).base64EncodedString()
        return "Basic \(encoded)"
    }
}

// 사용 예: 미국 뉴욕, 세션 고정
let proxy = ProxyConfig(
    baseUsername: "myuser",
    password: "mypass",
    country: "US",
    city: "newyork",
    session: "abc123"
)

let config = URLSessionConfiguration.ephemeral
config.connectionProxyDictionary = [
    kCFNetworkProxiesHTTPEnable as String: 1,
    kCFNetworkProxiesHTTPProxy as String: "gate.proxyhat.com",
    kCFNetworkProxiesHTTPPort as String: 8080,
    kCFNetworkProxiesHTTPSEnable as String: 1,
    kCFNetworkProxiesHTTPSProxy as String: "gate.proxyhat.com",
    kCFNetworkProxiesHTTPSPort as String: 8080
]
config.httpAdditionalHeaders = [
    "Proxy-Authorization": proxy.basicAuthHeader
]

let session = URLSession(configuration: config)

407 챌린지 처리 (대체 방법)

httpAdditionalHeaders 대신 URLSessionDelegate로 407 Proxy Authentication Required 응답을 가로채서 인증 정보를 제공할 수도 있습니다.

final class ProxyAuthDelegate: NSObject, URLSessionDelegate {
    let proxyConfig: ProxyConfig
    
    init(proxyConfig: ProxyConfig) {
        self.proxyConfig = proxyConfig
    }
    
    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
    ) {
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPProxy ||
           challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPSProxy {
            let cred = URLCredential(
                user: proxyConfig.fullUsername,
                password: proxyConfig.password,
                persistence: .forSession
            )
            completionHandler(.useCredential, cred)
        } else {
            completionHandler(.performDefaultHandling, nil)
        }
    }
}

let delegate = ProxyAuthDelegate(proxyConfig: proxy)
let sessionWithDelegate = URLSession(
    configuration: config,
    delegate: delegate,
    delegateQueue: nil
)

두 방식 모두 작동하지만, Proxy-Authorization 헤더 방식이 더 예측 가능하고 디버깅이 쉽습니다. 407 챌린지 방식은 URLSession 내부 재시도 로직이 추가 왕복을 유발할 수 있어 약 100ms~200ms의 지연이 발생할 수 있습니다.

SOCKS5 프록시 설정: 포트 1080

ProxyHat은 SOCKS5 게이트웨이도 gate.proxyhat.com:1080에서 제공합니다. SOCKS5는 HTTP CONNECT 터널링과 달리 프록시 레벨에서 인증이 명확하게 정의되어 있습니다.

import Foundation

func makeSOCKS5Session(proxy: ProxyConfig) -> URLSession {
    let config = URLSessionConfiguration.ephemeral
    
    let token = "\(proxy.fullUsername):\(proxy.password)"
    let encoded = Data(token.utf8).base64EncodedString()
    
    config.connectionProxyDictionary = [
        kCFNetworkProxiesSOCKSEnable as String: 1,
        kCFNetworkProxiesSOCKSProxy as String: "gate.proxyhat.com",
        kCFNetworkProxiesSOCKSPort as String: 1080,
        kCFNetworkProxiesSOCKSVersion as String: kCFNetworkProxiesSOCKSVersion5 as String,
        kCFNetworkProxiesSOCKSAuthentication as String: 1,
        kCFNetworkProxiesSOCKSUsername as String: proxy.fullUsername,
        kCFNetworkProxiesSOCKSPassword as String: proxy.password
    ]
    
    return URLSession(configuration: config)
}

SOCKS5는 모든 TCP 트래픽을 터널링하므로 HTTP뿐 아니라 WebSocket, gRPC 등에도 적용할 수 있습니다. 단, SOCKS5에서는 Proxy-Authorization 헤더가 의미가 없으며, kCFNetworkProxiesSOCKSUsername / kCFNetworkProxiesSOCKSPassword 키가 실제로 작동합니다.

async/await로 동시 요청 보내기: TaskGroup 예제

여러 지역의 데이터를 동시에 수집하는 시나리오를 생각해봅니다. Swift의 async/awaitTaskGroup을 사용하면 동시성을 안전하게 관리할 수 있습니다.

import Foundation

struct PriceData: Codable {
    let product: String
    let price: Double
    let currency: String
    let region: String
}

struct RegionRequest {
    let country: String
    let city: String
    let session: String
    let url: URL
}

enum FetchError: Error {
    case invalidResponse
    case decodingFailed
    case httpError(Int)
}

func fetchPriceData(
    for request: RegionRequest,
    baseUsername: String,
    password: String
) async throws -> PriceData {
    let proxy = ProxyConfig(
        baseUsername: baseUsername,
        password: password,
        country: request.country,
        city: request.city,
        session: request.session
    )
    
    let config = URLSessionConfiguration.ephemeral
    config.connectionProxyDictionary = [
        kCFNetworkProxiesHTTPEnable as String: 1,
        kCFNetworkProxiesHTTPProxy as String: "gate.proxyhat.com",
        kCFNetworkProxiesHTTPPort as String: 8080,
        kCFNetworkProxiesHTTPSEnable as String: 1,
        kCFNetworkProxiesHTTPSProxy as String: "gate.proxyhat.com",
        kCFNetworkProxiesHTTPSPort as String: 8080
    ]
    config.httpAdditionalHeaders = [
        "Proxy-Authorization": proxy.basicAuthHeader
    ]
    config.timeoutIntervalForRequest = 30
    config.timeoutIntervalForResource = 60
    
    let urlSession = URLSession(configuration: config)
    
    var req = URLRequest(url: request.url)
    req.httpMethod = "GET"
    req.setValue("application/json", forHTTPHeaderField: "Accept")
    
    let (data, response) = try await urlSession.data(for: req)
    
    guard let httpResponse = response as? HTTPURLResponse else {
        throw FetchError.invalidResponse
    }
    
    guard (200...299).contains(httpResponse.statusCode) else {
        throw FetchError.httpError(httpResponse.statusCode)
    }
    
    do {
        return try JSONDecoder().decode(PriceData.self, from: data)
    } catch {
        throw FetchError.decodingFailed
    }
}

func fetchMultipleRegions(
    requests: [RegionRequest],
    baseUsername: String,
    password: String
) async -> [(RegionRequest, Result<PriceData, Error>)] {
    await withTaskGroup(of: (RegionRequest, Result<PriceData, Error>).self) { group in
        var results: [(RegionRequest, Result<PriceData, Error>)] = []
        
        for request in requests {
            group.addTask {
                do {
                    let data = try await fetchPriceData(
                        for: request,
                        baseUsername: baseUsername,
                        password: password
                    )
                    return (request, .success(data))
                } catch {
                    return (request, .failure(error))
                }
            }
        }
        
        for await result in group {
            results.append(result)
        }
        
        return results
    }
}

// 사용 예
let requests = [
    RegionRequest(country: "US", city: "newyork", session: "s1", url: URL(string: "https://api.example.com/price")!),
    RegionRequest(country: "DE", city: "berlin", session: "s2", url: URL(string: "https://api.example.com/price")!),
    RegionRequest(country: "JP", city: "tokyo", session: "s3", url: URL(string: "https://api.example.com/price")!)
]

Task {
    let results = await fetchMultipleRegions(
        requests: requests,
        baseUsername: "myuser",
        password: "mypass"
    )
    for (req, result) in results {
        switch result {
        case .success(let data):
            print("\(req.country): \(data.price) \(data.currency)")
        case .failure(let error):
            print("\(req.country) 실패: \(error)")
        }
    }
}

각 요청마다 별도의 URLSession 인스턴스를 생성하는 점에 주의하세요. 동일한 세션을 공유하면 IP가 고정되어 지역 타겟팅이 무의미해집니다. TaskGroup은 Swift의 구조적 동시성을 제공하므로, 한 요청이 실패해도 다른 요청에 영향을 주지 않습니다.

curl로 동작 확인하기

Swift 코드를 작성하기 전에 curl로 프록시 연결을 검증하는 것이 좋습니다.

# HTTP 프록시 + 지역 타겟팅
curl -x http://user-country-US-city-newyork-session-abc123:pass@gate.proxyhat.com:8080 \
  https://httpbin.org/ip

# SOCKS5 프록시
curl -x socks5://user-country-DE:pass@gate.proxyhat.com:1080 \
  https://httpbin.org/ip

프록시 유형 비교

프록시 유형IP 출처차단 회피지연비용적합한 용도
Residential실제 ISP 주소매우 높음200ms~800ms중간~높음SERP 스크래핑, 지역 제한 콘텐츠
Mobile모바일 통신사매우 높음300ms~1200ms높음모바일 전용 API, 앱 엔드포인트
Datacenter클라우드/호스팅낮음50ms~200ms낮음공개 API 호출, 속도 우선 작업

데이터센터 프록시는 속도가 빠르지만 차단 확률이 높습니다. residential 프록시는 지연이 더 크지만 차단 회피율이 95% 이상으로 보고됩니다. 용도에 따라 적절한 유형을 선택하세요. 가격 페이지에서 각 유형의 요금을 확인할 수 있습니다.

프로덕션 팁: TLS, 재시도, ATS

URLSessionDelegate로 TLS 검증 커스터마이징

일부 환경에서는 인증서 핀닝이나 커스텀 CA가 필요합니다. URLSessionDelegateurlSession(_:didReceive:completionHandler:) 메서드로 TLS 챌린지를 처리합니다.

final class TLSPinningDelegate: NSObject, URLSessionDelegate {
    let pinnedCertData: Data
    
    init(pinnedCertData: Data) {
        self.pinnedCertData = pinnedCertData
    }
    
    func urlSession(
        _ session: URLSession,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
    ) {
        guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
              let trust = challenge.protectionSpace.serverTrust else {
            completionHandler(.performDefaultHandling, nil)
            return
        }
        
        let certCount = SecTrustGetCertificateCount(trust)
        guard certCount > 0,
              let serverCert = SecTrustGetCertificateAtIndex(trust, 0) else {
            completionHandler(.cancelAuthenticationChallenge, nil)
            return
        }
        
        let serverCertData = SecCertificateCopyData(serverCert) as Data
        
        if serverCertData == pinnedCertData {
            completionHandler(.useCredential, URLCredential(trust: trust))
        } else {
            completionHandler(.cancelAuthenticationChallenge, nil)
        }
    }
}

지수 백오프 재시도

import Foundation

enum RetryError: Error {
    case maxAttemptsExceeded
}

func fetchWithRetry(
    url: URL,
    session: URLSession,
    maxAttempts: Int = 5,
    baseDelay: TimeInterval = 1.0
) async throws -> Data {
    var lastError: Error?
    
    for attempt in 0..<maxAttempts {
        do {
            let (data, response) = try await session.data(from: url)
            
            if let http = response as? HTTPURLResponse {
                // 429 또는 5xx는 재시도
                if http.statusCode == 429 || (500...599).contains(http.statusCode) {
                    let delay = baseDelay * pow(2.0, Double(attempt))
                    try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
                    lastError = FetchError.httpError(http.statusCode)
                    continue
                }
            }
            
            return data
        } catch {
            lastError = error
            let delay = baseDelay * pow(2.0, Double(attempt))
            try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
        }
    }
    
    throw lastError ?? RetryError.maxAttemptsExceeded
}

지수 백오프는 서버 부하를 줄이고 차단을 피하는 데 효과적입니다. Retry-After 헤더가 있으면 그 값을 우선 사용해야 합니다. MDN의 Retry-After 문서를 참조하세요.

App Transport Security (ATS)

iOS 9 이상에서 ATS는 기본적으로 HTTPS 연결만 허용합니다. 프록시 자체는 HTTP로 연결하더라도, 대상 서버는 HTTPS여야 합니다. Info.plist에서 NSAppTransportSecurity를 조정할 수 있지만, 가능하면 기본 설정을 유지하세요. 프록시 게이트웨이에 HTTP로 연결하는 것은 ATS 제약에 영향을 받지 않습니다 — ATS는 대상 서버의 TLS를 검증하기 때문입니다.

온디바이스 프라이버시

URLSessionConfiguration.ephemeral을 사용하면 캐시, 쿠키, 자격 증명이 메모리에만 저장되고 디스크에 기록되지 않습니다. 프록시 인증 정보가 기기에 영속되는 것을 방지하려면 항상 ephemeral 구성을 사용하세요.

ProxyHat SDK와의 관계

ProxyHat은 Python과 Node.js용 SDK를 제공하지만, 게이트웨이 프로토콜은 동일합니다. Swift에서는 위와 같이 connectionProxyDictionary로 직접 구성하면 됩니다. Python SDK의 ProxyHat 클래스도 내부적으로 gate.proxyhat.com:8080을 호출합니다. 공식 문서는 docs.proxyhat.com에서 확인하세요. SERP 추적 관련 사용 사례는 SERP 추적 페이지를 참조하세요.

윤리와 법적 고려사항

프록시를 사용한 데이터 수집은 공개 데이터에 한하고, 각 사이트의 이용약관과 robots.txt를 존중해야 합니다. 미국에서는 Computer Fraud and Abuse Act (CFAA)가 무단 접근을 제한하며, EU에서는 GDPR이 개인정보 처리를 규율합니다. 인증이 필요한 페이지를 우회하거나 저작권이 있는 콘텐츠를 무단으로 수집하는 것은 법적 위험이 있습니다.

가능하면 공식 API를 우선 사용하세요. 많은 플랫폼이 정식 API를 제공하며, 이는 안정성과 법적 안전성 모두에서 우월합니다. 프록시가 필요한 경우에도 요청 빈도를 합리적으로 유지하고, 서버에 부담을 주지 않는 것이 좋습니다.

App Store 심사 가이드라인도 주의해야 합니다. 앱이 백그라운드에서 과도한 네트워크 요청을 보내거나 사용자 동의 없이 데이터를 수집하면 리젝될 수 있습니다. App Store Review Guidelines의 데이터 수집 관련 조항을 확인하세요.

Key Takeaways

  • URLSessionConfiguration.connectionProxyDictionarykCFNetworkProxiesHTTP* / kCFNetworkProxiesHTTPS* 키로 gate.proxyhat.com:8080을 설정한다.
  • 인증은 Proxy-Authorization: Basic 헤더로 직접 처리한다. kCFProxyUsernameKey는 URLSession에서 신뢰할 수 없다.
  • 사용자 이름에 user-country-US-city-newyork-session-abc123 형식으로 지역과 세션을 인코딩한다.
  • SOCKS5는 kCFNetworkProxiesSOCKS* 키로 포트 1080에 설정한다. SOCKS5는 Username/Password 키가 실제로 작동한다.
  • async/await + TaskGroup으로 동시 요청을 안전하게 관리한다. 각 지역마다 별도 URLSession 인스턴스를 사용한다.
  • 지수 백오프 재시도, ephemeral 구성, TLS 핀닩을 프로덕션에 적용한다.
  • 공개 데이터만 수집하고, CFAA, GDPR, App Store 가이드라인을 준수한다. 공식 API를 항상 우선한다.

FAQ

Swift에서 프록시 사용하기란 무엇인가요?

Swift에서 프록시 사용하기는 URLSessionConfiguration.connectionProxyDictionary를 통해 HTTP, HTTPS, SOCKS5 프록시를 URLSession에 적용하는 작업입니다. 시스템 전체 프록시 설정이 아닌 앱 단위로 프록시를 구성하므로, 다른 앱이나 시스템 트래픽에 영향을 주지 않습니다. ProxyHat 게이트웨이 gate.proxyhat.com:8080을 HTTP/HTTPS 프록시로, :1080을 SOCKS5 프록시로 설정할 수 있습니다.

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

데이터센터 IP가 차단되는 API 엔드포인트에 접근하고, 지역 제한 콘텐츠를 수집하며, SERP 스크래핑을 안정적으로 수행하려면 residential 프록시가 필수적입니다. Swift URLSession은 iOS와 macOS 모두에서 동일한 API를 제공하므로, 코드를 재사용하면서 프록시 기반 네트워킹을 구현할 수 있습니다. 또한 앱 샌드박스 내에서만 동작하므로 App Store 심사 기준과도 정렬됩니다.

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

용도에 따라 다릅니다. SERP 스크래핑이나 지역 제한 콘텐츠 접근에는 residential 프록시가 가장 적합합니다 — 차단 회피율이 95% 이상으로 보고됩니다. 공개 API 호출이나 속도가 우선인 작업에는 datacenter 프록시가 적합하며, 지연이 50ms~200ms로 낮습니다. 모바일 전용 API 엔드포인트에는 mobile 프록시가 필요할 수 있습니다. HTTP/HTTPS 트래픽에는 포트 8080, SOCKS5가 필요한 경우 포트 1080을 사용하세요.

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

세션 ID를 고유하게 유지하여 IP를 요청마다 회전시키고, 지수 백오프로 429 응답을 처리하며, 요청 간 적절한 지연을 두세요. Proxy-Authorization 헤더로 인증하고, user-country-US-session-xyz 형식으로 세션을 관리합니다. User-Agent와 Accept 헤더를 실제 브라우저와 유사하게 설정하고, URLSessionConfiguration.ephemeral로 쿠키와 캐시가 디스크에 남지 않도록 합니다. robots.txt와 이용약관을 항상 존중하세요.

URLSession에서 SOCKS5 프록시를 사용할 수 있나요?

네, 가능합니다. connectionProxyDictionarykCFNetworkProxiesSOCKSEnable을 1로 설정하고, kCFNetworkProxiesSOCKSProxygate.proxyhat.com, kCFNetworkProxiesSOCKSPort1080을 지정하면 됩니다. SOCKS5에서는 kCFNetworkProxiesSOCKSUsernamekCFNetworkProxiesSOCKSPassword 키가 실제로 작동하므로, HTTP 프록시와 달리 별도의 인증 헤더가 필요하지 않습니다.

시작할 준비가 되셨나요?

AI 필터링으로 148개국 이상에서 5천만 개 이상의 레지덴셜 IP에 액세스하세요.

가격 보기레지덴셜 프록시
← 블로그로 돌아가기