🇺🇸 EN

🌦 WeatherKit 완전정복

Apple 공식 날씨 API로 정확한 날씨 정보를 제공하세요. 현재 날씨부터 10일 예보까지!

⭐ 난이도: ⭐⭐ ⏱️ 예상 시간: 1h 📂 App Services

✨ WeatherKit이란?

WeatherKit은 Apple이 제공하는 공식 날씨 API입니다. iOS 16+에서 사용 가능하며, 월 50만 건까지 무료입니다.

🔑 설정 (Apple Developer)

Setup Steps
1. Apple Developer 콘솔 접속
2. Certificates, Identifiers & Profiles
3. Keys 탭에서 새 키 생성
4. "WeatherKit" 체크박스 활성화
5. 다운로드한 .p8 파일 안전하게 보관

⚠️ 키는 한 번만 다운로드 가능하므로 백업 필수!

🌡️ 현재 날씨 가져오기

CurrentWeather.swift
import WeatherKit
import CoreLocation

let weatherService = WeatherService()

func fetchCurrentWeather(for location: CLLocation) async throws -> CurrentWeather {
    let weather = try await weatherService.weather(for: location)
    return weather.currentWeather
}

// 사용 예시
Task {
    let location = CLLocation(latitude: 37.5665, longitude: 126.9780)  // 서울
    let current = try await fetchCurrentWeather(for: location)

    print("온도: \(current.temperature.value)°C")
    print("체감 온도: \(current.apparentTemperature.value)°C")
    print("습도: \(current.humidity * 100)%")
    print("날씨: \(current.condition.description)")
    print("바람: \(current.wind.speed.value) m/s")
}

📅 시간별 예보 (24시간)

HourlyForecast.swift
func fetchHourlyForecast(for location: CLLocation) async throws -> Forecast<HourWeather> {
    let weather = try await weatherService.weather(for: location)
    return weather.hourlyForecast
}

// 사용 예시
Task {
    let location = CLLocation(latitude: 37.5665, longitude: 126.9780)
    let hourly = try await fetchHourlyForecast(for: location)

    // 다음 12시간 예보
    for hour in hourly.prefix(12) {
        print("\(hour.date) - \(hour.temperature.value)°C - \(hour.condition)")
    }
}

📆 일별 예보 (10일)

DailyForecast.swift
func fetchDailyForecast(for location: CLLocation) async throws -> Forecast<DayWeather> {
    let weather = try await weatherService.weather(for: location)
    return weather.dailyForecast
}

// 사용 예시
Task {
    let location = CLLocation(latitude: 37.5665, longitude: 126.9780)
    let daily = try await fetchDailyForecast(for: location)

    for day in daily {
        print("\(day.date)")
        print("최고: \(day.highTemperature.value)°C")
        print("최저: \(day.lowTemperature.value)°C")
        print("강수 확률: \(day.precipitationChance * 100)%")
        print("일출: \(day.sun.sunrise!)")
        print("일몰: \(day.sun.sunset!)")
    }
}

☔ 분별 강수 예보 (다음 1시간)

MinuteForecast.swift
func fetchMinuteForecast(for location: CLLocation) async throws -> Forecast<MinuteWeather>? {
    let weather = try await weatherService.weather(for: location)
    return weather.minuteForecast
}

// 사용 예시 (미국만 지원)
Task {
    let location = CLLocation(latitude: 37.7749, longitude: -122.4194)  // 샌프란시스코
    if let minute = try await fetchMinuteForecast(for: location) {
        for m in minute {
            print("\(m.date) - 강수 강도: \(m.precipitation.value)")
        }
    } else {
        print("분별 예보 지원 안 함 (미국만 가능)")
    }
}

🚨 기상 경보

WeatherAlerts.swift
func fetchWeatherAlerts(for location: CLLocation) async throws -> [WeatherAlert]? {
    let weather = try await weatherService.weather(for: location)
    return weather.weatherAlerts
}

// 사용 예시
Task {
    let location = CLLocation(latitude: 37.5665, longitude: 126.9780)
    if let alerts = try await fetchWeatherAlerts(for: location) {
        for alert in alerts {
            print("⚠️ \(alert.summary)")
            print("경보 수준: \(alert.severity)")
            print("발효: \(alert.effectiveTime)")
            print("만료: \(alert.expireTime)")
        }
    }
}

🌤 날씨 상태 (WeatherCondition)

WeatherCondition.swift
import WeatherKit

func getWeatherIcon(_ condition: WeatherCondition) -> String {
    switch condition {
    case .clear:
        return "sun.max.fill"
    case .cloudy:
        return "cloud.fill"
    case .rain:
        return "cloud.rain.fill"
    case .snow:
        return "snow"
    case .sleet:
        return "cloud.sleet.fill"
    case .hail:
        return "cloud.hail.fill"
    case .thunderstorms:
        return "cloud.bolt.fill"
    case .heavyRain:
        return "cloud.heavyrain.fill"
    case .strongStorms:
        return "cloud.bolt.rain.fill"
    case .foggy:
        return "cloud.fog.fill"
    case .windy:
        return "wind"
    @unknown default:
        return "questionmark"
    }
}

// 한글 설명
func getWeatherDescription(_ condition: WeatherCondition) -> String {
    switch condition {
    case .clear: return "맑음"
    case .cloudy: return "흐림"
    case .rain: return "비"
    case .snow: return "눈"
    case .thunderstorms: return "뇌우"
    @unknown default: return "알 수 없음"
    }
}

📱 SwiftUI 통합

WeatherView.swift
import SwiftUI
import WeatherKit
import CoreLocation

@Observable
class WeatherViewModel {
    var currentWeather: CurrentWeather?
    var hourlyForecast: Forecast<HourWeather>?
    var isLoading = false

    let weatherService = WeatherService()

    func loadWeather(for location: CLLocation) async {
        isLoading = true
        defer { isLoading = false }

        do {
            let weather = try await weatherService.weather(for: location)
            currentWeather = weather.currentWeather
            hourlyForecast = weather.hourlyForecast
        } catch {
            print("날씨 로드 실패: \(error)")
        }
    }
}

struct WeatherView: View {
    var viewModel = WeatherViewModel()
    let location = CLLocation(latitude: 37.5665, longitude: 126.9780)

    var body: some View {
        ScrollView {
            VStack(spacing: 20) {
                if viewModel.isLoading {
                    ProgressView()
                } else if let current = viewModel.currentWeather {
                    // 현재 날씨
                    VStack {
                        Image(systemName: getWeatherIcon(current.condition))
                            .font(.system(size: 80))
                            .symbolRenderingMode(.multicolor)

                        Text("\(Int(current.temperature.value))°")
                            .font(.system(size: 72, weight: .thin))

                        Text(getWeatherDescription(current.condition))
                            .font(.title2)
                    }

                    // 시간별 예보
                    if let hourly = viewModel.hourlyForecast {
                        ScrollView(.horizontal, showsIndicators: false) {
                            HStack(spacing: 16) {
                                ForEach(hourly.prefix(24), id: \.date) { hour in
                                    VStack {
                                        Text(hour.date, format: .dateTime.hour())
                                            .font(.caption)
                                        Image(systemName: getWeatherIcon(hour.condition))
                                            .font(.title2)
                                        Text("\(Int(hour.temperature.value))°")
                                            .font(.headline)
                                    }
                                    .frame(width: 60)
                                }
                            }
                            .padding()
                        }
                    }
                }
            }
        }
        .task {
            await viewModel.loadWeather(for: location)
        }
    }
}

⚡ 에러 처리

ErrorHandling.swift
do {
    let weather = try await weatherService.weather(for: location)
} catch {
    if let weatherError = error as? WeatherError {
        switch weatherError {
        case .locationUnavailable:
            print("위치 정보 없음")
        case .locationOutOfRange:
            print("지원하지 않는 지역")
        case .networkUnavailable:
            print("네트워크 연결 없음")
        @unknown default:
            print("알 수 없는 에러")
        }
    }
}

💰 사용량 제한

Rate Limits
WeatherKit API 제한:
- 월 50만 건 무료
- 초과 시: $0.0001/call (100만 건당 $100)
- Rate limit: 분당 500 requests

⚠️ 캐싱을 적극 활용하세요!
- 현재 날씨: 10분 캐시
- 시간별 예보: 30분 캐시
- 일별 예보: 1시간 캐시

💡 HIG 체크리스트
✅ Apple 제공 날씨 아이콘 사용 (SF Symbols)
✅ 데이터 출처 표시 ("Weather data provided by Apple")
✅ 적절한 캐싱으로 API 호출 최소화
✅ 로딩 상태 표시
✅ 네트워크 에러 처리

📦 학습 자료

💻
GitHub 프로젝트
🍎
Apple 공식 문서
🎥
WWDC22 세션

📎 Apple 공식 자료

📘 공식 문서 🎬 WWDC 세션