HTTP API로 받은 JSON 데이터를 SwiftUI View에서 이용하기

Seodam·2022년 5월 2일
0
post-thumbnail

서비스를 만들다보면 다른 회사에서 제공하는 데이터를 받아오거나, 자신의 서비스를 여러 요소로 분리하기 위해 API를 사용하게 됩니다.

대부분은 HTTP 형태로 데이터를 주고받는 HTTP API를 사용하게 되는데요, 대표적인 예로 정부 공공데이터와 카카오 지도 서비스가 있습니다.

상당히 자주 사용되는 기술인데도, SwiftUI와 HTTP API를 결합해서 사용하는 한국어 자료가 거의 없어서 애플 공식 문서와 영어 자료를 읽고 시행착오를 거쳤는데요, 그 과정에서 배운 내용을 간단하게나마 정리했습니다.

Xcode iOS 프로젝트에서 HTTP API로 날씨 데이터(JSON)를 요청한 뒤, Text 뷰에 띄우는 과정을 순서대로 살펴보겠습니다.

API 등록하기

본인이 사용하고자 하는 API 서비스의 키가 필요합니다. 경우에 따라 토큰을 제공할 수도 있으며, 이는 사이트마다 서로 다릅니다. 키가 필요치 않은 경우에는 이 단계를 건너뛰어도 괜찮습니다.

프로젝트 생성, 파일 만들기

Xcode에서 WeatherAPI라는 이름의 iOS 프로젝트를 새로 만들고, 추가로 Network.swift, Weather.swift 파일을 만듭니다. 파일트리 구조는 아래와 같습니다.

Image.png

JSON 값을 받아줄 struct 만들기

이번에 사용할 서비스(https://api.openweathermap.org)는 위도, 경도, 사용자의 키를 입력받아 해당 위치의 날씨 정보를 제공합니다.

샘플 데이터를 Quicktype(https://app.quicktype.io/)에 입력하면 자동으로 Swift struct 코드를 만들어줍니다.

일반적으로 샘플 JSON 데이터는 사이트에서 제공하지만, 그렇지 않으면 브라우저 검색창에 해당 API를 입력해서 얻을 수 있습니다.

파싱한 결과를 Weather.swift 파일에 붙여 넣습니다.

/// Weather.swift
import Foundation

// MARK: - Welcome
struct Welcome: Codable {
    let coord: Coord
    let weather: [Weather]
    let base: String
    let main: Main
    let visibility: Int
    let wind: Wind
    let clouds: Clouds
    let dt: Int
    let sys: Sys
    let timezone, id: Int
    let name: String
    let cod: Int
    
	// 초기화에 사용할 샘플 데이터
    static let sample = Welcome(
        coord: Coord(lon: 0, lat: 0),
        weather: [],
        base: "",
        main: Main(temp: 0, feelsLike: 0, tempMin: 0, tempMax: 0, pressure: 0, humidity: 0, seaLevel: 0, grndLevel: 0),
        visibility: 0,
        wind: Wind(speed: 0, deg: 0, gust: 0),
        clouds: Clouds(all: 0),
        dt: 0,
        sys: Sys(type: 0, id: 0, country: "", sunrise: 0, sunset: 0),
        timezone: 0,
        id: 0,
        name: "",
        cod: 0)
}

// MARK: - Clouds
struct Clouds: Codable {
    let all: Int
}

// MARK: - Coord
struct Coord: Codable {
    let lon, lat: Double
}

// MARK: - Main
struct Main: Codable {
    let temp: Double
    let feelsLike, tempMin, tempMax: Double
    let pressure, humidity, seaLevel, grndLevel: Int
    
    enum CodingKeys: String, CodingKey {
        case temp
        case feelsLike = "feels_like"
        case tempMin = "temp_min"
        case tempMax = "temp_max"
        case pressure, humidity
        case seaLevel = "sea_level"
        case grndLevel = "grnd_level"
    }
}

// MARK: - Sys
struct Sys: Codable {
    let type, id: Int?
    let country: String
    let sunrise, sunset: Int
}

// MARK: - Weather
struct Weather: Codable {
    let id: Int
    let main, weatherDescription, icon: String
    
    enum CodingKeys: String, CodingKey {
        case id, main
        case weatherDescription = "description"
        case icon
    }
}

// MARK: - Wind
struct Wind: Codable {
    let speed: Double
    let deg: Int
    let gust: Double
}

SwiftUI View에서 변수 초기화를 위해 샘플 데이터를 하나 만들어야 합니다. 저는 Int와 Double에 0을, String은 빈 값을 넣어주었습니다.

데이터 불러오기

Network.swift 파일에 아래 코드를 넣고, apiKey에 할당받은 본인의 API Key를 입력합니다.

여기서는 위도(lat)와 경도(lon) 값으로 각각 40, -80을 사용했는데, 필요한 위치에 따라 다르게 입력하면 됩니다.

/// Network.swift

import Foundation
import SwiftUI

class Network: ObservableObject {
    @Published var weather: Welcome = Welcome.sample
    
    var apiKey = "YourAPIKey"

    func getWeather() {
        guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?lat=40&lon=-80&appid=\(apiKey)") else { fatalError("Missing URL") }

        let urlRequest = URLRequest(url: url)
		 
		 // Task 만들기
        let dataTask = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
            if let error = error {
                print("Request error: ", error)
                return
            }

            guard let response = response as? HTTPURLResponse else { return }

			  // 응답 상태코드가 200(성공)일 경우에만 디코딩
            if response.statusCode == 200 {
                guard let data = data else { return }
                DispatchQueue.main.async {
                    do {
                        let decodedWeather = try JSONDecoder().decode(Welcome.self, from: data)
                        self.weather = decodedWeather
                    } catch let error {
                        print("Error decoding: ", error)
                    }
                }
            }
        }
		 // Task 수행하기
        dataTask.resume()
    }
}

데이터 흐름 만들기

WeatherAPIApp.swift에 var network = Network()를 추가해서 클래스의 레퍼런스를 만들어줍니다.

.environmentObject(network)로 ContentView()에 전달합니다. 에러가 뜰텐데, ContentView()에 코드를 추가하면 사라지므로 내버려둡니다.

/// WeatherAPIApp.swift

import SwiftUI

@main
struct WeatherAPIApp: App {
    var network = Network()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(network)
        }
    }
}

SwiftUI에 출력하기

/// ContentView.swift

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var network: Network
    
    /// 사용할 변수를 Computed Properties로 계산하기.
    // 캘빈 온도를 섭씨 온도로 변경
    var temp: String { String(format: "%.1f", (network.weather.main.temp - 273.15)) } 
    var description: String { network.weather.weather.first?.weatherDescription ?? "" }
    
    var body: some View {
        VStack {
            Text("온도: \(temp)")
                .padding()
            Text("설명: \(description)")
        }
        .onAppear {
            network.getWeather()
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(Network())
    }
}

최상위 뷰에 .onAppear { network.getWeather() } 를 붙여서 뷰가 그려질 때 날씨 데이터를 받아오도록 합니다.

struct 구조 프로퍼티를 따라 원하는 값을 가져와서(network.weather.main.humidity), SwiftUI의 View에 플레이스 홀더에 넣습니다.

불러온 값을 계산해서 사용하려는 경우(예시: 캘빈 온도를 섭씨 온도로 바꾸기), calculated properties를 사용하면 init() 없이 사용할 수 있습니다. (init()을 사용하면 @EnvironmentObjec를 초기화할 수 없어 오류가 발생합니다.)

발생할 수 있는 문제들

JSON 데이터가 우연히 정수로 맞아 떨어지는 샘플 데이터를 이용하면 정밀한 타입(Double이나 Float)이어야 할 struct의 프로퍼티가 Int로 할당되는 경우가 있는데, 이럴 경우 다음 응답에서 오류가 발생할 수 있습니다. 오류 메시지에서 어떤 값에 문제가 있는지 확인하고, 안전하게 더 정밀한 타입으로(예시: Int → Double) 바꾸면 해결됩니다.

주의사항

API를 요청할 때 사용한 키 값을 깃허브 등의 공개된 장소에 올리면 안 됩니다.

더 궁금한 점이 있다면 댓글로 알려주세요.

참고자료

  1. Apple Developer Documentation

  2. HTTP Request - SwiftUI Advanced Handbook - Design+Code

profile
twitter: @seodam_hst / 플랜B메이커, 너드, 개발자.

0개의 댓글