관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 1. Alamofire 사용 방법 - Network Layer 구현 (Moya 프레임워크처럼 사용하는 방법) 본문

iOS 응용 (swift)

[iOS - swift] 1. Alamofire 사용 방법 - Network Layer 구현 (Moya 프레임워크처럼 사용하는 방법)

jake-kim 2021. 10. 16. 02:20

1. Alamofire 사용 방법 - Network Layer 구현 (Moya 프레임워크처럼 사용하는 방법)

2. Alamofire 사용 방법 - 토큰 갱신 방법1 (Interceptor, adapt, retry)

3. Alamofire 사용 방법 - 토큰 갱신 방법2 (AuthenticationCredential, Authenticator, AuthenticationInterceptor)

4. Alamofire 사용 방법 - 로그 Log (EventMonitor)


Alamofire를 이용한 Network Layer

Network/Bases

  • TargetType은 API들의 공통 Endpoint를 가지고 있는 모듈
  • Alamofire에 내장되어 있는 protocol인 URLRequestConvertible의 asURLRequest를 구현하여 AF.request할 때 URLRequest객체 커스텀
import Alamofire

protocol TargetType: URLRequestConvertible {
    var baseURL: String { get }
    var method: HTTPMethod { get }
    var path: String { get }
    var parameters: RequestParams { get }
}

extension TargetType {

    // URLRequestConvertible 구현
    func asURLRequest() throws -> URLRequest {
        let url = try baseURL.asURL()
        var urlRequest = try URLRequest(url: url.appendingPathComponent(path), method: method)
        urlRequest.setValue(ContentType.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)

        switch parameters {
        case .query(let request):
            let params = request?.toDictionary() ?? [:]
            let queryParams = params.map { URLQueryItem(name: $0.key, value: "\($0.value)") }
            var components = URLComponents(string: url.appendingPathComponent(path).absoluteString)
            components?.queryItems = queryParams
            urlRequest.url = components?.url
        case .body(let request):
            let params = request?.toDictionary() ?? [:]
            urlRequest.httpBody = try JSONSerialization.data(withJSONObject: params, options: [])
        }

        return urlRequest
    }
}

enum RequestParams {
    case query(_ parameter: Encodable?)
    case body(_ parameter: Encodable?)
}

extension Encodable {
    func toDictionary() -> [String: Any] {
        guard let data = try? JSONEncoder().encode(self),
              let jsonData = try? JSONSerialization.jsonObject(with: data),
              let dictionaryData = jsonData as? [String: Any] else { return [:] }
        return dictionaryData
    }
}
  • RequestParams에서 알 수 있듯이, 모든 parameter들은 Encodable을 준수하는 struct를 받도록 설계했으므로, Encodable을 extension으로 toDictionary() 메소드도 정의

Domain, Request, Response 모델

  • Domain 모델: Network에서 사용되는 모델이 아닌, UseCase에서 사용되는 핵심 모델

Domain 모델

struct Login {
    let name: String
}
  • Request, Response 모델: API를 통해 호출하고 응답받을때 받을 모델

Request, Rersponse 모델

  • Request는 Encodable을 준수, Response는 Decodable을 준수
struct LoginRequest: Encodable {
    let userName: String
    let password: String
}
  • Response에서는 extension으로 toDomain 정의: Domain으로 변경하여 사용하는 쪽에서는 Domain모델을 받을 수 있도록 추후 completion에서 전달
struct LoginResponse: Decodable {
    let name: String
    let accessToken: String
    let refreshToken: String
}

extension LoginResponse {
    var toDomain: Login {
        return Login(name: name)
    }
}

API, Target 모듈

  • LoginTarget: Login도메인에서 사용되는 Endpoint 정의
enum LoginTarget {
    case login(LoginRequest)
    case getUserDetails(UserDetailsRequest)
}

extension LoginTarget: TargetType {

    var baseURL: String {
        return "https://www.apiserver.com"
    }

    var method: HTTPMethod {
        switch self {
        case .login: return .post
        case .getUserDetails: return .get
        }
    }

    var path: String {
        switch self {
        case .login: return "/login"
        case .getUserDetails: return "/details"
        }
    }

    var parameters: RequestParams {
        switch self {
        case .login(let request): return .body(request)
        case .getUserDetails(let request): return .body(request)
        }
    }

}
  • LoginAPI: 외부에서 해당 모듈을 통해 접근
    • 핵심: 사용하는쪽에서 받는 모델은 Domain모델인 Login을 사용할수 있게 completion에는 Login 모델을 명시하고,
               API를 호출하는데 필요한 request, response는 도메인 모델이 아닌것으로 명시 (LoginRequest, LoginResponse)
import Alamofire

struct LoginAPI {

    /// 이름과 패스워드로 로그인
    static func login(request: LoginRequest, completion: @escaping (_ succeed: Login?, _ failed: Error?) -> Void) {
        AF.request(LoginTarget.login(request))
            .responseDecodable { (response: AFDataResponse<LoginResponse>) in
                switch response.result {
                case .success(let response):
                    completion(response.toDomain, nil)
                case .failure(let error):
                    completion(nil, error)
                }
            }
    }

    /// 유저 정보 조회
    static func getUserDetails(request: UserDetailsRequest, completion: @escaping (_ succeed: Login?, _ failed: Error?) -> Void) {
        AF.request(LoginTarget.getUserDetails(request))
            .responseDecodable { (response: AFDataResponse<UserDetailsResponse>) in
                switch response.result {
                case .success(let response):
                    completion(response.toDomain, nil)
                case .failure(let error):
                    completion(nil, error)
                }
            }
    }
}

사용하는 쪽

  • API호출은 static이므로 편리하게 접근 가능
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let request = LoginRequest(userName: "name", password: "1234")
        LoginAPI.login(request: request) { succeed, failed in
            if let succeed = succeed {
                print(succeed)
            }
        }
    }

}

* 전체 소스코드: https://github.com/JK0369/ExAlamofire

Comments