티스토리 뷰
API 호출 모듈화를 통해 코드의 재사용성을 높이고 유지보수를 용이하게 할 수 있습니다.
기존에는 이런식으로 API를 작성하여 호출해주었습니다
//
// RequsetAPI.swift
// 부산 사회 복지 정보를 제공하는 어플
// https://apps.apple.com/kr/app/id1588773594
// Created by jh on 2021/09/17.
//
import Foundation
import Alamofire
struct fetchAPI {
private init() { }
static let shared = fetchAPI()
func getData(numOfRows: Int, PageNo: Int, completion: @escaping (_ data: [Item]) -> Void) {
guard let encodingKey = Bundle.main.object(forInfoDictionaryKey: "API_KEY") as? String else { return }
let url = "https://apis.data.go.kr/6260000/SocialWelfareCenterProgramsService/getProgramInfoList?ServiceKey=\(encodingKey)&numOfRows=\(numOfRows)&pageNo=\(PageNo)&resultType=json"
AF.request(url, method: .get).responseJSON { response in
switch response.result {
case .success(_):
guard let data = response.data else { return }
print(data)
do {
let decoder = JSONDecoder()
let welfareData = try decoder.decode(DataModel.self, from: data)
completion(welfareData.response.body.items.item)
} catch { print("error \(error)") }
case .failure(let error):
print("errorCode: \(error._code)")
print("errorDescription: \(error.errorDescription!)")
}
}.resume()
}
}
만약 API 호출이 N개가 될 경우 매번 아래 request 호출을 작성해주어야 합니다.
모듈화하는 이유로는
url로 network 통신하는 코드가 동일하므로 하나의 모듈화된 코드로 재사용해주기 위함입니다.
모듈화 고려사항으로
1. completion 부분의 data: [Item] 형태를 Generic 형태 T로 하면 효용성을 높일 수 있습니다.
2. fetchData 말고도 postData, deleteData 등 다양한 경우를 고려할 수 있습니다
먼저
API의 역할에 따른 구조를 먼저 이렇게 나눌 수 있습니다
1. Endpoints: API 엔드포인트를 관리하는 구조체
2. APIClient: API 호출을 처리하는 기본 클래스 또는 구조체
3. APIService: 특정 API를 호출하기 위한 메서드가 정의된 프로토콜 또는 클래스
4. Models: API 응답 데이터를 매핑하기 위한 모델들
각 구조로 모듈화하여 코드를 작성해보겠습니다
먼저 Endpoint 입니다.
//
// EndPoint.swift
// URLSessionTest_240813
//
// Created by jh on 8/14/24.
//
import Foundation
struct Endpoint {
private var basePath: String
var path: String {
return basePath.hasPrefix("/") ? basePath : "/" + basePath
}
var method: HTTPMethod
var headers: [String: String]
var queryItems: [URLQueryItem]
var url: URL? {
var components = URLComponents()
components.scheme = "https"
components.host = "apis.data.go.kr"
components.path = path
components.queryItems = queryItems.isEmpty ? nil : queryItems
components.percentEncodedQuery = components.percentEncodedQuery?
.replacingOccurrences(of: "+", with: "%2B")
return components.url
}
init(path: String, method: HTTPMethod, headers: [String: String] = [:], queryItems: [URLQueryItem] = []) {
self.basePath = path
self.method = method
self.headers = headers
self.queryItems = queryItems
}
}
enum HTTPMethod: String {
case get = "GET"
case post = "POST"
// 다른 HTTP 메서드 추가
}
enum NetworkError: Error {
case invalidURL
case requestFailed(description: String)
case noData
case decodingFailed
case httpResponseNotOK(statusCode: Int)
case unknownError
}
extension NetworkError: LocalizedError {
public var errorDescription: String? {
switch self {
case .invalidURL:
return "The URL provided was invalid."
case .requestFailed(let description):
return "The network request failed: \(description)"
case .noData:
return "No data was returned by the server."
case .decodingFailed:
return "Failed to decode the data from the server."
case .httpResponseNotOK(let statusCode):
return "HTTP request returned an unsuccessful status code: \(statusCode)"
case .unknownError:
return "An unknown error occurred."
}
}
}
EndPoint는
path, method, header, query(numOfRows = 5, pageNo = 1) 를 받아서 URLRequest로 만들어주는 역할
let url = "https://apis.data.go.kr/6260000/SocialWelfareCenterProgramsService/getProgramInfoList?ServiceKey=\(encodingKey)&numOfRows=\(numOfRows)&pageNo=\(PageNo)&resultType=json"
path는 위 url에서 /SocialWelfareCenterProgramsService/getProgramInfoList 에 해당합니다
(path 맨앞에 "/"를 안 붙일 경우 url 에러나서 basePath로 rapping해줬음)
그리고 API KEY를 query item component 로 만들때, encode가 제대로 되지 않습니다
그래서 + 부분을 %2B 로 교체해주는 코드
components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
를 작성하여 query encode를 작업해줍니다.
출처 https://stackoverflow.com/questions/43052657/encode-using-urlcomponents-in-swift
그 다음
네트워크 요청을 처리하고 반환하는 APIClient 입니다.
import Foundation
class APIClient {
static let shared = APIClient()
private init() {}
func request<T: Decodable>(_ endpoint: Endpoint) async throws -> T {
guard let url = endpoint.url else {
throw NetworkError.invalidURL
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = endpoint.method.rawValue
urlRequest.allHTTPHeaderFields = endpoint.headers
do {
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw NetworkError.httpResponseNotOK(statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0)
}
do {
let decodedData = try JSONDecoder().decode(T.self, from: data)
return decodedData
} catch {
throw NetworkError.decodingFailed
}
} catch {
throw NetworkError.requestFailed(description: error.localizedDescription)
}
}
func postRequest<T: Decodable, U: Encodable>(to endpoint: Endpoint, body: U) async throws -> T {
guard let url = endpoint.url else {
throw NetworkError.invalidURL
}
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = endpoint.method.rawValue
urlRequest.allHTTPHeaderFields = endpoint.headers
urlRequest.httpBody = try JSONEncoder().encode(body)
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
do {
let (data, response) = try await URLSession.shared.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw NetworkError.httpResponseNotOK(statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0)
}
do {
let decodedData = try JSONDecoder().decode(T.self, from: data)
return decodedData
} catch {
throw NetworkError.decodingFailed
}
} catch {
throw NetworkError.requestFailed(description: error.localizedDescription)
}
}
}
1. DataModel 부분을 Generic 형태로 바꿔주었습니다
2. 그리고 endPoint를 URLRequest에 넣고
let (data, response) = try await URLSession.shared.data(for: urlRequest)를 수행합니다
3. iOS15 부터 지원하는 swift concurrency 문법 async await를 사용하여, escaping closure를 refactoring 해주었습니다
4. 그리고 각 error를 Endpoint 구조체 아래에 작성해주었던 네트워크 에러 Enum 타입으로 반환해주었습니다.
그리고 API 명세 프로토콜을 작성해 줍니다
protocol APIService {
func fetchData<T: Decodable>(from endpoint: Endpoint) async throws -> T
func postData<T: Decodable, U: Encodable>(to endpoint: Endpoint, body: U) async throws -> T
// func deleteData(from endpoint: Endpoint) async throws
}
그리고 ViewModel 에서 API를 호출할때 APIService로 의존성을 주입을 해주기 위해 NetworkSerivce 를 작성해줍니다.
NetworkService에서는 해당 APIService의 fetchData를 정의해주고,
그리고 fetchData에서 APIClient의 싱글톤 request 메소드를 실행하는 함수입니다.
import Foundation
class NetworkService: APIService {
func fetchData<T>(from endpoint: Endpoint) async throws -> T where T : Decodable {
try await APIClient.shared.request(endpoint)
}
func postData<T: Decodable, U: Encodable>(to endpoint: Endpoint, body: U) async throws -> T {
try await APIClient.shared.postRequest(to: endpoint, body: body)
}
}
ViewModel 내부에서는 numOfRows와 pageNo 같은 쿼리문을 인자로 받아 endpoint로 작성해주며,
APIService의 fetchData를 호출해줍니다.
//
// ViewModel.swift
// URLSessionTest_240813
//
// Created by jh on 8/14/24.
//
import Foundation
class NetworkViewModel {
private var apiService: APIService
var data: DataModel?
init(apiService: APIService) {
self.apiService = apiService
}
func loadData(numOfRows: Int, pageNo: Int) async {
guard let decodedKey = Bundle.main.object(forInfoDictionaryKey: "API_KEY") as? String else {
print("API Key is not found.")
return
}
let queryItems = [
URLQueryItem(name: "ServiceKey", value: decodedKey),
URLQueryItem(name: "numOfRows", value: "\(numOfRows)"),
URLQueryItem(name: "pageNo", value: "\(pageNo)"),
URLQueryItem(name: "resultType", value: "json")
]
let endpoint = Endpoint(path: "6260000/SocialWelfareCenterProgramsService/getProgramInfoList", method: .get, queryItems: queryItems)
do {
let data: DataModel = try await apiService.fetchData(from: endpoint)
DispatchQueue.main.async {
self.data = data
print("Data loaded successfully: \(data)")
}
} catch {
DispatchQueue.main.async {
print("Failed to load data: \(error)")
}
}
}
}
그리고 DI 형태로 APIService를 init의 파라미터로 받아서
APIService의 fetchData를 호출해주고있습니다.
이렇게 작성해주면 테스트가 용이해지는데,
이런식으로 MockService 를 생성하여 테스트 할 수 있습니다.
class MockService: APIService {
func fetchData<T>(from endpoint: Endpoint) async throws -> T where T: Decodable {
// 여기서 테스트에 사용할 가짜 데이터를 반환
let mockData = ... // T 타입의 가짜 데이터 생성
return mockData
}
}
// 테스트 시:
let mockService = MockService()
let viewModel = NetworkViewModel(apiService: mockService)
await viewModel.loadData(numOfRows: 5, pageNo: 1)
APIKey는 내부 번들키로 빼두어 두었습니다.
빼두는 방법은
Configuration Settings File 작성해주고, 내부에 API KEY를 작성해줍니다
그리고 Project info의 debug release Configurations에 위에서 작성한 APIKey 파일을 추가합니다.
그 다음 Targets Info Plist에
키값 API_KEY 에 $(API_KEY)를 추가해주면 됩니다.
(위 부분이 git에 올라가지 않도록 해주려면 gitignore에 추가해주면 됩니다
https://ggasoon2.tistory.com/18 )
추가로 url을 호출하여 networking할때 보안관련하여 ATS를 작성해주어야합니다.
App Transport Security Settings 를 생성해주고 하위값으로 YES를 넣어주면 됩니다.
마지막으로
위에서 작성한 ViewModel을 ViewController에서 호출하게 되면
이렇게 호출한 API response data가 잘 나오는 모습입니다.
전체 코드입니다
https://github.com/JangJaeHyung1/NetworkModule
- Total
- Today
- Yesterday
- swift network module
- 엔디소프트 레이세이
- swift urlsession module
- swift urlsession refactoring
- llm pdf rag
- swift excel read
- swift 자간
- readysay
- swift urlcomponent encode
- 레디세이 어플
- rag 기반 llm 챗봇
- 레디세이
- swift urlsession network module
- swift get excel
- swift 엑셀 읽기
- swift network refactoring
- focus timer 어플
- rag llm pdf
- rag 기반 llm
- llm csv
- swift queryitem encode
- filemanager excel read
- swift filemanager excel
- swift 네트워크 모듈화
- swift urlsession 공통화
- swift filemanager get excel
- chatgpt rag llm
- 공부 타이머 어플
- swift network 공통화
- swift 엑셀 가져오기
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |