와챠의 우당탕탕 개발 기록장
[Android/kotlin] 기상청 단기 예보 조회 api 연결 with Jetpack Compose, MVVM, Hilt (1) 본문
[Android/kotlin] 기상청 단기 예보 조회 api 연결 with Jetpack Compose, MVVM, Hilt (1)
minWachya 2025. 6. 10. 12:20Jetpack Compose로 네트워크 연결을 해보려고 한다.
아무래도 안드가 제공해주는 강의에선 실제 네트워크 연결이 아니다보니까 기술적 성장에 한계가 있는 거 같아 직접 연결해보기로 했다.
그동안 배운 개념들 총 집합...!!!
Hilt 이용해서 의존성 주입하고, (프로젝트가 크진 않지만 확장 가능성 고려해서) MVVM 패턴으로 플젝 구성해보려고 한다.
근데 넘 길어질 거 같아서 2개로 나눠서 올릴 듯
레츠꼬
<목차>
0. 준비
- 결과 화면
- 플젝 초기 설정
- api 명세 설명
1. Hilt, Retrofit2 라이브러리 추가
2. Hilt 초기 설정
3. 네트워크 연결을 위한 Retrofit2 객체 생성
4. UI에서 사용할 데이터 정리
5. request, response 생성
0. 준비
- 결과 화면
왼쪽 UI 중에서... 오른쪽 부분에 대한 개발 과정을 적어보려고 한다.
나머지 개발 부분도 차차..ㅋ
- 플젝 초기 설정
1. GitHub Repository 설정하고, Jira까지 연동해봤당ㅋ 이정도까지 안 해도 되는데 그냥... 미니 플젝이어도 관리 잘 하고 싶었음
2. Figma 통해서 UI/Flow(메뉴구성도, 요구사항정의서, 화면 설계서)/Style(Color, App Logo, Icon, Font)까지 정해 둠
3. 단기 예보 조회(이하 기상청) api가 제공하는 정보 중 내가 쓸 정보들만 모아서 따로 api 명세서를 정리했다.
Color, Font 등 AppTheme는 Material Theme Builder를 통해 생성했다. 관련 글은 전에 작성한 적 있어서 패스
[Compose] 2-1. Material Design 3으로 앱 테마 지정 / Material Theme Builder / Color, Type, Theme, Shape 설정
이제 강의 2: 레이아웃, 테마 설정, 애니메이션을 공부해보자. Material Theme Builder를 통해 AppTheme 생성Surface로 색조와 그림자 설정색 변경을 통한 강조서체 스타일 설정도형(모서리) 설정1. Material The
min-wachya.tistory.com
- api 명세 설명
api 명세서만 따로 간단히 소개해보겠다.
기상청에서 단기 예보 정보를 얻기 위해선 아래 정보를 url에 포함해 요청해야 한다.
http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0
/getVilageFcst
?serviceKey=인증키
&numOfRows=10
&pageNo=1
&base_date=20210628
&base_time=050
0&nx=55
&ny=127
각 쿼리파라미터 설명은 아래와 같다.
아래 위 쿼리 파라미터를 통해 요청했을 때 제공받는 response는 아래와 같다.
{
"response":{
"header":{
"resultCode":"00",
"resultMsg":"NORMAL_SERVICE"
},
"body":{
"dataType":"JSON",
"items":{
"item":[{
"baseDate":"20250604",
"baseTime":"1700",
"category":"TMP",
"fcstDate":"20250604",
"fcstTime":"1800",
"fcstValue":"22",
"nx":55,
"ny":127
}]
},
"pageNo":1,
"numOfRows":10,
"totalCount":1052
}
}
}
이 중에서 내가 필요한 건 아래와 같음. 정리해뒀어여
이 정보를 토대로 개발 ㄱㄱ
1. Hilt, Retrofit2 라이브러리 추가
Hilt, retrofit2 관련 라이브러리를 추가해준다.
- libs.versions.toml 파일에 아래 내용 추가해주고 싱크
[versions]
// ...
googleFont = "1.8.2"
hilt = "2.55"
hiltAndroid = "2.56.2"
hiltLifecycle = "2.9.1"
retrofit = "2.9.0"
location = "21.3.0"
ksp = "2.0.21-1.0.27"
[libraries]
// ...
ui-text-google-fonts = { module = "androidx.compose.ui:ui-text-google-fonts", version.ref = "googleFont"}
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-android-ksp = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hiltAndroid" }
lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "hiltLifecycle" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "location" }
[plugins]
// ...
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltAndroid" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
- build.gradle: Project에 아래 코드 추가
plugins {
// ...
alias(libs.plugins.hilt.android) apply false
alias(libs.plugins.ksp) apply false
}
- build.gradle: App에 아래 코드 추가
plugins {
// ...
alias(libs.plugins.ksp)
alias(libs.plugins.hilt.android)
}
// ...
dependencies {
//...
// GoogleFont
implementation(libs.ui.text.google.fonts)
// Hilt
implementation(libs.hilt.android)
implementation(libs.lifecycle.viewmodel.compose)
ksp(libs.hilt.android.ksp)
//retrofit2
implementation (libs.converter.gson)
implementation (libs.retrofit)
}
2. Hilt 초기 설정
@HiltAndroidApp이 붙은 Application 클래스를 추가하여 작업의 시작점을 나타내준다.
이렇게 해야 Hilt가 앱의 전반적인 생명주기를 관리할 수 있고, 컴파일 때 의존성 객체를 제공받을 수 있다.
package com.example.clothesbyweather
@HiltAndroidApp
class ClothesByWeatherApplication: Application() {}
3. 네트워크 연결을 위한 Retrofit2 객체 생성
di 패키지 밑에 네트워크 연결을 위한 객체를 생성해준다.
의존성 객체를 생성한다는 것을 알리기 위해 클래스에 @Module을 사용했고,
retrofit 객체를 싱글톤으로 관리하기 위해 InstallIn으로도 SingletonComponent을 명시했다.
의존성 객체를 생성하기 위해 @Provider를 사용했고, 하나의 객체만 필요하기 때문에 @Singleton을 사용했다.
Retrofit 객체에 baseUrl로 기상청에서 요구하는 baseUrl을 작성하면 된다.
package com.example.clothesbyweather.di
@Module
@InstallIn(SingletonComponent::class)
object RetrofitModule {
@Provides
@Singleton
fun providesOkHttpClient(): OkHttpClient =
OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.writeTimeout(10, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS)
.build()
@Provides
@Singleton
fun providesRetrofit(okHttpClient: OkHttpClient): Retrofit =
Retrofit.Builder()
.baseUrl("http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/")
.client(okHttpClient)
.addConverterFactory(
GsonConverterFactory.create(
GsonBuilder().serializeNulls().create()
)
)
.build()
}
4. UI에서 사용할 데이터 정리
여기서 사용되는 데이터는 시간, 날씨 정보에 따른 날씨 이모지, 온도, 습도, 강수량 정보이다.
이 데이터를 ui에서 편하게 사용하기 위해 HomeWeather라는 클래스로 만들어 관리하려고 한다.
이때 문제가 있는데, 기상청에서는 예측 시간을 "오전 1시"와 같은 형식이 아니라 "0100"으로 보낸다. 또한 날씨 정보에 따른 이미지도 제공하지 않기 때문에 기존 정보를 통 이모지도 추가적으로 만들어야 한다.
온도, 습도, 강수량은 api가 주는 정보를 그대로 사용하고, 시간이랑 날씨 정보에 따른 이모지는 아래의 추가 작업을 거치려고 한다.
시간: fcstTime(예측 시간)도 "오전 1시" 형식에 맞춰서 출력될 수 있도록 했다.
이모지: pty(강수 형태), sky(하늘 상태)에 따라서 날씨 이모지를 정하도록 했고, pty, sky에는 접근할 수 없도록 private를 사용했다.
이걸 고려해서 짜면 코드는 아래와 같다.
package com.example.clothesbyweather.domain.entity
data class Home(val weatherList : ArrayList<HomeWeather>)
data class HomeWeather(
private val pty: Int,
private val sky: Int,
private val fcstDate: String,
private val fcstTime: String,
val temperature: Int,
val humidity: Int,
val precipitation: Int
) {
val weatherEmoji: String
get() =
// PTY: 없음(0), 비(1), 비/눈(2), 눈(3), 소나기(4)
if(pty != 0) when(pty) {
1 -> "🌧"
2 -> "🌨"
3 -> "❄"
else -> "🌧"
}
// SKY: 맑음(1), 구름많음(3), 흐림(4)
else when(sky) {
1 -> "☀"
3 -> "🌤"
else -> "☁"
}
val time: String
get() {
val hour = fcstTime.substring(0..1).toInt()
return if(hour < 12) "오전 ${hour}시" else "오후 ${hour}시"
}
}
5. request, response 생성
-request
api 명세서에서 설명했듯이 url로 요청 시 url에 한 페이지 수 결과, 예보 지점 x, y..등의 정보를 함께 전달해야 한다.
쿼리 파라미터로 보내는 정보가 많기 때문에 data class로 만들어 관리하려고 한다.
package com.example.clothesbyweather.data.remote.entity.request
data class HomeRequest(
val numOfRows : Int,
val pageNo : Int,
val dataType : String,
val baseDate : String,
val baseTime : String,
val nx : Int,
val ny : Int
)
- response
response가 좀 중요하다...!!!
왜냐면 나는 api가 주는 데이터를 그대로 쓸 것이 아닌, 위에서 만든 HomeWeather에 맞게 변형하고 싶기 때문이다.
이를 위해 api가 제공해주는대로 정보 중 필요한 정보만 받되, toHome, toWeatherList를 통해 UI에서 사용되는 데이터로 변경하고자 했다.
그리고 category의 경우 받는 데이터가 일정해서 enum class로 놓고 관리했다. 이 덕분에 toWeatherList에서 category를 통해 데이터 관리하는 코드를 깔끔히 짤 수 있었다!
package com.example.clothesbyweather.data.remote.entity.response
data class HomeResponse(val response: WeatherResponse) {
fun toHome(): Home =
Home(weatherList = this.response.body.items.toWeatherList())
}
data class WeatherResponse(val header: WeatherHeader, val body: WeatherBody)
data class WeatherHeader(val resultCode : Int, val resultMsg : String)
data class WeatherBody(val dataType : String, val items : WeatherItems, val totalCount : Int)
data class WeatherItems(val item : ArrayList<WeatherItem>) {
fun toWeatherList(): ArrayList<HomeWeather> {
val weatherGroup = item.groupBy{ it.fcstTime }
var weatherList = arrayListOf<HomeWeather>()
weatherGroup.forEach { group ->
var pty = 0
var sky = 0
var temperature = 0
var humidity = 0
var precipitation = 0
group.value.forEach { weather ->
when(weather.category) {
// 1시간 기온
CategoryType.TMP -> temperature = weather.fcstValue.toInt()
// 강수 확률
CategoryType.POP -> precipitation = weather.fcstValue.toInt()
// 강수 형태
CategoryType.PTY -> pty = weather.fcstValue.toInt()
// 습도
CategoryType.REH -> humidity = weather.fcstValue.toInt()
// 하늘 상태
CategoryType.SKY -> sky = weather.fcstValue.toInt()
else -> {}
}
}
weatherList += HomeWeather(
pty = pty,
sky = sky,
fcstDate = group.value[0].fcstDate,
fcstTime = group.value[0].fcstTime,
temperature = temperature,
humidity = humidity,
precipitation = precipitation
)
}
return weatherList
}
}
data class WeatherItem(
val category: CategoryType,
val fcstDate: String,
val fcstTime: String,
val fcstValue: String,
)
enum class CategoryType {
TMP, // 1시간 기온
POP, // 강수 확률
PTY, // 강수 형태
REH, // 습도
SKY, // 하늘 상태
PCP, // 1시간 강수량
SNO, // 1시간 신적설
TMN, // 일 최저 기온
TMX, // 일 최고 기온
UUU, // 풍속(동서성분)
VVV, // 풍속(남북성분)
WAV, // 파고
VEC, // 풍향
WSD // 풍속
}
일단 이정도만 해놓고..,
글이 넘 길어지니까 2탄으로 다시 써야겠다 ㄱㄷ