소개팅 앱 3. 유저 정보 활용하기

변현섭·2023년 8월 23일
0

이번 포스팅에서는 Firebase의 Realtime Database와 Storage를 이용해 유저의 정보와 프로필 이미지를 저장하고 불러오는 방법에 대해 알아보도록 하겠습니다.

1. 데이터베이스에 유저 정보 저장하기

① 파이어베이스 콘솔에 접속해 Realtime Database를 생성한다.

  • 이번에는 잠금모드로 시작을 선택해보자.

② Realtime Database의 규칙 탭에 들어가 read와 write 권한을 모두 허용(true)해주자.

③ Realtime Database를 사용하기 위해서는 아래의 의존성을 Module 수준 gradle 파일의 dependencies에 추가해주어야 한다.

implementation("com.google.firebase:firebase-database-ktx")

④ 우리가 저장해야 할 유저의 정보는 닉네임, 성별, 지역, 나이, UID이다. JoinActivity 파일을 아래와 같이 수정하자.

private lateinit var auth: FirebaseAuth

private var nickname = ""
private var gender = ""
private var region = ""
private var age = ""
private var uid = ""

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_join)
    auth = Firebase.auth
    
    val joinBtn = findViewById<Button>(R.id.join)
    joinBtn.setOnClickListener {
        val email = findViewById<TextInputEditText>(R.id.email)
        val password = findViewById<TextInputEditText>(R.id.password)
        nickname = findViewById<TextInputEditText>(R.id.nickname).text.toString()
        gender = findViewById<TextInputEditText>(R.id.gender).text.toString()
        region = findViewById<TextInputEditText>(R.id.region).text.toString()
        age = findViewById<TextInputEditText>(R.id.age).text.toString()
        
        auth.createUserWithEmailAndPassword(email.text.toString(), password.text.toString())
            .addOnCompleteListener(this) { task ->
                if (task.isSuccessful) {
                    Log.d("JoinActivity","회원가입 완료")
                    uid = FirebaseAuthUtils.getUid()
                    
                    val intent = Intent(this, MainActivity::class.java)
                    startActivity(intent)
                } else {
                    Log.d("JoinActivity","회원가입 실패")
                }
            }
    }
}

⑤ 유저의 정보를 Realtime Database에 저장하기 위해 Data Class를 만들자. auth 디렉토리 하위로 UserInfo 클래스를 생성한다.

data class UserInfo (
    val uid : String? = null,
    val nickname : String? = null,
    val gender : String? = null,
    val region : String? = null,
    val age : String? = null
)

⑥ 이번에는 utils 패키지 하위로 FirebaseRef라는 Kotlin 클래스를 생성하여 Realtime Database에 값을 저장하기 위한 정적 멤버 함수를 만들어보자.

class FirebaseRef {
    companion object {
        val database = Firebase.database
        val userInfo = database.getReference("userInfo")
    }
}

⑦ JoinActivity 파일의 addOnCompleteListener 메서드의 if 문을 아래와 같이 수정한다.

if (task.isSuccessful) {
    Log.d("JoinActivity","회원가입 완료")
    uid = FirebaseAuthUtils.getUid()
    val userInfo = UserInfo(uid, nickname, gender, region, age)
    
    FirebaseRef.userInfo.child(uid).setValue(userInfo)
    val intent = Intent(this, MainActivity::class.java)
    startActivity(intent)
}
  • userInfo라는 Reference(root 기준 첫번째 자식 노드)의 자식 노드는 uid를 key값으로 갖고, userInfo라는 Data Class를 value로 갖는다.

⑧ 코드를 실행시킨 후 회원가입을 진행하면, 유저의 정보가 파이어베이스의 Realtime Database에 저장되는 것을 확인할 수 있다.

2. 유저 정보 가져오기

① 먼저 데이터베이스에서 가져온 유저의 정보를 저장할 List를 생성한다. MainActivity 파일의 onCreate 메서드 앞에 아래의 내용을 추가하자.

private val userInfoList = mutableListOf<UserInfo>()

② MainActivity에 데이터베이스에서 값을 가져오는 메서드를 정의하자. onCreate 메서드 외부에 작성해야 한다.

private fun getUserInfoList() {
    val postListener = object : ValueEventListener {
        override fun onDataChange(dataSnapshot: DataSnapshot) {
            for(dataModel in dataSnapshot.children) {
                val userInfo = dataModel.getValue(UserInfo::class.java)
                userInfoList.add(userInfo!!)
            } 
            cardStackAdapter.notifyDataSetChanged()
        }
        
        override fun onCancelled(databseError: DatabaseError) {
            Log.w("MainActivity", "onCancelled", databseError.toException())
        }
    }
    FirebaseRef.userInfo.addValueEventListener(postListener)
}

③ 이제 기존에 만들었던 initList와 더미데이터는 주석처리하고, CardStackAdapter에 userInfoList를 넣는다.

cardStackAdapter = CardStackAdapter(baseContext, userInfoList)

④ 이후 CardStackAdapter의 입력인자와 item의 자료형을 String에서 UserInfo로 변경해주자.

class CardStackAdapter(val context : Context, val items: MutableList<UserInfo>) : RecyclerView.Adapter<CardStackAdapter.ViewHolder>() {
	...
	inner class ViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView) {
    fun bindItems(item : UserInfo) {

⑤ 이제 onCreate 메서드 정의부 최하단에 이 메서드를 호출해주면 된다.

  • Adapter가 이미 뷰를 만든 이후에 getUserInfoList를 호출하기 때문에 반드시 Adapater를 동기화하는 로직이 getUserInfoList 메서드 안에 포함되어 있어야 한다.
  • cardStackAdapter.notifyDataSetChanged()가 Adapter 동기화를 위해 작성되었다.
getUserInfoList()

⑥ 메인화면의 카드뷰 이미지에 유저의 정보가 연결되게 만들자. 먼저 item_card.xml 파일의 TextView의 id 속성을 지정하자.

android:id="@+id/itemNickname"
android:id="@+id/itemAge"
android:id="@+id/itemRegion"

⑦ CardStackAdapter의 ViewHolder 클래스를 아래와 같이 수정한다.

inner class ViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView) {
    val nickname = itemView.findViewById<TextView>(R.id.nickname)
    val age = itemView.findViewById<TextView>(R.id.age)
    val region = itemView.findViewById<TextView>(R.id.region)
    
    fun bindItems(item : UserInfo) {
        nickname.text = item.nickname
        age.text = item.age
        region.text = item.region
    }
}

⑧ 코드를 실행시켜보면 이미지 카드에 유저의 정보가 잘 적용되었음을 확인할 수 있다.

3. 이미지 처리하기

1) 이미지 가져오기

① 파이어베이스 콘솔에서 빌드 > Strage를 클릭한다. Storage 시작하기 버튼을 클릭한다.

  • 프로덕션 모드로 생성한다.

② Strage를 사용하려면, Module 수준의 build.gradle 파일의 dependencies에 아래의 의존성을 추가해야 한다.

implementation("com.google.firebase:firebase-storage-ktx")

③ JoinActivity에서 프로필 이미지를 클릭하면 휴대폰에 저장된 사진을 불러올 수 있도록 만들어보자. onCreate 메서드를 아래와 같이 수정하자.

lateinit var profileImage : ImageView

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_join)
    auth = Firebase.auth
    profileImage = findViewById(R.id.profile)
    
    val getAction = registerForActivityResult(
        ActivityResultContracts.GetContent(),
        ActivityResultCallback { uri ->
            profileImage.setImageURI(uri)
        }
    )
    
    profileImage.setOnClickListener {
        getAction.launch("image/*")
    }
  • registerForActivityResult: 액티비티 결과를 처리하는 콜백을 등록한다. 여기서는 이미지 선택이라는 사용자의 action을 처리하고, 그에 대한 결과(이미지 uri)를 콜백 메서드에 전달하고 있다.
  • ActivityResultContracts.GetContent(): 사용자로부터 이미지 컨텐츠를 입력받기 위한 액티비티 결과 컨트랙트이다.
  • 액티비티 결과 컨트랙트에서 전달해준 uri를 받아, profileImage의 URI로 설정한다.
  • getAction.launch("image/*"): 프로필 이미지를 클릭했을 때, 사용자가 이미지를 선택할 수 있는 액티비티가 화면에 노출된다.

※ Activity Result Contract
액티비티 결과 컨트랙트란, Android 앱에서 액티비티 간의 상호작용을 단순화하고 개선하기 위한 API로, 액티비티 결과 처리를 더 쉽고 직관적으로 만들기 위해 도입한 개념이다. 이미지 선택, 파일 열기, 사진 촬영 등의 작업을 처리하기 위한 액티비티 결과 컨트랙트가 이미 정의되어있기 때문에, 결과 처리를 위한 콜백 메서드만 정의하면 된다.

④ 코드를 실행시킨 후 회원가입 화면에서 프로필 이미지를 클릭해보면, 휴대폰의 "내 파일"에 저장되어 있는 모든 이미지가 나타날 것이다. 그 중 원하는 이미지를 선택하여 본인의 프로필 이미지로 사용할 수 있다.

  • AVD에서 테스트해보고 싶은 경우, 이미지를 직접 다운로드 받아야 한다.
  • AVD를 켜고 Google에 들어가 본인이 원하는 이미지를 선택한 후 좌클릭을 꾹 누르면 Download image 버튼이 생긴다.
  • 다양한 이미지를 다운로드 받아 본인의 프로필 사진으로 등록해보자.

2) 이미지 저장하기

① 이제 프로필에 등록된 사진을 Storage에 업로드하는 메서드를 정의하자. JoinActivity의 onCreate 메서드 외부에 작성하면 된다. 자세한 사용법은 아래의 공식 문서를 참조하기 바란다.
>> Storage 관련 파이어베이스 공식 문서

  • 어떤 유저의 프로필 이미지인지 파일명으로 식별하기 위해, 파일명을 {uid}.jpeg로 짓겠다.
  • 아래의 코드는 대충 어떤 뜻이지만 이해하면 충분하다.
private fun uploadImage(uid: String) {
    val storage = Firebase.storage
    val storageRef = storage.reference.child(uid + ".jpeg")
    profileImage.isDuplicateParentStateEnabled = true
    profileImage.buildDrawingCache()
    
    val bitmap = (profileImage.drawable as BitmapDrawable).bitmap
    val baos = ByteArrayOutputStream()
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos)
    
    val data = baos.toByteArray()
    var uploadTask = storageRef.putBytes(data)
    uploadTask.addOnFailureListener{
    }.addOnSuccessListener { taskSnapshot ->
        
    }
}
  • Firebase.storage: Firebase의 Storage 서비스를 사용하기 위해 Firebase.storage 인스턴스를 생성한다.
  • storage.reference.child(uid + ".jpeg"): 업로드할 이미지를 저장할 Firebase Storage의 경로를 지정한다.
  • isDuplicateParentStateEnabled: 해당 뷰의 상태가 부모 뷰의 상태와 중복될 때, 해당 뷰에 중복 상태를 적용할지 여부를 결정한다. 예를 들어, Button 안에 TextView가 있는 상태에서 Button이 포커스되면 TextView도 포커스 된 걸로 인식한다는 뜻이다.
  • buildDrawingCache(): 드로잉 캐시(안드로이드에서 뷰의 그래픽 데이터를 비트맵 형태로 캐싱하는 메커니즘)를 빌드하여 이미지를 불러온다.
  • (profileImage.drawable as BitmapDrawable).bitmap: profileImage의 Drawable을 BitmapDrawable로 캐스팅하여, Bitmap을 가져온다. 즉, 이미지를 비트맵 형태로 얻고 있는 것이다.
  • ByteArrayOutputStream(): 바이트 배열 형식으로 데이터를 저장하기 위한 ByteArrayOutputStream을 생성한다.
  • bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos): 비트맵을 JPEG 형식으로 압축한다. 100은 압축 품질(최대 품질)을 나타낸다.
  • baos.toByteArray(): ByteArrayOutputStream에서 바이트 배열 데이터를 가져온다.
  • storageRef.putBytes(data): storageRef 경로에 바이트 데이터를 업로드한다.
  • addOnFailureListener: 업로드 작업이 실패한 이벤트에 대한 리스너이다.
  • addOnSuccessListener: 업로드 작업이 성공한 이벤트에 대한 리스너이다.

② 만약 사진이 업로드 되지 않는다면 Storage의 보안 규칙을 확인해보자. 읽고 쓰기가 허용이 안 되어 있다면, 아래와 같이 규칙을 변경해야 한다.

③ 회원가입이 완료되었을 때 이미지를 Storage에 저장할 수 있도록, addOnCompleteListener의 if문에 아래의 내용을 추가한다.

val userInfo = UserInfo(uid, nickname, gender, region, age)
FirebaseRef.userInfo.child(uid).setValue(userInfo)
uploadImage(uid)

④ 코드를 실행시켜보자. 회원가입 시 등록한 이미지가 파이어베이스의 Storage에 잘 저장되어야 한다.

4. 예외 처리하기

① 파이어베이스 이메일/비밀번호 사용자 인증에는 "유효한 이메일의 형식(@, .com 등)"과 "6자리 이상의 비밀번호"를 입력해야 한다는 기본 조건이 존재한다.

② 하지만, 그 외의 다른 예외는 직접 처리해주어야 한다. 예외 처리는 백엔드에서도 많이 해보았기 때문에 간단히만 짚고 넘어가겠다.

③ 먼저 비밀번호 확인란을 activity_join.xml 파일에 추가하자.

<com.google.android.material.textfield.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="10dp"
    app:counterMaxLength="12"
    app:counterEnabled="true">
    
    <com.google.android.material.textfield.TextInputEditText
        android:id="@+id/passwordChk"
        android:hint="password check"
        android:inputType="textPassword"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
        
</com.google.android.material.textfield.TextInputLayout>

④ JoinActivity의 joinBtn의 setOnClickListener를 아래와 같이 수정하자. 처리해주어야 할 예외 상황은 아래와 같다.

  • 프로필 이미지를 등록하지 않았을 때(기본 이미지조차 선택하지 않았을 때)
  • 이메일, 비밀번호, 닉네임, 성별, 지역, 나이 비어있을 때
  • 비밀번호 글자 수 제한을 넘겼을 때
  • 비밀번호와 비밀번호 확인에 입력된 값이 다를 때
  • 나이에 숫자가 아닌 문자가 입력되었을 때
joinBtn.setOnClickListener {
    val email = findViewById<TextInputEditText>(R.id.email)
    val password = findViewById<TextInputEditText>(R.id.password)
    val passwordChk = findViewById<TextInputEditText>(R.id.passwordChk)
    
    if(email.text.toString().isEmpty()) {
        Toast.makeText(this, "이메일을 입력해주세요", Toast.LENGTH_SHORT).show()
    }
    
    else if(password.text.toString().isEmpty()) {
        Toast.makeText(this, "비밀번호를 입력해주세요", Toast.LENGTH_SHORT).show()
    }
    else if(password.text.toString().length > 12) {
        Toast.makeText(this, "비밀번호는 12자 이내로만 입력할 수 있습니다.", Toast.LENGTH_SHORT).show()
    }
    
    else if(password.text.toString() != passwordChk.text.toString()) {
        Toast.makeText(this, "비밀번호와 비밀번호 확인의 입력 값이 다릅니다.", Toast.LENGTH_SHORT).show()
    }
    
    else {
        try {
            nickname = findViewById<TextInputEditText>(R.id.nickname).text.toString()
            gender = findViewById<TextInputEditText>(R.id.gender).text.toString()
            region = findViewById<TextInputEditText>(R.id.region).text.toString()
            age = findViewById<TextInputEditText>(R.id.age).text.toString()
            
            if(nickname.isEmpty()) {
                Toast.makeText(this, "닉네임을 입력해주세요", Toast.LENGTH_SHORT).show()
            }
            
            else if(gender.isEmpty()) {
                Toast.makeText(this, "성별을 입력해주세요", Toast.LENGTH_SHORT).show()
            }
            
            else if(region.isEmpty()) {
                Toast.makeText(this, "지역을 입력해주세요", Toast.LENGTH_SHORT).show()
            }
            
            else if(age.isEmpty()) {
                Toast.makeText(this, "나이를 입력해주세요", Toast.LENGTH_SHORT).show()
            }
            
            else {
                age.toInt() // 나이에 숫자 값이 들어왔는지 확인
                auth.createUserWithEmailAndPassword(email.text.toString(), password.text.toString())
                    .addOnCompleteListener(this) { task ->
                        if (task.isSuccessful) {
                            Log.d("JoinActivity","회원가입 완료")
                            uid = FirebaseAuthUtils.getUid()
                            val userInfo = UserInfo(uid, nickname, gender, region, age)
                            FirebaseRef.userInfo.child(uid).setValue(userInfo)
                            uploadImage(uid)
                            val intent = Intent(this, MainActivity::class.java)
                            startActivity(intent)
                        } else {
                            Toast.makeText(this, "이메일 형식이 잘못되었거나, 비밀번호가 6자리 미만입니다.", Toast.LENGTH_SHORT).show()
                        }
                    }
            }
        } catch (e: NumberFormatException) {
            Toast.makeText(this, "나이 입력 칸에는 숫자만 입력할 수 있습니다.", Toast.LENGTH_SHORT).show()
        }
    }
}

참고로, "프로필 이미지를 등록하지 않았을 때"라는 것은 기본 이미지조차 선택되지 않았을 때이다. 프로필 이미지를 한번 선택한 후, 다시 선택할 때 back 버튼을 누르면 아래와 같이 아무런 이미지도 없는 상태가 된다. 바로 이 상태에 대한 예외처리인 것이다.

5. 데이터베이스에서 이미지 불러오기

① 파이어베이스의 Storage, Realtime Database, Authentication의 모든 내용을 삭제한다.

② 이미지를 불러오기 위해 Glide 라이브러리를 사용하자. 아래의 의존성을 Module 수준의 build.gradle의 dependencies에 추가한다.

implementation("com.github.bumptech.glide:glide:4.12.0")
annotationProcessor("com.github.bumptech.glide:compiler:4.12.0")

③ Module 수준의 build.gradle의 defaultConfig에 아래의 내용을 추가한다.

  • 이는 다양한 라이브러리를 사용하면서 생길 수 있는 컴파일 오류를 방지하기 위한 설정이다.
multiDexEnabled = true

④ 인터넷 접속을 허용하기 위해 AndroidManifest.xml 파일에 아래의 내용을 추가한다.

<uses-permission android:name="android.permission.INTERNET"/>

⑤ CardStackAdapter에 ViewHolder 클래스를 아래와 같이 수정한다.

inner class ViewHolder(itemView : View) : RecyclerView.ViewHolder(itemView) {
    val nickname = itemView.findViewById<TextView>(R.id.itemNickname)
    val age = itemView.findViewById<TextView>(R.id.itemAge)
    val region = itemView.findViewById<TextView>(R.id.itemRegion)
    val image = itemView.findViewById<ImageView>(R.id.profileImage)
    
    fun bindItems(item : UserInfo) {
        val storageRef = Firebase.storage.reference.child(item.uid + ".jpeg")
        storageRef.downloadUrl.addOnCompleteListener(OnCompleteListener { task ->
            if(task.isSuccessful) {
                Glide.with(context)
                    .load(task.result)
                    .into(image)
            }
        })
        
        nickname.text = item.nickname
        age.text = item.age
        region.text = item.region
    }
}

⑥ 이제 코드를 실행시켜보면, Card View 이미지에 프로필 사진이 잘 나타나는 것을 확인할 수 있을 것이다.

profile
Java Spring, Android Kotlin, Node.js, ML/DL 개발을 공부하는 인하대학교 정보통신공학과 학생입니다.

0개의 댓글