ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Swift]여러 서버와 통신하기, URLSession 네트워킹 코드, 제네릭 함수로 만들어 재사용하기
    앱 개발자 2023. 9. 11. 11:08
    반응형

    하나의 서버랑 통신한다면, 네트워킹 코드에 상수를 박아넣고 딱 그 용도로만 쓰면 됩니다. 하지만? 우리가 실무에서든 사이드프로젝트에서든 딱 하나의 서버랑만 통신하지는 않을 거예요.

    이 글에서 여러 서버와 통신하는 방법을 알아봅니다. 라이브러리는 쓰지 않았어요. 제네릭 함수로 만들어 여러 서버와 통신할 때 재사용하기 좋은 코드를 만들어 봤어요.

    여러 서버와 통신한다는 말은:
    URL, URLRequest, Request, Response 등등이 다르다는 말이에요. 그런데 이것들이 다르다고 서버마다 URLSession 코드를 복붙하면? 진짜 진짜 마음이 안좋습니다.


    먼저 기존에 하나의 서버랑만 통신하는 코드를 볼게요.

    기존 코드 :

    private func fetchData(url: String, completion: @escaping NetworkCompletion) {
            let endpoint = url
            let encodedString = endpoint.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
            let timestamp = String(Int(Date().timeIntervalSince1970 * 1000))
            let signature = Signatures.generateSignature(timestamp: timestamp, secret: NaverAPI.Credentials.apiSecret, method: "GET", uri: "/keywordstool")
            
            // 1. URL구조체 만들기
            guard let url = URL(string: encodedString) else {
                print("Error: cannot create URL")
                completion(.failure(.urlError))
                return
            }
            
            // 2. URL요청 생성
            var request = URLRequest(url: url)
            request.httpMethod = "GET"
            request.addValue(timestamp, forHTTPHeaderField: "X-Timestamp")
            request.addValue(NaverAPI.Credentials.apiKey, forHTTPHeaderField: "X-API-KEY")
            request.addValue(NaverAPI.Credentials.customerId, forHTTPHeaderField: "X-Customer")
            request.addValue(signature, forHTTPHeaderField: "X-Signature")
    
            // 3. 요청을 가지고 작업세션시작
            URLSession.shared.dataTask(with: request) { (data, response, error) in
                // 에러가 없어야 넘어감
                guard error == nil else {
                    print("Error: error calling GET")
                    print(error!)
                    completion(.failure(.networkingError))
                    return
                }
                
                // HTTP 200번대 정상코드인 경우만 다음 코드로 넘어감
                guard let response = response as? HTTPURLResponse, (200 ..< 299) ~= response.statusCode else {
                    print("Error: HTTP request failed")
                    completion(.failure(.networkingError))
                    return
                }
                
                guard let safeData = data else {
                    completion(.failure(.dataError))
                    return
                }
                
                
                // 메서드 실행해서, 결과를 받음
                if let result = self.parseJSON(safeData, responseType: RelKwdStatResponse.self) {
                    print("Parse 실행")
                    completion(.success(result))
                } else {
                    print("Parse 실패")
                    completion(.failure(.parseError))
                }
                    
                    
            }.resume() //작업 시작
            
            
        }

    이 코드를 보고 어떤 부분을 공통으로 쓰이도록 만들 수 있을까 고민해봅니다.

    • URL은 지금처럼 input parameter로 받아야죠
    • URLRequest는 서버마다 다르게 보낼 부분입니다. 이것도 input에 추가해야겠네요.
    • 그 아래로 있는 URLSession 작업 부분은 공통적으로 쓰일 부분입니다.
    • 가장 중요한 응답값 처리! 서버마다 응답하는 JSON이 다릅니다. parseJSON도 타입별로 변환되도록 제네릭으로 수정되어 있죠.
    • 반환타입은 함수를 호출하는 쪽에서 정하는 게 맞아요. 그러니 input에 추가!
    • 네트워크 통신이 완료된 이후 작업을 처리하는 completion 클로저도 input에 그대로 챙겨가기로 해요.


    제네릭 함수로 바꾼 코드 :

    private func fetchData<T: Decodable>(endpoint: String,
                                    headers: [String: String],
                                    responseType: T.Type,
                                    completion: @escaping NetworkCompletion) {
            guard let url = URL(string: endpoint) else {
                print("Error: cannot create URL")
                completion(.failure(.urlError))
                return
            }
    
            var request = URLRequest(url: url)
            request.httpMethod = "GET"
            
            for (key, value) in headers {
                request.setValue(value, forHTTPHeaderField: key)
            }
    
            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: request) { (data, response, error) in
                guard error == nil else {
                    print("Error: error calling GET")
                    print(error!)
                    completion(.failure(.networkingError))
                    return
                }
                
                guard let response = response as? HTTPURLResponse, (200 ..< 299) ~= response.statusCode else {
                    print("Error: HTTP request failed")
                    completion(.failure(.networkingError))
                    return
                }
                
                guard let safeData = data else {
                    completion(.failure(.dataError))
                    return
                }
                
                if let result = self.parseJSON(safeData, responseType: responseType) {
                    print("Parse 실행")
                    completion(.success(result))
                } else {
                    print("Parse 실패")
                    completion(.failure(.parseError))
                }
            }
            task.resume()
        }
        

    제네릭 함수로 바꿨습니다! 기존 코드와 동일하게 작동합니다.

    • 제네릭 함수는 함수이름 오른쪽에 꺽쇠괄호를 열고 제네릭 이름표를 선언해줘요.
    • 아무 타입이나 되는게 아니고, Decodable 프로토콜을 채택한 타입만 이 제네릭함수에서 쓰일 수 있습니다. 그래서 <T: Decodable>
    • URLRequest에 답을 Header를 파라미터로 받아요.
    • 함수이름 옆에 T를 선언해놨으니 responseType에 T.Type을 쓸 수 있습니다.
    • 이제 여러 서버랑 통신할 수 있는 코드가 됐어요.


    그럼 어떻게 호출하는지 볼까요?

    서버 1과 통신하기

    블로그 데이터를 조회하는 API랑 통신해요.

    func fetchBlogCafe(query: String, completion: @escaping BlogCafeCompletion) {
            let headers = [
                "X-Naver-Client-Id": NaverAPI.Credentials.clientId,
                "X-Naver-Client-Secret": NaverAPI.Credentials.clientSecret
            ]
            
            let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
            
            //blog 데이터 조회
            fetchData(
                endpoint: "\(NaverAPI.openapiURL)\(NaverAPI.blogPath)query=\(encodedQuery)",
                headers: headers,
                responseType: BlogResponse.self
            ) { result in
                switch result {
                case .success(let response):
                    print("블로그 데이터 받았다!")
                    self.blogResponse = response as? BlogResponse
                    completion(response)
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
      }


    서버 2와 통신하기

    키워드 검색량을 조회하는 API랑 통신해요.

    func fetchRelKwdStat(hintKeywords: String, completion: @escaping NetworkCompletion) {
            let endpoint = "\(NaverAPI.keywordsToolURL)hintKeywords=\(hintKeywords)&showDetail=1"
            let encodedString = endpoint.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
            let timestamp = String(Int(Date().timeIntervalSince1970 * 1000))
            let signature = Signatures.generateSignature(timestamp: timestamp, secret: NaverAPI.Credentials.apiSecret, method: "GET", uri: "/keywordstool")
            let kwdHeaders = [
                "X-Timestamp": timestamp,
                "X-API-KEY": NaverAPI.Credentials.apiKey,
                "X-Customer": NaverAPI.Credentials.customerId,
                "X-Signature": signature
            ]
            
            fetchData(
                endpoint: encodedString,
                headers: kwdHeaders,
                responseType: RelKwdStatResponse.self
            ) { result in
                completion(result)
            }
    }
        


    제네릭 함수 처음 만들어봐서 신기합니다. 굉장히 유용하네요. parseJSON도 제네릭으로 바꿨어요. 앞으로 제네릭 애용할 것 같습니다.


    네트워킹 라이브러리 적극 활용하자

    네트워킹 라이브러리는 이렇게 개발자가 직접 구현할 필요 없이, 편하게 네트워크 관련 로직을 이용하도록 만들어졌어요. 대표적으로 Alamofire, Moya가 있습니다.

    저도 기본 URLSession을 써봤으니 곧 라이브러리도 장착해보려 해요.

    마지막으로 iOS 네트워킹 관련해서 배달의민족 우아한 기술 블로그의 이 글을 정독해봅시다!

    https://techblog.woowahan.com/2704/

    iOS Networking and Testing | 우아한형제들 기술블로그

    {{item.name}} Why Networking? Networking은 요즘 앱에서 거의 필수적인 요소입니다. 설치되어 있는 앱들 중에 네트워킹을 사용하지 않는 앱은 거의 없을 겁니다. API 추가가 쉽고 변경이 용이한 네트워킹

    techblog.woowahan.com

    반응형
You can do everything.