본문 바로가기

개발일기

8월 한달 토이 프로젝트를 해보자! -10 : Firebase 실패기 (진행중)

일단 회원가입 페이지 및.. 만들었는데, Detail 페이지는 앞의 LogIn, ListFragment를 적절히 섞은 것과 비슷한 형태라 할 수 있다. (만들어는 놓았지만 포스팅은 생략하겠다는 말)

이제, 메인스트림의 틀은 잡혔다고 본다. 계속 이걸 유지하고 세팅, 새 포스트 작성 등 다른 요소를 추가하기에는... 심심하다. 둘도 결국 UI밖에 건드리지 못할테니까.

그런고로, 이제 만들어봅시다 데이터!

firebase를 프로젝트에 연결하는 것으로 시작한다. 우선 프로젝트를 추가하는 것으로 시작할 것이다. 프로젝트 이름은... 음... 그냥 앱 이름으로 할까?

근데 나 이거 블로그에 썼나? 왜 기시감이 들지..

프로젝트 레벨 build.gradle에 아래와 같이 추가해준다.

buildscript {
    dependencies {
        classpath 'com.google.gms:google-services:4.3.13'
    }
}

다음으로 앱 레벨 build.gradle의 id를 추가해주고,

plugins {
    ...
    id 'com.google.gms.google-services'
}

공식문서를 따라 dependency를 추가해준다. 아래 링크 주석을 그대로 가져온 이유는 프로젝트를 구성하며 필요할 때마다 그때그떄 바로 접속하여 의존성을 추가하기 위함이다.

  // Import the Firebase BoM
  implementation platform('com.google.firebase:firebase-bom:30.3.2')

  // Add the dependency for the Firebase SDK for Google Analytics
  // When using the BoM, don't specify versions in Firebase dependencies
  implementation 'com.google.firebase:firebase-analytics-ktx'

  // Add the dependencies for any other desired Firebase products
  // https://firebase.google.com/docs/android/setup#available-libraries

Firabase에서 firestore 사용을 설정해준 후, terminal에서 내 프로젝트 디렉토리로 이동 후 firebase init 커맨드를 실행 후.. 하라는 대로 고르며 엔터를 누르는 프로세스를 거치면.. 따란! 되었음을 프로젝트 폴더에서 확인할 수 있다.

Diary 클래스에 아래와 같이 매핑 함수를 만들어주고, LogInViewModel에서 데이터베이스에 삽입하려 했다. 참, drawable을 가져오기 위하여 LogInViewModel을 AndroidViewModel을 상속받도록 하였다. LogInViewModel(application: Application) : AndroidVieModel(application)과 같이 구성하였으며, LogInFragment에서의 수정은 딱히 해줄 것이 없다.

Diary.kt

fun mappingForFireStore(): HashMap<Any?, String> {
    return hashMapOf(
        id to "id",
        account to "account",
        title to "title",
        content to "content",
        thumbnail to "thumbnail",
        likesCount to "likes_count",
        isLiked to "is_liked",
        comments to "comments",
        createdTime to "created_time",
        updatedTime to "updated_time"
    )
}

LoginViewModel.kt

private fun asyncTestFirebase() {
    val db = Firebase.firestore

    val data1 = Diary(
        id = UUID.randomUUID().toString(),
        account = idInput.value!!,
        title = "My first data",
        content = "I hope this would be successful. Just uploading my first data.",
        createdTime = Calendar.getInstance(Locale.KOREAN).time.time,
        thumbnail = BitmapFactory.decodeResource(getApplication<Application>().resources, R.drawable.sample_photo)
        )
        .mappingForFireStore()

    db.collection("diaries")
        .add(data1)
        .addOnSuccessListener { documentReference ->
            Log.d("Haha", "DocumentSnapshot added with ID: ${documentReference.id}")
        }
        .addOnFailureListener { error ->
            Log.d("OMG", error.stackTraceToString())
        }
}

logIn() 함수의 validation 이후를 아래와 같이 변경해주었다. 이를 위하여 Job 변수를 추가해주었다. ( job: Job? = null ) 나중에 Retrofit이든 ktor이든 쓰려면 CoroutineScope를 사용해줘야 할 것이기에 일단 써줌.

job = ioScope.launch {
    val process = async { asyncTestFirebase() }
    process.await()
    _logInFlag.postValue(true)
}

그리고 앱이 멋지게.. 펑! 터졌다.

java.lang.IllegalArgumentException: Could not serialize object. Maps with non-string keys are not supported

어이쿠야, Key-Value 페어를 해줘야 하는데, Value-Key 페어를 해주는 바람에 Diary의 속성들이 Key로 인식되어버렸다. 거꾸로 돌려준다.

다음 문제로는 우선 Bitmap을 저장하는 것이었고 (파이어스토어는 저장용량에 한계가 있다), 이 문제는 우선 Bitmap을 ByteArray로 변환해주어보았다.


// reference :
// https://www.android--code.com/2020/06/android-kotlin-bitmap-to-byte-array.html

fun Bitmap.toByteArray(): ByteArray{
    ByteArrayOutputStream().apply {
        compress(Bitmap.CompressFormat.JPEG,10,this)
        return toByteArray()
    }
}

다음으로는 array를 오브젝트로 파이어스토어에 저장할 수 없어 list를 사용하라는 Exception이 발생하여, 위 함수를 이제 toList()만 붙여 바꿔주었다. 그랬더니 List에 Byte를 저장할 수 없다고.. ㅂㄷㅂㄷ...

마지막으로는 .decodeResource(getApplication<Application>().resources, R.drawable.sample_photo) .toByteArray() .toString()로 스트링 처리해준 뒤, 아래 함수로 변환해주고자 하였다.

fun toBitmap(byteString: String): Bitmap {
    val byteArr = byteString.toByteArray()
    return BitmapFactory.decodeByteArray(byteArr,0,byteArr.size)
}

결과는, 일단 파이어스토어에 데이터를 넣는 것에는 성공하였다! 다음은 이걸 불러올 수 있을지 테스트해보아야 한다.

그런데 문제가 있다.

Diary 오브젝트로 가져올 때

does not define a no-argument constructor. If you are using ProGuard, make sure these constructors are not stripped

와 같은 오류가 떴다. Diary 의 속성들을 모두 초기화 해주니 이건 해결. 근데 이번엔 thumbnail 받아오는 쪽에서 문제가 생겼다. 봐보니 중간에 잘리는 듯. 문자열이 아니라 아예 리스트를 보내야 할 것 같다.

아이디어가 하나 떠올랐다. 썸네일을 사용하는 것이 아니라, 일단 리스트에서는 사진을 보여주지 않고, 디테일 페이지 들어갈 때 링크 된 사진이 있을 경우 해당 사진을 끌어오는 식으로. 네트워킹 호출은 빈번하게 발생할 수밖에 없겠지만, 일단 사진을 저장하는 나은 방식이라 생각 됨. 스토리지에 uri를 id를 링크해서 저장하는 방식을 취할 것임.

-> 그렇다. Diarythumbnail 프로퍼티는 비트맵, 바이트 배열, Base64 변환 문자열 등을 저장하는 대신 (변환 스트링은 메모리가 남아나질 않는다...) 이미지를 스토리지에 저장하고, 저장하는 uri를 저장하는 방식을 취하는 것이 현재 나로서는 최선이라 생각된다. 그러면 Detail 페이지에 진입할 때 해당 uri를 호출해야 하기는 하지만, 리스트 화면에 진입할 때는 오히려 이미지 받아오기, 변환 등의 과정을 거치지 않아도 되니 효과적일 수 있을 것이다.

우선, 헷갈리지 않게 Diarythumbnail -> imageUri로 변경해준다.

DiaryListBindingAdapter의 모양도 변경해주도록 한다. 더 이상 이미지가 들어올 자리는 없다. xml도 변경해주고, bind()도 변경해주며, 이에 따라 높이도 조정해준다.

그런데 써두고 보니, 현재의 상태로는 이미지가 존재하는지 여부를 체크하지 못하고 매번 디테일 페이지에서 호출을 하게 된다. 이미지 저장하는 함수를 나중에 포스트 작성 페이지에서 만들게 될 텐데, 그에 따라서 isThumbnailExist라는 프로퍼티를 변경해주면 굳이 없는데 호출을 하는 케이스를 방지할 수 있지 않을까?

하여, val isThumbnailExist: Boolean = false 요렇게 추가해주었다!

근데 이미지가 문제가 아닌 것 같다. Firestore 저장이 안 됨. 계속 아래의 로그가 뜬다. 저는 더 이상 이미지 같은 큰 것을 저장하지도 않는데, 왜죠???

E/SQLiteQuery: exception: Row too big to fit into CursorWindow requiredPos=0, totalRows=9; query: SELECT overlay_mutation, largest_batch_id FROM document_overlays WHERE uid = ? AND collection_path = ? AND largest_batch_id > ?

관련하여 결국 StackOverFlow에 질문을 올렸고... 일단은 할 수 있는 다른 것을 하도록 하자. 그 동안 해보지 않은 Firebase로 REST Api 호출하기! 회원가입이 가능해진다면, 그 뒤로 할 수 있는 것들이 빠르게 늘 것이다!