안녕하세요.
이번에는 이전에 정리했던 Swift의 Generic, associatedtype, typealias를 활용한 API 요청 구조를 직접 토이 프로젝트에 적용해본 경험을 정리해보려고 합니다.
드디어 테스트용 코드만 주구장창 써보다가, 실제로 네트워크 요청을 날려봤습니다.
왜 이걸 해봤냐면요.
이전 글들에서 여러 가지 API 구조를 설계해봤는데, 거기서 얻은 결론이 “타입 안전성 + 확장성 + 코드 재사용성 = Swift스럽다”는 거였죠. 근데 그때는 대부분 콘솔 출력 수준의 테스트만 했고, 실질적인 네트워크 호출은 없었단 말이죠?
그래서 “그래, 한 번 진짜 API호출도 해보자”는 마음으로 음식 레시피 오픈 API를 이용해서 실제 요청을 보내는 구조로 프로젝트를 구성해봤습니다.
이번엔 진짜 데이터를 받아오기 위해, 아래처럼 실제 요청 정보들을 담은 struct를 만들었습니다.
struct RecipeRequest: APIRequestProtocol {
// 여기가 리턴 타입 설정하는곳!
typealias ResponseData = Recipe
let apiCase: APICase = .recipe
let apiKey: String?
let serviceKey: String = "COOKRCP01"
let dataType: String = "json"
let startIndex: Int
let endIndex: Int
let recipePart: String
init(startIndex: Int,
endIndex: Int,
recipePart: String) throws {
self.apiKey = apiCase.apiKey
guard apiKey != nil else {
throw APIError.APIKeyError
}
self.startIndex = startIndex
self.endIndex = endIndex
self.recipePart = recipePart
}
// MARK: 실제 요청 url
var urlStr: String {
return "\(apiCase.baseUrl)/\(apiKey!)/\(serviceKey)/\(dataType)/\(startIndex)/\(endIndex)/RCP_PARTS_DTLS=\(recipePart)"
}
}
이전 글에서 말했던 “요청마다 struct 만들어 넘기는 방식” 그대로 사용했고, API 키도 xcconfig 파일에서 관리하도록 설정했습니다.
// RecipeContainer의 구조는 생략... 너무 길어요..
struct Recipe: Decodable {
let recipe: RecipeContainer
enum CodingKeys: String, CodingKey {
case recipe = "COOKRCP01"
}
}
응답이 꽤 깊은 JSON 구조를 갖고 있어서 CodingKeys 열심히 써서 파싱했습니다.
final class NetworkManager {
func request<T: APIRequestProtocol>(_ request: T) async throws -> T.ResponseData {
guard let url = URL(string: request.urlStr) else { throw APIError.URLError }
let request = URLRequest(url: url)
let (data, res) = try await URLSession.shared.data(for: request)
guard let statusCode = res as? HTTPURLResponse, statusCode.statusCode == 200 else {
throw APIError.StatusCodeError
}
return try JSONDecoder().decode(T.ResponseData.self, from: data)
}
}
Generic을 사용해, 어떤 요청이든 처리할 수 있는 구조로 만들었습니다. 예전 글에서 만들었던 구조를 거의 그대로 가져왔고, 이번에는 URLSession을 실제로 사용해서 진짜 데이터 받아왔어요.
let (data, res) = try await URLSession.shared.data(for: request)
이렇게 Swift Concurrency를 통해 API 요청을 사용하도록 구성했습니다.
Task {
do {
let recipeData = try await networkManager.request(
RecipeRequest(startIndex: 1, endIndex: 5, recipePart: "돼지고기")
)
recipeItem = recipeData.recipe.recipeItems
} catch {
print(error.localizedDescription)
}
}
직접 RecipeRequest를 생성해서 넘겨주고, 응답 받은 데이터는 바로 파싱해서 recipeItem에 할당했습니다.
이번엔 “설계만 열심히 해놓고 안 써보는 사람”에서 “직접 네트워크 요청도 해보는 사람”으로 진화해봤습니다. 직접 해보니 구조에 대한 이해도도 더 깊어지고, 놓쳤던 문제들도 보이더라고요.
진짜 중요한 건 뭔가를 “완벽하게” 만들려는 게 아니라 “일단 써보는” 거라는 걸 다시 느꼈어요. 덕분에 associatedtype, typealias, Generic의 실전 감각도 한층 올라간 느낌입니다.
다음엔 Combine까지 활용해서 검색 -> 검색 결과로 UI 업데이트를 진행해보겠습니다!