Swift - API 설계 프로젝트 적용

SDTCOW·2025년 4월 3일

Swift

목록 보기
4/7

안녕하세요.

이번에는 이전에 정리했던 Swift의 Generic, associatedtype, typealias를 활용한 API 요청 구조를 직접 토이 프로젝트에 적용해본 경험을 정리해보려고 합니다.

드디어 테스트용 코드만 주구장창 써보다가, 실제로 네트워크 요청을 날려봤습니다.

왜 이걸 해봤냐면요.

이전 글들에서 여러 가지 API 구조를 설계해봤는데, 거기서 얻은 결론이 “타입 안전성 + 확장성 + 코드 재사용성 = Swift스럽다”는 거였죠. 근데 그때는 대부분 콘솔 출력 수준의 테스트만 했고, 실질적인 네트워크 호출은 없었단 말이죠?

그래서 “그래, 한 번 진짜 API호출도 해보자”는 마음으로 음식 레시피 오픈 API를 이용해서 실제 요청을 보내는 구조로 프로젝트를 구성해봤습니다.

구조 설명

1. 요청 정보를 담은 RecipeRequest 구조체

이번엔 진짜 데이터를 받아오기 위해, 아래처럼 실제 요청 정보들을 담은 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 파일에서 관리하도록 설정했습니다.

2. 응답 데이터를 받는 Recipe, RecipeItem 구조체

// RecipeContainer의 구조는 생략... 너무 길어요..
struct Recipe: Decodable {
    let recipe: RecipeContainer
    
    enum CodingKeys: String, CodingKey {
        case recipe = "COOKRCP01"
    }
}

응답이 꽤 깊은 JSON 구조를 갖고 있어서 CodingKeys 열심히 써서 파싱했습니다.

3. 요청을 처리하는 NetworkManager

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에 할당했습니다.

해보고 느낀 점

  1. "편하다"
  • 솔직히 이건 내가 직접 구조를 만들고, 그 구조를 쓰면서 생긴 착각일 수도 있긴 한데요, 진짜로 편했어요.
    어떤 요청이든 struct 하나 만들어서 필요한 정보만 넣고 request를 던지면, 그에 맞는 타입의 response가 깔끔하게 오는 구조거든요.
    물론, 이게 가능한 이유는 미리 ResponseData를 지정해뒀기 때문이고, 결국 명세서에 어떤 변수가 들어가고, 어떤 데이터 타입이 리턴되는지는 상세히 적어놓아야 겠죠?
  1. ResponseData를 명확하게 지정하니 타입 캐스팅이 필요 없어짐
  • 위와 비슷한 느낌인데요, 이전 글에서는 Any로 넘겼다가 계속 if let 파티였는데, 이번엔 타입 안전성 챙기면서 깔끔하게 처리 가능 했습니다.

마무리

이번엔 “설계만 열심히 해놓고 안 써보는 사람”에서 “직접 네트워크 요청도 해보는 사람”으로 진화해봤습니다. 직접 해보니 구조에 대한 이해도도 더 깊어지고, 놓쳤던 문제들도 보이더라고요.

진짜 중요한 건 뭔가를 “완벽하게” 만들려는 게 아니라 “일단 써보는” 거라는 걸 다시 느꼈어요. 덕분에 associatedtype, typealias, Generic의 실전 감각도 한층 올라간 느낌입니다.

다음엔 Combine까지 활용해서 검색 -> 검색 결과로 UI 업데이트를 진행해보겠습니다!

profile
iOS 개발자가 되고싶은 사람

0개의 댓글