-
[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/반응형'앱 개발자' 카테고리의 다른 글
Quick Recap: Simplify Your YouTube Viewing with Instant Summaries (5) 2024.09.10 iOS 앱 출시 스크린샷 준비, 피그마&캔바 활용 (0) 2024.06.03 Swift JSON 디코딩하는데 타입이 여러 데이터 타입으로 들어온다면? (0) 2023.09.05 [Swift5] UISlider tap to change value: Tap on UISlider to Set the Value (0) 2023.08.22 IOS 앱 <NEW ME> 기획부터 출시까지 (0) 2022.07.22