URLSession, Alamofire 간단한 내용

rbw·2023년 9월 19일
0

TIL

목록 보기
90/99

https://medium.com/@greenSyntax/network-layer-in-ios-1-4-bf15ba91347e 이 글을 보고 번역/정리한 글 자세한 내용은 위 블로그를 참조 wind

URLSession

이는 Foundation 프레임워크의 일부입니다. 네트워크 요청을 할 때 사용합니다. 주요 세 가지 작업이 있는데

  1. Data Task - GET POST PUT DELETE 요청들
  2. Download Task - 웹으로부터 데이터를 다운로드
  3. Upload Task - 웹으로 데이터를 업로드

이 존재함니당. 위 작업들을 작성하기 전에 항상 URLSession, URLSessionConfiguration을 신경써야 합니다

URLSessionConfiguration은 URLSession에 대한 종속성으로, 사용할 때 먼저 얘를 생성하고 URLSession에 주입을 시킵니다. 그리고 URLSession이 사용할 준비가 된다면 위 작업들 중 하나를 수행합니다.

URLSessionConfiguration

얘의 기본 타입들을 살펴보면

  1. default - 기본형
  2. ephemeral - 영구 저장소를 사용하지 않을 때, 캐시 쿠키, 자격증명을 디스크에 기록 x
  3. background(withIdentifier: "app.example.name") - 백그라운드 지원을 받을 수 있을 때, 앱이 실행되지 않는 동안 업로드 및 다운로드 수행 가능

가 있습니다. 그리고 모든 구성에는 몇 개의 설정 프로퍼티가 있는데

  • timeoutIntervalForRequest: 추가 데이터를 기다릴 때 사용할 타임아웃 간격
  • httpAdditionalHeaders: 말 그대로 헤더(딕셔너리임)를 추가하는 프로퍼티

URLSessionConfiguration의 준비가 되었다면 URLSession을 만들 준비가 된것임니다.

final class URLSessionApiClient {
    
    private let configuration: URLSessionConfiguration
    private let session: URLSession
    
    init() {
        self.configuration = URLSessionConfiguration.default
        self.configuration.timeoutIntervalForRequest = 30.0
        self.configuration.httpAdditionalHeaders = ["Content-Type": "application/json"]
        
        // Create URLSession
        self.session = URLSession(configuration: self.configuration)
    }
}

위 작업이 너무 번거롭다고 느껴진다면 URLSession이 기본인 URLSession.shared를 제공합니다.

DataTask with GET

이 친구는 REST API를 사용하는데 필요한 HTTP 요청을 나타냅니다. 얜 그리고 비동기 메소드입니다. 리턴값으로는 Data, URLResponse, Error 객체를 리턴합니다.

func dataTask(_ url: URL) {
    let urlRequest = URLRequest(url: url)
    self.session.dataTask(with: urlRequest) { data, response, error in
        //TODO: Datatask Callback
        print("HTTP Data:\(data)")
    }.resume()
}

dataTask(_:)는 항상 URLRequest 타입을 기대하므로 만들기 위해서는 url을 사용해서 생성해야합니다. 또한 이 친구는 비동기 메소드이기 때문에 우리는 해당 메소드에 컴플리션 핸들러를 추가 할 필요가 있슴니다.

func dataTask(_ url: URL, onCompletion: @escaping (_ result: Result<Data, Error>) -> Void) {
    let urlRequest = URLRequest(url: url)
    self.session.dataTask(with: urlRequest) { data, response, error in
        
        // onFailure
        if let err = error {
            onCompletion(.failure(err))
            return
        }
        
        // onSuccess
        if let data = data {
            onCompletion(.success(data))
        } else {
            onCompletion(.failure(AppError.noHttpBody))
        }
    }.resume()
}

이제 이 친구를 사용할 땐 클로저를 주고 Result 타입으로 에러인지 성공인지 체크하면 됨니다 ! 저희가 이제 할 거로는 Result<Data, Error> 타입을 좀 더 제네릭하게 바꿔보겠습니다 ! 간단함니다

func dataTask<T: Codable>(_ url: URL, onCompletion: @escaping (_ result: Result<T, Error>) -> Void) {
    ...
    if let data = try? JSONDecoder().decode(T.self, from: data) {
        ...
    }
}

// vc에서는 아래처럼 타입을 명시해서 사용하면 됩니다.
override func viewDidLoad() {
    ...
    dataTask(url) { (_ result: Result<TestModel, Error>) in
        ...
    }
}

이제 GET은 어느정도 보았으니 POST를 살펴보겠슴다

DataTask with POST

저희는 dataTask() 메소드를 개선하여 url만 보내는거 대신 모든 HTTP에 필요한 다른 매개변수를 보낼겁니당 해당 파라미터는 아래와 같습니다.

  1. API URL
  2. HTTP Method
  3. Custom Headers
  4. Query Params in URL
  5. HTTP Body

이 친구들을 열거형과 구조체로 만들면 더 사용성이 좋겠네요

enum HTTPMethod: String {
    case GET
    case POST
    case PUT
    case DELETE
}

struct APIRequest {
    let url: URL
    let method: HTTPMethod
    let headers: [String: String]?
    let queryParams: [String: Any]?
    let body: Data?
}

이제 URL 대신 APIREquest를 받도록 dataTask() 메서드를 리팩토링하겠습니다

// 최종적인 URL을 반환하는 함수를 만들었슴니다 URL 처리를 여기서 하면 좋겠네용
private func prepareURL(_ api: APIRequest) -> URL? {
    var urlComponents = URLComponents(string: api.url.absoluteString)
    let queryItems = api.queryParams?.map({ (key, value) in
        return URLQueryItem(name: key, value: String(describing: value) )
    })
    urlComponents?.queryItems = queryItems
    return urlComponents?.url
}

func dataTask<T: Codable>(_ api: APIRequest, onCompletion: @escaping (_ result: Result<T, Error>) -> Void) {
    
    guard let url = prepareURL(api) else {
        return onCompletion(.failure(AppError.invalidURL))
    }
    
    var urlRequest = URLRequest(url: url)
    urlRequest.httpMethod = api.method.rawValue
    urlRequest.allHTTPHeaderFields = api.headers
    urlRequest.httpBody = api.body
    ... 
    // 아래는 기존의 datatTask와 같습니다 
}

// 사용할 때 
override func viewDidLoad(){
    ...
    // GET
    let apiRequest = APIRequest(url: url, method: .GET, headers: nil, queryParams: nil, body: nil)
    dataTask(apiRequest) { (_ result: Result<TestModel, Error>) in
        ... 
    }

    // POST
    let newTest = TestModel(user: 1234, id: 1234, title: "tetst")
    let newTestData = try? JSONEncoder().encode(newTest)

    let postsAPI = URL(string: "~~~")
    let apiRequest = APIRequest(url: postsAPI, method: .POST, headers:
    ["Content-Type":"application/json"], queryParams: nil, body: newTestData)
    dataTask(apiRequest) { (_ result: Result<TestModel, Error>) in
        ... 
    }
}

함수 파라미터의 기본값을 nil로 주는것도 좋을거같네요 이건 팀원과 협의하면 좋을듯 합니다

이번 파트의 마지막으로는 유효성 검사를 넣으려고 함니다. 아래 코드를 dataTask 함수 내부의 session.dataTask 클로저에 넣어준다면 끗임니다

guard (200...299).contains((response as? HTTPURLResponse)?.statusCode ?? 0) else {
    onCompletion(.failure(AppError.httpFailure))
    return
}

Introduce Alamofire

알라모파이어는 유명하죠 다들 많이 사용하는데 한 번 살펴보겠씁니다. 이 친구는 URLSession을 대신해서 사용할 수 이씁니다. 여러 기능이 존재함니다 Alamofire 깃허브 페이지를 들어가면 많이 나와이씀니다 !

이제 저희는 이 친구를 사용해서 위의 동작을 동일하게 작성해보려고 합니다. Alamofire는 URLSession의 추상화라는 점을 알아두세여

// 이렇게 프로토콜로 만들어 준다면
// 사용할 때는 프로토콜로 타입을 명시하고 init에서 주입하여 사용하면 더 유연할듯함니다 !
protocol NetworkClient {
    func dataTask<T: Codable>(
        _ api: APIRequest,
        onCompletion: @escaping (_ result: Result<T, Error>) 
        -> Void)
}

final class AlamofireApiClient: NetworkClient {
    private let session = AF.session

    private func prepareURL(_ api: APIRequest) -> URL? {
        ...
    }
    
    func dataTask<T>(_ api: APIRequest, onCompletion: @escaping (Result<T, Error>) -> Void) where T : Decodable, T : Encodable {
        ...
        // 알아서 유효성 검사를 해주는거 같네용
        AF.request(urlRequest).validate().response { response in
            // dataTask와 동일 !
            ...
        }
    }
}

요 글을 작성하신 분은 일반적으로 GET POST만 사용할 경우에는 URLSession을 선호하지만, caching, logging, metric analysis, upload & download 가 필요한 경우에는 Alamofire를 사용할거 같다고 하시네요 ! 공부해야게씀다..

profile
hi there 👋

0개의 댓글