2 분 소요

Api 적용하기

우선 json으로 만든 파일을 웹사이트에 올려 api처럼 가져오게 했다.

이후, Medium 글을 통해서 코드를 작성했다.

이글을 통해서 작성한것은 바로 Generic을 사용했다는 점이다. 이전에 Generic을 사용해본적이 없기에 이번에는 좀 사용하면서 내걸로 조금씩 만들고 싶었다.

ApiModel 만들기

struct ApiModel: Codable {
    let tours: [Tour]
}

enum NetworkError: Error {
    case badUrl
    case invalidRequest
    case badResponse
    case badStatus
    case failedToDecodeResponse
}

class ApiService {
    func downloadData<T: Codable>(fromURL: String) async -> T? {
        do {
            guard let url = URL(string: fromURL) else { throw NetworkError.badUrl }
            let (data, response) = try await URLSession.shared.data(from: url)
            guard let response = response as? HTTPURLResponse else { throw NetworkError.badResponse }
            guard response.statusCode >= 200 && response.statusCode < 300 else { throw NetworkError.badStatus }
            guard let decodedData = try? JSONDecoder().decode(ApiModel.self, from: data) else {
                throw NetworkError.failedToDecodeResponse
            }
            
            return decodedData.tours as? T
        } catch NetworkError.badUrl {
            print("There was an error creating the URL")
        } catch NetworkError.badResponse {
            print("Did not get a valid response")
        } catch NetworkError.badStatus {
            print("Did not get a 2xx status code from the response")
        } catch NetworkError.failedToDecodeResponse {
            print("Failed to decode response into the given type")
        } catch {
            print("An error occured downloading the data")
        }
        
        return nil
    }
}

여기서 하나 다른점이라면

guard let decodedData = try? JSONDecoder().decode(ApiModel.self, from: data) else {
                throw NetworkError.failedToDecodeResponse
            }

이 부분이다 참고글에서는 여기도 역시 T.self를 통해 Generic을 사용했지만, 내가만든 json의 구조에서는

"tours": [
        {
            "title": "해운대",
            "imageUrl": "https://www.visitbusan.net/uploadImgs/files/cntnts/20191229153531987_oen",
            "description": "해운대해수욕장은 대한민국 부산광역시 해운대구 중동과 우동에 걸쳐서 위치한 대한민국 최대규모의 해수욕장이다.\n모래사장의 총면적은 120,000m², 길이는 1.5 km,\n폭은 70m ~ 90m이다",
            "address": "부산광역시 해운대구 해운대해변로 280",
            "latitude": 35.1594965,
            "longitude": 129.162576,
            "resList": [

이런식으로 tours라는 녀석으로 시작해서 담고있기에 Generic을 사용하려면 애초에 json을 구성할때 tours를 뺐어야했다.

하지만 이미 업로드 하기도 해서 저기만 ApiModel.self를 사용했다.

ApiViewModel 만들기

@MainActor 항상 Main Queue에서 실행이 된다. 즉 Uikit에서 사용했던 DispatchQueue.main이라고 생각하면 된다.

import Foundation

@MainActor class ApiViewModel: ObservableObject {
    @Published var apiData = [Tour]()
    
    init() {
        Task {
            await fetchData()
        }
    }
    
    func fetchData() async {
        let url = "https://run.mocky.io/v3/42391865-6e96-4db3-9f68-1e2970796cad"
        guard let downloadedData: [Tour] = await ApiService().downloadData(fromURL: url) else { return }
        apiData = downloadedData
    }
}

특징 DispatchQueue.main @MainActor
메인 스레드에서의 실행 요청 명시적으로 DispatchQueue.main.async 사용 자동으로 메인 스레드에서 실행
적용 범위 특정 코드 블록 메서드, 프로퍼티, 클래스 전체
코드 가독성 복잡할 수 있음 가독성이 높아짐
비동기 메서드와의 호환성 추가로 async 블록 필요 async 메서드에서도 안전하게 실행

즉,

  • @MainActor를 사용하면, 명시적으로 DispatchQueue.main.async를 사용할 필요가 없다.
  • @MainActor는 메서드나 클래스 전체에서 메인 스레드 실행을 보장하기 때문에, 코드가 더 간결하고 안전해진다.
  • Swift의 최신 비동기 API(async/await)와 잘 어울리며, UI 업데이트 코드에서 더 많이 사용된다.

init을 사용한건, 여기서 내가 만든 json은 이미 내용이 정해져있고, 양이 방대하지도 않고 페이징이필요없기에 한번 로드하면 끝이라 init으로 한번 로드했을때 가져오게 했다.

그게아니라 View가 다시 로드될때마다 사용하고 싶다면

@ObservedObject var json = loadJsonModel()
@StateObject var vm = ApiViewModel()

NavigationStack {
            Text("관광 고고")
                List {
                    ForEach(vm.apiData, id: \.self) { tour in
                        NavigationLink(value: tour) {
                            CellView(title: tour.title, imageUrl: tour.imageURL)
                        }
                    }
                }
            .navigationDestination(for: Tour.self) { model in
                DetailView(title: model.title, imageUrl: model.imageURL, description: model.description, address: model.address, coordinate: CLLocationCoordinate2D(latitude: model.latitude, longitude: model.longitude), shopList: model.resList, cameraPosition: .camera(MapCamera(centerCoordinate: CLLocationCoordinate2D(latitude: model.latitude, longitude: model.longitude), distance: 500, heading: 90, pitch: 80)))
            }
        }
        .onAppear {
            if vm.apiData.isEmpty {
                Task {
                    await vm.fetchData()
                }
            }
        }

onAppear를 통해서 함수를 호출하면 된다.

@ObservedObject vs @StateObject 비교는 새롭게 글을 작성해야 할듯하다.

카테고리: ,

업데이트: