이번 포스팅에서는 Firebase의 Realtime Database와 Storage를 이용해 유저의 정보와 프로필 이미지를 저장하고 불러오는 방법에 대해 알아보도록 하겠습니다.
① 파이어베이스 콘솔에 접속해 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)
}
⑧ 코드를 실행시킨 후 회원가입을 진행하면, 유저의 정보가 파이어베이스의 Realtime Database에 저장되는 것을 확인할 수 있다.
① 먼저 데이터베이스에서 가져온 유저의 정보를 저장할 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 메서드 정의부 최하단에 이 메서드를 호출해주면 된다.
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
}
}
⑧ 코드를 실행시켜보면 이미지 카드에 유저의 정보가 잘 적용되었음을 확인할 수 있다.
① 파이어베이스 콘솔에서 빌드 > 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/*")
}
※ Activity Result Contract
액티비티 결과 컨트랙트란, Android 앱에서 액티비티 간의 상호작용을 단순화하고 개선하기 위한 API로, 액티비티 결과 처리를 더 쉽고 직관적으로 만들기 위해 도입한 개념이다. 이미지 선택, 파일 열기, 사진 촬영 등의 작업을 처리하기 위한 액티비티 결과 컨트랙트가 이미 정의되어있기 때문에, 결과 처리를 위한 콜백 메서드만 정의하면 된다.
④ 코드를 실행시킨 후 회원가입 화면에서 프로필 이미지를 클릭해보면, 휴대폰의 "내 파일"에 저장되어 있는 모든 이미지가 나타날 것이다. 그 중 원하는 이미지를 선택하여 본인의 프로필 이미지로 사용할 수 있다.
① 이제 프로필에 등록된 사진을 Storage에 업로드하는 메서드를 정의하자. JoinActivity의 onCreate 메서드 외부에 작성하면 된다. 자세한 사용법은 아래의 공식 문서를 참조하기 바란다.
>> Storage 관련 파이어베이스 공식 문서
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 ->
}
}
② 만약 사진이 업로드 되지 않는다면 Storage의 보안 규칙을 확인해보자. 읽고 쓰기가 허용이 안 되어 있다면, 아래와 같이 규칙을 변경해야 한다.
③ 회원가입이 완료되었을 때 이미지를 Storage에 저장할 수 있도록, addOnCompleteListener의 if문에 아래의 내용을 추가한다.
val userInfo = UserInfo(uid, nickname, gender, region, age)
FirebaseRef.userInfo.child(uid).setValue(userInfo)
uploadImage(uid)
④ 코드를 실행시켜보자. 회원가입 시 등록한 이미지가 파이어베이스의 Storage에 잘 저장되어야 한다.
① 파이어베이스 이메일/비밀번호 사용자 인증에는 "유효한 이메일의 형식(@, .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 버튼을 누르면 아래와 같이 아무런 이미지도 없는 상태가 된다. 바로 이 상태에 대한 예외처리인 것이다.
① 파이어베이스의 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 이미지에 프로필 사진이 잘 나타나는 것을 확인할 수 있을 것이다.