[Swift] Network_REST API 연동

건희·2024년 10월 31일
0

프로젝트 진행 중 URLSession을 이용한 REST API 연동 작업을 수행하게 되었다.

전체적인 네트워크 통신 구현은 다음과 같이 진행된다.

  1. API 요청 url 구성을 위한 EndPoint 프로토콜 구현
  2. EndPoint를 채택하는 AppEndPoint 구현
  3. EndPoint를 통해 서버로부터 수신한 결과를 반환하는 HTTPClient 프로토콜 구현
  4. Service 로직 구현

1. API 요청 url 구성을 위한 EndPoint 프로토콜 구현

protocol Endpoint {
    var scheme: String { get }
    var host: String  { get }
    var path: String { get }
    var method: RequestMethod { get }
    var query: [String: String]? { get }
    var header: [String: String]? { get }
    var body: [String: String]? { get }
}

extension Endpoint {
    var scheme: String {
        return "http"
    }
}

enum RequestMethod: String {
    case get = "GET"
    case post = "POST"
}

enum RequestError: Error {
    case decode
    case invalidURL
    case noResponse
    case unauthorized
    case unexpectedStatusCode
    case unknown

    var message: String {
        switch self {
        case .decode:
            return "Decode Error"
        case .invalidURL:
            return "Session expired"
        case .noResponse:
            return "noResponse"
        case .unauthorized:
            return "unauthorized"
        case .unexpectedStatusCode:
            return "unexpected"
        case .unknown:
            return "unknown"
        }
    }
}

2. EndPoint를 채택하는 AppEndPoint 구현

enum AppEndPoint {
    case kakaoLogin(code: String)
    case appleLogin(identityToken: String)
    case test
}

extension AppEndPoint: Endpoint {
    var host: String {
        switch self {
        case .kakaoLogin, .appleLogin, .test:
            return "app.shop"
        }
    }
    
    var path: String {
        switch self {
        case .kakaoLogin:
            return "/auth/kakao"
        case .appleLogin:
            return "/auth/apple/"
        case .test:
            return "/health/"
        }
    }
    
    var query: [String : String]? {
        switch self {
        case .kakaoLogin(let code):
            return ["code": code]
        default:
            return nil
        }
    }
    
    var method: RequestMethod {
        switch self {
            
        case .appleLogin:
            return .post
            
        case .kakaoLogin, .test:
            return .get
        }
    }
    
    var header: [String : String]? {
        switch self {
        case .kakaoLogin, .appleLogin, .test:
            return [
                "accept": "application/json",
                "Content-Type": "application/json"
            ]
        }
    }
    
    var body: [String : String]? {
        switch self {
        case .appleLogin(let identityToken):
            return [
                "identityToken": identityToken,
            ]
            
        default:
            return nil
        }
    }
}

3. EndPoint를 통해 서버로부터 수신한 결과를 반환하는 HTTPClient 프로토콜 구현

import Foundation

protocol HTTPClient {
    func request<T: Decodable>(endpoint: Endpoint, responseModel: T.Type) async -> Result<T, RequestError>
}

extension HTTPClient {
    func request<T:Decodable>(
        endpoint: Endpoint,
        responseModel: T.Type
    ) async -> Result<T, RequestError> {
        var urlComponents = URLComponents()
        urlComponents.scheme = endpoint.scheme
        urlComponents.host = endpoint.host
        urlComponents.path = endpoint.path
        if let query = endpoint.query {
            urlComponents.queryItems = dictToQueryItems(dict: query)
        }
        
        guard let url = urlComponents.url else {
            return .failure(.invalidURL)
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = endpoint.method.rawValue
        request.allHTTPHeaderFields = endpoint.header
        
        if let body = endpoint.body {
            request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
        }
        
        do {
            let (data, response) = try await URLSession.shared.data(for: request, delegate: nil)
            guard let response = response as? HTTPURLResponse else {
                return .failure(.decode)
            }
            
            switch response.statusCode {
            case 200...299:
                guard let decodeResponse = try? JSONDecoder().decode(responseModel, from: data) else {
                    return .failure(.decode)
                }
                return .success(decodeResponse)
            case 401:
                return .failure(.unauthorized)
            default:
                return .failure(.unexpectedStatusCode)
            }
        } catch(let error) {
            print(error)
            return .failure(.unknown)
        }
    }
    
    func dictToQueryItems(dict: [String: String]?) -> [URLQueryItem]? {
        var queryItems: [URLQueryItem] = []
        for (key, value) in dict ?? [:] {
            queryItems.append(URLQueryItem(name: key, value: value))
        }
        return queryItems
    }
}

4. Service 로직 구현

import Foundation

protocol AppServicable {
    func postAppleLogin(identityToken: String) async -> Result<AppleLoginResModel, RequestError>
    func test() async -> Result<String, RequestError>
}

class AppService: AppServicable, HTTPClient {
    
    static let shared = AppService()
    private init() {}
    
    func postAppleLogin(identityToken: String) async -> Result<AppleLoginResModel, RequestError> {
        return await request(endpoint: AppEndPoint.appleLogin(identityToken: identityToken), responseModel: AppleLoginResModel.self)
    }
    
    func getKakaoUserToken(code: String) async -> Result<KakaoLoginResModel, RequestError> {
        return await request(endpoint: AppEndPoint.kakaoLogin(code: code), responseModel: KakaoLoginResModel.self)
    }
    
    func test() async -> Result<String, RequestError> {
        return await request(endpoint: AppEndPoint.test, responseModel: String.self)
    }
}
profile
💻 🍎

0개의 댓글