[Apple] Fetching Website Data into Memory

Reference


Remind

  • Task들은 기본적으로 suspend 상태에서 생성되므로 resume()을 호출하여 시작하게 해야 한다
  • completion handler에서 error, status code, MIME type을 확인하도록 구현한다
  • completion handler는 Task를 생성한 것과 다른 GCD 큐에서 호출된다
    (따라서, UI 업데이트는 Main 큐에서 수행하도록 구현해야 한다)
  • Delegate를 사용하면 Task 진행과 관련한 각종 event를 수신할 수 있다
  • shared session은 delegate를 지정할 수 없다

⚙️ Overview

서버와의 소규모 인터렉션에 대한 response data를, URLSessionDataTask로 메모리에 넣거나, URLSessionDownloadTask로 파일로 저장할 수 있습니다. Data Task는 웹서비스 endpoint를 호출한다던지 하는 용도에 이상적입니다

URLSession.shared
URLSession 인스턴스를 사용하여 Task를 생성할 수 있습니다. URLSession에 별다른 커스텀 설정이 필요없다면, 이미 만들어져 있는 shared 인스턴스를 사용할 수 있습니다

Delegate callback이 필요한 경우
만약 delegate callback으로 Data 전송과 상호작용하려면, shared 대신 session 인스턴스를 만들어 써야 합니다. session을 만들 때, URLSessionConfiguration 인스턴스를 사용하고 URLSessionDelegate를 구현하는 class를 전달해야 합니다

session은 재사용될 수 있다
session은 여러 Task를 만들기 위해 재사용될 수 있기 때문에, 만약 Task마다 configuration을 달리 하려면 Task별 session을 새로 생성하고 configuration을 프로퍼티로 넣어줘야 합니다

NOTE
필요 이상으로 session을 만들지 않도록 유의해야 합니다. 예로, 여러 코드에서 유사한 configuration을 가지는 session이 필요하다면 하나만 만들어서 공유하게 합니다

dataTask() / resume()
session을 만들어 가지고 있다면, dataTask() 메서드 중 하나를 호출하여 Data Task를 생성할 수 있습니다. Task들은 기본적으로 suspend 상태에서 생성되므로 resume()을 호출하여 시작하게 합니다


⚙️ Receive Results with a Completion Handler

간단한 방식
Data를 fetch하는 가장 간단한 방법은 completion handler를 사용하는 Data Task를 만드는 것입니다. 이 방식으로, Task는 서버의 response,data,error를 우리가 정의한 completion handler 코드블럭에게 보냅니다. 아래 그림은 session과 Task의 관계와, 어떻게 result가 completion handler로 보내지는지를 보여줍니다

Completion Handler에 구현할 것
completion handler를 사용하는 Data Task를 만들려면, session 메서드 중 dataTask(with:)를 호출합니다. 이 메서드의 파라미터로 전달할 completion handler 코드블럭에는 아래 3가지가 필요합니다

  1. error가 nil인지 확인할 것
    nil이 아니라면 request 전송 오류가 발생한 것이므로 처리가 필요합니다

  2. response의 status code와 MIME type이 예상한 것인지 확인할 것
    그렇지 않다면, 서버 에러 처리가 필요합니다

  3. 필요에 따라, data 인스턴스를 사용할 것

//예제코드
func startLoad() {
    let url = URL(string: "https://www.example.com/")!
    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            self.handleClientError(error)
            return
        }
        guard let httpResponse = response as? HTTPURLResponse,
            (200...299).contains(httpResponse.statusCode) else {
            self.handleServerError(response)
            return
        }
        if let mimeType = httpResponse.mimeType, mimeType == "text/html",
            let data = data,
            let string = String(data: data, encoding: .utf8) {
            DispatchQueue.main.async {
                self.webView.loadHTMLString(string, baseURL: url)
            }
        }
    }
    task.resume()
}

❗️중요❗️ : UI 업데이트는 Main 큐에서
completion handler는 Task를 생성한 것과 다른 GCD 큐에서 호출됩니다. 그러므로, data 혹은 error를 사용하여 UI를 업데이트하는 경우, 위 코드처럼 명시적으로 Main 큐에서 수행하도록 해야 합니다


⚙️ Receive Transfer Details and Results with a Delegate

Task 진행상태를 더 자세하게 알고 싶다면
Task 진행상태에 대해 더 자세하게 접근하고 싶다면, Data Task를 생성할 때 completion handler 대신 session에 delegate를 설정해놓는 방법이 있습니다. 아래 그림 참고

Delegate는 Data 전송 중 발생하는 각종 event를 수신한다
이 방식에서는 data의 일부가 도착하면 URLSessionDataDelegate의 메서드인 urlSession(_:dataTask:didReceive:)로 제공됩니다. 이는 data 전송이 완료되거나 에러로 인해 실패할 때까지 계속됩니다. 또한, delegate는 (data 일부 도착 외에) data 전송 중 발생하는 다른 종류의 event들도 수신합니다

shared는 delegate 불가
delegate 방식을 사용하려면 shared가 아닌 별도의 URLSession 인스턴스를 직접 만들어야 합니다. 새로운 session을 만들면, session의 delegate를 설정해줄 수 있습니다

delegate를 사용하는 URLSession 생성 예시
delegate가 될 custom class가 delegate 프로토콜 중 하나 이상을 채택하게 구현합니다 (URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate, and URLSessionDownloadDelegate). 그리고 URLSession 인스턴스를 만듭니다. 또한, 여기서 configuration 인스턴스를 커스터마이즈할 수 있는데, 그 예로 waitsForConnectivity를 true로 두는 것은 좋은 아이디어입니다

class MyClass: URLSessionDelegate {
  private lazy var session: URLSession = {
      let configuration = URLSessionConfiguration.default
      configuration.waitsForConnectivity = true
      return URLSession(configuration: configuration,
                        delegate: self, delegateQueue: nil)
  }()
  ...
}

delegate 메서드 구현 예시

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse,
                completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
    guard let response = response as? HTTPURLResponse,
        (200...299).contains(response.statusCode),
        let mimeType = response.mimeType,
        mimeType == "text/html" else {
        completionHandler(.cancel)
        return
    }
    completionHandler(.allow)
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
    self.receivedData?.append(data)
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
    DispatchQueue.main.async {
        self.loadButton.isEnabled = true
        if let error = error {
            handleClientError(error)
        } else if let receivedData = self.receivedData,
            let string = String(data: receivedData, encoding: .utf8) {
            self.webView.loadHTMLString(string, baseURL: task.currentRequest?.url)
        }
    }
}

위 예제 코드 뿐만 아니라 다양한 delegate 프로토콜들이 다양한 메서드들을 제공합니다. 예로, 사용자 인증 관련 처리, redirect 등이 있습니다.

좋은 웹페이지 즐겨찾기