[TIL] Android 앱 개발 심화 : 지도API, Retrofit 외 2개

지혜·2024년 1월 24일

Android_TIL

목록 보기
45/70

✏240124 수요일 TIL(Today I learned) 오늘 배운 것

📖구글 지도 API 사용하기

1. GoogleCloud에서 사용자 인증키 만들기

  • GoogleCloud에 접속하여 구글 계정으로 로그인 -> 새 프로젝트 생성 -> [메뉴] API 및 서비스 -> 사용자 인증 정보 -> 사용자 인증정보 만들기 : API 키 -> 생성된 API키 복사

2. 지도 사용 설정하기

  • build.gradle파일에 Googlemap library를 추가한다.

    dependencies {
    	...
    	implementation 'com.google.android.gms:play-services-maps:18.1.0'
    	implementation 'com.google.android.gms:play-services-location:21.0.1'
    }
  • AndriodManifest.xmlpermissiongoogle-map API키를 추가

    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    ...
    	<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
        <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
        <uses-permission android:name="android.permission.INTERNET"/>
    
    	<application
         		...
               				>
          	<uses-library android:name="org.apache.http.legacy" android:required="true"/>
          	<meta-data android:name="com.google.android.maps.v2.API_KEY"
               android:value="== Google Cloud에서 생성한 API키 입력!! ==="/>
          	<meta-data android:name="com.google.android.gms.version"
               android:value="@integer/google_play_services_version"/>
          	<activity
          		...
    	  	</activity>
      	</application>
    </manifest>
    

3. 구글맵 레이아웃 (xml 코드)

  • <fragment xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/mapView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:name="com.google.android.gms.maps.SupportMapFragment"/>

4. 지도 제어하기

  • 메인 액티비티에서 OnMapReadyCallback 상속받고 onMapReady 오버라이드 받아서 사용

    class MainActivity : AppCompatActivity(), OnMapReadyCallback {
    
       private lateinit var mGoogleMap: GoogleMap
       //var googleMap: GoogleMap? = null
    
       //위치 서비스가 gps를 사용해서 위치를 확인
       lateinit var fusedLocationClient: FusedLocationProviderClient
    
       //위치 값 요청에 대한 갱신 정보를 받는 변수
       lateinit var locationCallback: LocationCallback
    
       lateinit var locationPermission: ActivityResultLauncher<Array<String>>
    
       //지도를 출력하는 뷰 객체 연결하기
       override fun onCreate(savedInstanceState: Bundle?) {
           super.onCreate(savedInstanceState)
           setContentView(R.layout.activity_main)
    
           locationPermission = registerForActivityResult(
               ActivityResultContracts.RequestMultiplePermissions()){ results ->
               if(results.all{it.value}){
                   (supportFragmentManager.findFragmentById(R.id.mapView) as SupportMapFragment)!!.getMapAsync(this)
               }else{ //문제가 발생했을 때
                   Toast.makeText(this,"권한 승인이 필요합니다.",Toast.LENGTH_LONG).show()
               }
           }
    
           //권한 요청
           locationPermission.launch(
               arrayOf(
                   Manifest.permission.ACCESS_COARSE_LOCATION,
                   Manifest.permission.ACCESS_FINE_LOCATION
               )
           )
       }
    
       // 지도 객체를 이용할 수 있는 상황이 될 때
       override fun onMapReady(p0: GoogleMap) {
    
           val seoul = LatLng(37.566610, 126.978403)
           mGoogleMap = p0
           mGoogleMap.mapType = GoogleMap.MAP_TYPE_NORMAL // default 노말 생략 가능
           mGoogleMap.apply {
           		//마커 표시하기
               val markerOptions = MarkerOptions()
               markerOptions.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE))
               markerOptions.position(seoul)
               markerOptions.title("서울시청")
               markerOptions.snippet("Tel:01-120")
               addMarker(markerOptions)
           }
    
           fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
           updateLocation()
       }
    
       fun updateLocation(){
    		//위치 요청하기
           val locationRequest = LocationRequest.create().apply {
               interval = 1000
               fastestInterval = 500
               priority = LocationRequest.PRIORITY_HIGH_ACCURACY
           }
    
           locationCallback = object : LocationCallback(){
               //1초에 한번씩 변경된 위치 정보가 onLocationResult 으로 전달된다.
               override fun onLocationResult(locationResult: LocationResult) {
                   locationResult?.let{
                       for (location in it.locations){
                           Log.d("위치정보",  "위도: ${location.latitude} 경도: ${location.longitude}")
                              setLastLocation(location) //계속 실시간으로 위치를 받아오고 있기 때문에 맵을 확대해도 다시 줄어든다.
                       }
                   }
               }
           }
           //권한 처리
           if (ActivityCompat.checkSelfPermission(
                   this,
                   Manifest.permission.ACCESS_FINE_LOCATION
               ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
                   this,
                   Manifest.permission.ACCESS_COARSE_LOCATION
               ) != PackageManager.PERMISSION_GRANTED
           ) {
               return
           }
    
           fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback,
               Looper.myLooper()!!
           )
       }
    
    	//지도의 중심 이동하기
       fun setLastLocation(lastLocation: Location){
           val LATLNG = LatLng(lastLocation.latitude,lastLocation.longitude)
    		
           //현재위치 마커로 표시
           val makerOptions = MarkerOptions().position(LATLNG).title("현재위치")
           val cameraPosition = CameraPosition.Builder().target(LATLNG).zoom(15.0f).build()
    
           mGoogleMap.addMarker(makerOptions)
           mGoogleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition))
       }
    }
    • onCreate에서 지도를 출력하는 뷰 객체와 연결한다.
    • updateLocation 에서 LocationRequest.create()LocationCallback()으로 위치를 요청한다.
    • setLastLocation에서 지도의 중심을 이동하고, 현재위치를 마커를 추가하여 표시해준다.
    • MarkerOptions().position()을 사용하여 마커를 설정할 수 있다. 이외에 title, icon, snippet 등을 설정할 수 있다. .addMarker()를 사용하여 지도상에 표시한다.


📖Retrofit (서버와 클라이언트)

  • http통신을 쉽고 편하게 해줄 수 있는 HTTP 클라이언트 라이브러리이다.
  • 비동기처리도 잘 해준다.

[서버와 클라이언트의 통신 과정]

  • 클라이언트(사용자 대표)는 서버(데이터/리소스제공)에 특정한 정보나 서비스를 요청하고, 서버는 해당 요청을 처리한 뒤 적절한 응답을 클라이언트에게 전송한다.
  • 3-Tier 아키텍쳐 : 클라이언트 ⇆ 서버 ⇆ 데이터베이스
  • 통신방식에는 여러 종류가 있지만 REST API같은 웹서비스 통신 방식을 사용하기 위해 HTTP/HTTPS(웹 기반) 프로토콜을 사용한다.

[API]✨

  • API(Application Programming Interface)
    : 서버가 클라이언트에게 리소스를 잘 활용할 수 있도록 인터페이스를 제공해주는 것을 API라고 한다. 즉, 리소스를 활용할 수 있게 만들어주는 양식이고, URL,URI같은 주소를 통해 접근한다.
  • [RESTful API] REST API (Representational State Transfer)
    • 리소스가 고유한 URI로 식별된다.
    • JSON과 XML 형식으로 표현된다.
    • 요청은 서버에서 필요한 모든 정보를 포함하고 있어야한다. -> 각 요청을 서버가 개별적으로 처리 (stateless통신)
    • 사용자 인터페이스 (클라이언트)와 데이터 저장소가 분리되어 독립성이 높아진다.
    • 응답 데이터에 캐싱이 가능한지 여부를 명시하여 성능향상에 도움이 된다.
    • 서버와 클라이언트 사이에 보안 등의 다양한 계층이 존재 할 수 있다.
    • 주요 HTTP 메서드 : GET(리소스조회), POST(생성), PUT(수정), DELETE(삭제)

[JSON & GSON]

  • JSON : JavaScript Object Notation, 데이터를 저장하거나 전송할 때 많이 사용되는 경량의 DATA 교환 형식이다.
    • JSON데이터는 하나의 NAME(String타입)과 VALUE(다양한타입,null가능)로 이루어져있다.
  • GSON : Google에서 제공하는 오픈소스 라이브러리, Java와 Kotlin에서 주로 사용된다. '직렬화(Serialization)'와 '역직렬화(Deserialization)' 작업을 간단하게 할 수 있도록 도와준다. (= 간결성과 효율성이 높아진다.)

[Retrofit]

  • REST APIHTTP 요청을 자바 인터페이스로 변환하는 것을 주 목적으로 한다.
  • 다양한 데이터 변환 컨버터를 제공하고, 비동기 프로그래밍 라이브러리 (RxJava, Coroutines 등)와 연동이 가능하다.
  • 간단한 어노테이션으로 요청 메서드나 URIL을 정의할 수 있어서 복잡한 HTTP API 요청을 쉽고 간결하게 만든다.
  • 인터셉터를 사용하여 요청/응답 프로세스를 확장하거나 수정할 수 있고 내부적으로 OkHttp 라이브러리를 사용하여 통신하기 때문에 안정성이 높다.

[Retrofit 사용하기]

  1. http통신을 할 때에는 인터넷 사용권한을 AndroidManifest.xml에 추가해줘야한다.

    <uses-permission android:name="android.permission.INTERNET"/>
  2. build.gradle에 라이브러리를 추가해줘야한다.

    dependencies {
    	...
     implementation 'com.squareup.retrofit2:retrofit:2.x.x'
     implementation 'com.squareup.retrofit2:converter-gson:2.x.x' // Gson 컨버터 추가
     	...
    }
  3. [NetWorkInterface.kt] : API 인터페이스를 정의해줘야한다.

    interface ApiService {
       @GET("users/{id}")
       fun getUser(@Path("id") id: Int): Call<User>
    }
    
    //예시코드 @GET 안에 유저아이디는 요청주소의 세부옵션 값을 넣는다.
    interface NetWorkInterface {
       @GET("getCtprvnRltmMesureDnsty") //시도별 실시간 측정정보 조회 주소
       suspend fun getDust(@QueryMap param: HashMap<String, String>): Dust
    }
  4. [NetWorkClient.kt] : Retrofit 인스턴스를 생성한다.
    ->보통 이 포맷을 그대로 사용한다.

    val retrofit = Retrofit.Builder()
       .baseUrl("https://api.example.com/")
       .addConverterFactory(GsonConverterFactory.create())
       .build()
        
    val apiService = retrofit.create(ApiService::class.java)
    
    //예시코드
    object NetWorkClient {
    
    	//서비스URL 마지막에 /까지 붙여주기
       private const val DUST_BASE_URL = "http://apis.data.go.kr/B552584/ArpltnInforInqireSvc/"
    
       private fun createOkHttpClient(): OkHttpClient {
           val interceptor = HttpLoggingInterceptor()
    
           if (BuildConfig.DEBUG)
               interceptor.level = HttpLoggingInterceptor.Level.BODY
           else
               interceptor.level = HttpLoggingInterceptor.Level.NONE
    
           return OkHttpClient.Builder()
               .connectTimeout(20, TimeUnit.SECONDS)
               .readTimeout(20, TimeUnit.SECONDS)
               .writeTimeout(20, TimeUnit.SECONDS)
               .addNetworkInterceptor(interceptor)
               .build()
           }
    
       private val dustRetrofit = Retrofit.Builder()
           .baseUrl(DUST_BASE_URL).addConverterFactory(GsonConverterFactory.create()).client(
               createOkHttpClient()
           ).build()
    
       val dustNetWork: NetWorkInterface = dustRetrofit.create(NetWorkInterface::class.java)
    }
  1. JSON형태의 모델 클래스를 data class 형태로 작성한다.
    • DTO에서 data class를 api에서 넘겨주는 정보의 헤더 바디 값들과 똑같이 만들어주면 된다.
    • 변수명이 원래 서버에서 사용하는 값과 다르다면 오류가 발생하지만, 앱 내에서 다른 변수명으로 사용하고 싶다면, @SerializedName("서버에서 변수명") val 앱내변수명:자료형 형태를 사용하여 변경할 수 있다.

[Retrofit 응답 처리]

  • 동기식 요청

    //NetWorkClient에 apiService 설정.
    val response: Response<User> = apiService.getUser(id).execute()
  • 비동기식 요청 : 콜백 사용

    //Call 인터페이스의 enqueue을 통해서도 비동기 구현 가능.
    apiService.getUser(id).enqueue(object: Callback<User> {
       override fun onResponse(call: Call<User>, response: Response<User>) {
           // 처리
       }
    
       override fun onFailure(call: Call<User>, t: Throwable) {
           // 오류 처리
       }
    })
  • 응답 객체 사용 : Response 객체를 통해 HTTP 응답의 여러 정보에 접근

    if (response.isSuccessful) {
       val user: User? = response.body()
    } else {
       // 오류 메시지 처리
       val error: String = response.errorBody()?.string() ?: "Unknown error"
    }
  • 오류 처리하기 (onFailure 콜백) : 네트워크 오류나 데이터 변환 오류 등에서 호출

    override fun onFailure(call: Call<User>, t: Throwable) {
       // 오류 메시지 표시
       Log.e("API_ERROR", t.message ?: "Unknown error")
    }
  • http통신,데이터베이스연결같은 것들은 UI 메인스레드에서 불가능하다.
    메인스레드에서 실행해버리면 통신하고서 통신쪽에서 렉이걸린다거나 딜레이가 생기면 그동안 메인UI가 멈춰버린채로 있기 때문이다.

  • 코루틴에서는 UI쓰레드를 바로 건드릴 수 없기 때문에, 이를 위해선 runOnUiThread{ } 를 사용해줘야한다.



📖앱 개발 프로세스

  • 대체로 [아이디어구상 -> 실제 기능 구상 (+광고 기획) -> 그래픽디자인 -> 본격적인 앱개발 -> 테스트 -> 배포 (+마케팅)] 순서로 진행된다.


📖디버깅

  • 소스 코드의 오류 또는 버그를 찾아서 수정하는 과정
  • 오류가 났을 때 안내해주는 Logcat과 build의 안내문구로 오류를 추적하여 원인을 고칠 수 있다.
  • 직접 디버깅 도구로 코드를 하나씩 돌려가면서 안에 어떤 데이터가 들어가는지 추적해가면서 코드를 작성할 수도 있다. 특히 (이중)for문 등에서 여러 데이터가 돌아갈 때, 데이터의 변화가 어떻게 나타나는지 확인할 때 유용하게 사용할 수 있다.

✏새롭게 알게된 것과 내일 할 일

  • 직접 구글에서 제공하는 API나, 공공에서 지원해주는 오픈API를 활용해서 어플리케이션을 만드는 법을 배웠다. 확실히 이런 것들을 사용하면서 어플리케이션의 완성도가 더욱 높아지는 것 같다.
  • 레트로핏은 좀 더 사용해보면서 응답 처리에 대한 내용을 확인해야 할 것 같다. 요청해서 온 응답에 대한 값들을 가공해서 원하는대로 사용하기 때문에 그냥 기본적인 코드나 한 번 사용해 본걸로는 쉽게 감이 오기 어려운 것 같다.
  • 좀 더 디자인 면에서도 (스피너같은 것)에 대해 깃허브에서 제공하는 디자인툴..?을 활용하는 법도 잠깐 알려주셨는데, 실제로 사용할 수 있을지... 이것도 많이 해봐야알 것 같다.
  • 앱 개발 프로세스나 디버깅의 경우는.. 얘네야 말로 직접 겪어서 체득해야하는 것들이라서.. 그래도 오류 찾는 연습은 나름의 요령이 생겼다고 생각한다..ㅎ,, 디버깅 도구로 데이터 확인을 할 수 있다는 사실은 이번에 처음 알아서, 솔직히 한 번도 안썼는데.. 맨날 로그 찍는것보다 이게 나을지.. 사용을 꼭 해봐야겠다.
  • 어려운 내용은 맞는데, 지금은 어렵다기 보단 복잡하다? 는 느낌이 더 강하다. 만약 JSON이나 데이터베이스 수업 경험이 없었으면.. 때려쳤을지 모른다.ㅎㅎ 나는 개인적으로 조금이라도 코드가 복잡해지면 해롱해롱해지는 경우가 아주 많은데, 이번 수업을 통해 이를 극복했으면 좋겠다.

  • 일단 어거지로라도..ㅎㅎ 강의를 다 들었기 때문에, 내일은 수준별 수업 강의 내용과 과제 내용을 판단해서 실습에 대한 보충을 하고 개인과제를 할지, 바로 개인과제를 할지 고민해봐야겠다. 근데 만약 실습 보충을 한다고 하더라도 목요일안에 끝내고 금요일에는 개인과제에 들어갈 수 있도록 조정해야할 것이다. 이번 개인과제도 무사히 끝내고 팀 과제에 보탬이 될 수 있도록 파이팅하자! 파이팅~
profile
파이팅!

0개의 댓글