맨땅에 코딩

'학습의 숲' 소소한 개발일지 - 회원 가입, 로그인, 로그아웃 본문

KAU 2023 (2학년)/객체지향프로그래밍

'학습의 숲' 소소한 개발일지 - 회원 가입, 로그인, 로그아웃

나는 푸딩 2024. 7. 25. 20:44

UserRepository

🍀 Firebase Realtime Database를 통해 사용자 정보를 가져오고, 사용자 정보를 데이터베이스에 저장하는 기능을 제공한다. 이를 통해 앱의 다른 부분에서 사용자 정보를 쉽게 관리할 수 있다.

 

package com.example.forestlearning.repository

import androidx.lifecycle.MutableLiveData
import com.example.forestlearning.UserData
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.ValueEventListener
import com.google.firebase.database.ktx.database
import com.google.firebase.ktx.Firebase

class UserRepository {
    // Firebase 데이터베이스 인스턴스 초기화
    val database = Firebase.database
    // Firebase 데이터베이스의 "Users" 레퍼런스에 대한 참조 생성
    private val usersRef = database.getReference("Users")

    // 사용자 이름을 가져오는 함수
    fun getName(uid: String, name: MutableLiveData<String>) {
        // "Users"에 있는 해당 사용자의 "name" 레퍼런스 가져오기
        val nameRef = usersRef.child(uid).child("name")
        nameRef.addValueEventListener(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                // 데이터 변경 이벤트가 발생하면, 이름을 업데이트
                val newName = snapshot.value.toString()
                name.postValue(newName)
            }
            override fun onCancelled(error: DatabaseError) {
                // 데이터 가져오기가 취소되었을 때의 처리
            }
        })
    }

    // 사용자 이메일을 가져오는 함수
    fun getEmail(uid: String, email: MutableLiveData<String>) {
        // "Users"에 있는 해당 사용자의 "email" 레퍼런스 가져오기
        val emailRef = usersRef.child(uid).child("email")
        emailRef.addValueEventListener(object : ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                // 데이터 변경 이벤트가 발생하면, 이메일을 업데이트
                val newEmail = snapshot.value.toString()
                email.postValue(newEmail)
            }
            override fun onCancelled(error: DatabaseError) {
                // 데이터 가져오기가 취소되었을 때의 처리
            }
        })
    }

    // 데이터베이스에 사용자 정보를 저장하는 함수
    fun postUser(name: String, email: String, uId: String) {
        // 사용자 데이터 생성
        val user = UserData(name, email, uId)
        // "Users"에 사용자 데이터 저장
        usersRef.child(uId).setValue(user)
    }
}

 

  1. database: Firebase Realtime Database의 인스턴스다. 이를 통해 데이터베이스에 접근할 수 있다.

  2. usersRef: "Users"라는 이름의 데이터베이스 레퍼런스를 가리킨다. 이를 통해 "Users" 아래에 저장된 사용자 정보에 접근할 수 있다.

  3. getName(): 특정 사용자의 이름을 가져오는 함수다. 사용자의 UID를 파라미터로 받아 해당 사용자의 이름 데이터에 대한 레퍼런스를 가져온다. 이 레퍼런스에 ValueEventListener를 추가하여 데이터가 변경될 때마다 이름을 업데이트한다.

  4. getEmail(): 특정 사용자의 이메일을 가져오는 함수다. 사용자의 UID를 파라미터로 받아 해당 사용자의 이메일 데이터에 대한 레퍼런스를 가져온다. 이 레퍼런스에 ValueEventListener를 추가하여 데이터가 변경될 때마다 이메일을 업데이트한다.

  5. postUser(): 데이터베이스에 사용자 정보를 저장하는 함수다. 사용자의 이름, 이메일, UID를 파라미터로 받아 UserData 객체를 생성하고, 이를 "Users" 레퍼런스 아래에 저장한다.

UserViewModel

🍀 사용자의 정보를 LiveData로 관리하고, 이 정보를 Firebase Realtime Database와 동기화하는 역할을 한다. 이를 통해 앱의 다른 부분에서 사용자 정보를 쉽게 관찰하고 업데이트할 수 있다.

 

package com.example.forestlearning.viewmodel

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.example.forestlearning.repository.UserRepository
import com.google.firebase.database.DataSnapshot
import com.google.firebase.database.DatabaseError
import com.google.firebase.database.FirebaseDatabase
import com.google.firebase.database.ValueEventListener

class UserViewModel : ViewModel() {
    // UserRepository 인스턴스 생성
    private val repository = UserRepository()

    // 사용자 이메일을 저장하는 MutableLiveData 생성
    private val _email = MutableLiveData("")
    val email: LiveData<String> get() = _email

    // 사용자 이름을 저장하는 MutableLiveData 생성
    private val _name = MutableLiveData("")
    val name: LiveData<String> get() = _name

    // 사용자 UID를 저장하는 MutableLiveData 생성
    private val _uid = MutableLiveData("")
    val uid: LiveData<String> get() = _uid

    // 사용자 정보를 설정하고 Firebase Realtime Database에 저장하는 함수
    fun setUser(name: String, email: String, uid: String) {
        // 사용자 정보가 변경되었을 경우에만 Firebase Realtime Database에 저장
        if (_name.value != name || _email.value != email || _uid.value != uid) {
            _name.value = name
            _email.value = email
            _uid.value = uid
            repository.postUser(name, email, uid)
        }
    }

    // Firebase Realtime Database로부터 사용자 정보를 가져오는 함수
    fun fetchUser(uid: String) {
        // Firebase Realtime Database로부터 사용자 정보를 가져오기
        val db = FirebaseDatabase.getInstance()
        db.getReference("Users").child(uid).addListenerForSingleValueEvent(object:
            ValueEventListener {
            override fun onDataChange(snapshot: DataSnapshot) {
                // Firebase Realtime Database에서 가져온 사용자 정보를 LiveData에 저장
                _name.value = snapshot.child("name").value.toString()
                _email.value = snapshot.child("email").value.toString()
                _uid.value = uid
                // UserRepository를 통해 Firebase Realtime Database에 사용자 이름과 이메일 저장
                repository.getName(uid, _name)
                repository.getEmail(uid, _email)
            }

            override fun onCancelled(error: DatabaseError) {
                // 데이터 가져오기 취소되었을 때의 처리
            }
        })
    }
}

 

  1. repository: UserRepository의 인스턴스다. 이를 통해 사용자 정보를 Firebase Realtime Database에 저장하거나 가져올 수 있다.

  2. _email, _name, _uid: 사용자의 이메일, 이름, UID를 저장하는 MutableLiveData다. MutableLiveData는 값이 변경될 수 있는 LiveData다. LiveData는 데이터의 변경을 관찰하고 UI를 업데이트하는데 사용된다.

  3. email, name, uid: _email, _name, _uid의 getter다. 이를 통해 UserViewModel 외부에서는 LiveData를 읽기 전용으로 접근할 수 있다.

  4. setUser(): 사용자 정보를 설정하고 Firebase Realtime Database에 저장하는 함수다. 사용자의 이름, 이메일, UID를 파라미터로 받아 해당 정보가 변경되었을 경우에만 MutableLiveData와 Firebase Realtime Database에 저장한다.

  5. fetchUser(): Firebase Realtime Database로부터 사용자 정보를 가져오는 함수다. 사용자의 UID를 파라미터로 받아 해당 사용자의 정보를 Firebase Realtime Database에서 가져오고, 이를 MutableLiveData에 저장한다.

SingInFragment

🍀 사용자가 입력한 이름, 이메일, 비밀번호를 Firebase에 등록하여 회원 가입을 구현하고, 회원 가입이 성공적으로 이루어지면 이메일 인증 메일을 전송하는 로직을 가지고 있다. Firebase Authentication 서비스의 특성을 활용하여 사용자 인증을 쉽게 구현할 수 있도록 한다.

 

package com.example.forestlearning

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.example.forestlearning.databinding.FragmentSignInBinding
import com.example.forestlearning.viewmodel.UserViewModel
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase

class SignInFragment : Fragment() {

    private var binding: FragmentSignInBinding? = null
    private val mAuth : FirebaseAuth = Firebase.auth
    val viewModel: UserViewModel by activityViewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentSignInBinding.inflate(inflater, container, false)

        binding?.signInEndButton?.setOnClickListener {
            val name = binding?.editName?.text.toString().trim()
            val email = binding?.editEmail?.text.toString().trim()
            val password = binding?.editPassword?.text.toString().trim()
            signIn(name, email, password)
        }

        binding?.signInNoButton?.setOnClickListener {
            findNavController().navigate(R.id.action_signInFragment2_to_loginFragment2)
        }

        return binding?.root
    }

    //회원 가입 함수
    private fun signIn(name: String, email: String, password: String) {
        mAuth.createUserWithEmailAndPassword(email, password)?.addOnCompleteListener { task ->
            if (task.isSuccessful) {
                mAuth.currentUser?.sendEmailVerification()?.addOnCompleteListener { sendTask ->
                    if (sendTask.isSuccessful) {
                        //회원가입 성공시 실행
                        Toast.makeText(requireContext(), "회원가입에 성공했습니다. 이메일 인증 후 로그인이 가능합니다.", Toast.LENGTH_SHORT).show()
                        viewModel.setUser(name, email, mAuth.currentUser?.uid ?: "")
                        findNavController().navigate(R.id.action_signInFragment2_to_loginFragment2)
                    } else {
                        Toast.makeText(requireContext(), "이메일 전송에 실패했습니다.", Toast.LENGTH_SHORT).show()
                        findNavController().navigate(R.id.action_signInFragment2_to_loginFragment2)
                    }
                }
            } else {
                Toast.makeText(requireContext(), "회원가입에 실패했습니다.", Toast.LENGTH_SHORT).show()
            }
        }?.addOnFailureListener {
            Toast.makeText(requireContext(), "회원가입에 실패했습니다.", Toast.LENGTH_SHORT).show()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        binding = null
    }

}

 

  1. binding: FragmentSignInBinding 인스턴스를 가지고 있으며, 이를 통해 레이아웃에 정의된 뷰들에 접근할 수 있다.

  2. mAuth: Firebase Authentication의 인스턴스다. 이를 통해 Firebase와의 인증 관련 작업을 수행할 수 있다.

  3. viewModel: UserViewModel의 인스턴스를 가지고 있으며, 이를 통해 사용자 데이터를 관리한다.

  4. onCreateView(): 프래그먼트의 뷰가 처음 생성될 때 호출되는 메소드이다. 여기서 레이아웃을 inflate하고, 버튼의 클릭 리스너를 설정한다.

  5. signInEndButton: 회원 가입을 완료하는 버튼이다. 클릭하면 입력된 이름, 이메일, 비밀번호를 가져와 signIn() 함수를 호출하게 된다.

  6. signInNoButton: 회원 가입을 취소하는 버튼이다. 클릭하면 LoginFragment로 이동한다.

  7. signIn(): 회원 가입 로직을 수행하는 함수이다. createUserWithEmailAndPassword() 함수를 통해 Firebase에 사용자를 등록하고, 성공적으로 등록되면 이메일 인증 메일을 전송한다. 이메일 인증 메일 전송이 성공하면 UserViewModel에 사용자 정보를 저장하고, LoginFragment로 이동한다.

  8. onDestroy(): 프래그먼트가 소멸될 때 호출되는 메소드이다. 여기서 binding을 null로 설정하여 메모리 누수를 방지한다.

Authentication

🍀 Firebase Authentication을 통해 로그인 상태를 관리하고, 현재 로그인한 사용자의 이메일을 저장하는 역할을 한다. 이를 통해 앱의 다른 부분에서 현재 로그인 상태를 쉽게 확인하고, 로그인한 사용자의 정보를 가져올 수 있다.

 

package com.example.forestlearning

import androidx.multidex.MultiDexApplication
import com.google.firebase.auth.FirebaseAuth
import com.google.firebase.auth.ktx.auth
import com.google.firebase.ktx.Firebase

class Authentication: MultiDexApplication() {
    //companion 블록 = 클래스의 인스턴스를 생성하지 않고도 접근할 수 있는 정적 멤버를 정의하는데 사용
    companion object {
        var auth: FirebaseAuth = Firebase.auth

        //로그인 확인 여부로 사용하는 email 변수
        var email: String? = null

        //로그인 여부를 확인하는 함수
        fun checkLogin(): Boolean {
            //firebase에 등록한 사용자 정보 불러오기
            val currentUser = auth.currentUser
            return currentUser?.let {
                //유저 정보가 있으면 email 가져오기
                email = currentUser.email

                //로그인 되어있는 경우
                currentUser.isEmailVerified
            } ?: let {
                false
            }
        }
    }
}

 

  1. MultiDexApplication: Android의 멀티덱스 지원을 위한 기본 애플리케이션 클래스이다. 멀티덱스를 사용하는 이유는, 앱이 참조하는 메소드의 총 수가 65,536개를 초과하는 경우 발생하는 문제를 해결하기 위함이다.

  2. auth: Firebase Authentication의 인스턴스이다. 이를 통해 Firebase와의 인증 관련 작업을 수행할 수 있다.

  3. email: 현재 로그인한 사용자의 이메일을 저장하는 변수다. 이 변수를 통해 어느 사용자가 로그인했는지 알 수 있다.

  4. checkLogin(): 현재 사용자의 로그인 상태를 체크하는 함수다. auth.currentUser를 통해 현재 로그인한 사용자의 정보를 가져올 수 있다. 만약 로그인한 사용자가 있고, 그 사용자가 이메일 인증을 완료한 상태라면 true를 반환하며, 그렇지 않다면 false를 반환한다.

  5. Companion object: 코틀린에서는 static 키워드가 없는 대신 companion object를 사용하여 정적 멤버를 정의할 수 있다. 이를 통해 인스턴스를 생성하지 않고도 해당 멤버들에 접근할 수 있다.

LoginFragment

🍀 사용자가 입력한 이메일과 비밀번호로 Firebase에 로그인을 시도하고, 로그인이 성공적으로 이루어지면 이메일 인증을 확인한 후 사용자 정보를 UserViewModel에 저장하는 로직을 가지고 있다. Firebase Authentication 서비스의 특성을 활용하여 사용자 인증을 쉽게 구현할 수 있도록 한다.

 

package com.example.forestlearning

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.example.forestlearning.databinding.FragmentLoginBinding
import com.example.forestlearning.Authentication.Companion.auth
import com.example.forestlearning.viewmodel.UserViewModel
import com.google.firebase.auth.FirebaseAuth

class LoginFragment : Fragment() {

    private var binding: FragmentLoginBinding? = null
    val viewModel: UserViewModel by activityViewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentLoginBinding.inflate(inflater, container, false)

        //회원가입 버튼을 누르면 회원가입 화면으로 이동
        binding?.signInButton?.setOnClickListener {
            findNavController().navigate(R.id.action_loginFragment2_to_signInFragment2)
        }

        //로그인 버튼 이벤트
        binding?.loginButton?.setOnClickListener {
            val email = binding?.inputEmail?.text.toString().trim()
            val password = binding?.inputPassword?.text.toString().trim()

            if (binding?.inputEmail?.text?.isEmpty() == false && binding?.inputPassword?.text?.isEmpty() == false) {
                login(email, password)
            } else {
                Toast.makeText(requireContext(), "이메일과 비밀번호를 입력해주세요.", Toast.LENGTH_SHORT).show()
            }
        }

        return binding?.root
    }

    //로그인 함수
    private fun login(email: String, password: String) {
        auth.signInWithEmailAndPassword(email, password).addOnCompleteListener { task ->
            if (task.isSuccessful) {
                // 로그인 성공시 실행
                if (Authentication.checkLogin()) {
                    // 이메일 인증이 된 경우
                    Authentication.email = email
                    // 현재 로그인된 사용자의 UID 가져오기
                    val currentUser = FirebaseAuth.getInstance().currentUser
                    val uid = currentUser?.uid
                    uid?.let { viewModel.fetchUser(it) } // 뷰모델에 개인정보 저장

                    Toast.makeText(requireContext(), "로그인에 성공했습니다.", Toast.LENGTH_SHORT).show()
                    findNavController().navigate(R.id.action_loginFragment2_to_mainActivity)
                } else{
                    // 이메일 인증이 안 된 경우
                    Toast.makeText(requireContext(), "이메일 인증에 실패했습니다.", Toast.LENGTH_SHORT).show()
                }
            } else {
                // 로그인 실패시 실행
                Toast.makeText(requireContext(), "로그인에 실패했습니다.", Toast.LENGTH_SHORT).show()
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        binding = null
    }
}

 

  1. binding: FragmentLoginBinding 인스턴스를 가지고 있으며, 이를 통해 레이아웃에 정의된 뷰들에 접근할 수 있다.

  2. viewModel: UserViewModel의 인스턴스를 가지고 있으며, 이를 통해 사용자 데이터를 관리한다.

  3. onCreateView(): 프래그먼트의 뷰가 처음 생성될 때 호출되는 메소드이다. 여기서 레이아웃을 inflate하고, 버튼의 클릭 리스너를 설정한다.

  4. signInButton: 회원 가입 화면으로 이동하는 버튼이다. 클릭하면 SignInFragment로 이동한다.

  5. loginButton: 로그인을 시도하는 버튼입니다. 클릭하면 입력된 이메일과 비밀번호를 가져와 login() 함수를 호출한다.

  6. login(): 로그인 로직을 수행하는 함수이다. signInWithEmailAndPassword() 함수를 통해 Firebase에 로그인을 시도하고, 성공적으로 로그인되면 UserViewModel에 사용자 정보를 저장하고, MainActivity로 이동한다.

  7. onDestroyView(): 프래그먼트의 뷰가 소멸될 때 호출되는 메소드이다. 여기서 binding을 null로 설정하여 메모리 누수를 방지한다.

LogoutFragment

🍀 사용자가 로그아웃 버튼을 누르면 Firebase Authentication의 signOut() 메소드가 호출되어 사용자를 로그아웃시키고, Navigation 컴포넌트의 navigate() 메소드를 호출하여 HostActivity로 이동한다. 이 과정에서 ViewModel을 사용하여 사용자 데이터를 관리하고, Data Binding을 사용하여 UI를 업데이트한다.

 

package com.example.forestlearning

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.example.forestlearning.databinding.FragmentLogoutBinding
import com.example.forestlearning.viewmodel.UserViewModel

class LogoutFragment : Fragment() {

    private var binding: FragmentLogoutBinding? = null
    val viewModel: UserViewModel by activityViewModels()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // FragmentLogoutBinding을 인플레이트하여 binding 객체를 생성
        binding = FragmentLogoutBinding.inflate(inflater, container, false)

        // 현재 로그인한 사용자의 ID를 가져오기
        val uid = Authentication.auth.currentUser?.uid
        // uid가 null이 아니라면, 해당 uid의 사용자 정보를 가져오기
        if (uid != null) {
            viewModel.fetchUser(uid)
        }

        // 사용자 이름이 변경될 때마다 텍스트 뷰의 내용을 업데이트
        viewModel.name.observe(viewLifecycleOwner){ newName ->
            binding?.txtName?.text = "$newName 님"
        }

        // 로그아웃 버튼을 클릭하면 로그아웃 처리를 하고, 로그인 화면으로 이동
        binding?.logoutButton?.setOnClickListener {
            Authentication.auth.signOut()
            Authentication.email = null
            findNavController().navigate(R.id.action_logoutFragment_to_hostActivity)
        }

        return binding?.root
    }

    override fun onDestroy() {
        super.onDestroy()
        binding = null
    }
}

 

  1. 변수 선언:
    • binding: FragmentLogoutBinding 타입의 변수로, 이 변수를 통해 레이아웃에 있는 뷰에 접근할 수 있다.
    • viewModel: UserViewModel을 activityViewModels()를 통해 가져오며, 이 ViewModel은 activity 수명 주기를 따른다. 이는 여러 Fragment가 같은 ViewModel 인스턴스를 공유할 수 있게 해준다.
  2. onCreateView(): Fragment의 뷰가 처음 생성될 때 호출되는 메소드입니다. 레이아웃 XML을 인플레이트하고, 사용자 데이터를 가져오며, 로그아웃 버튼의 클릭 리스너를 설정한다.
    • FragmentLogoutBinding.inflate(): XML 레이아웃 파일을 인플레이트하여 binding 객체를 생성한다.
    • Authentication.auth.currentUser?.uid: 현재 로그인한 사용자의 ID를 가져온다.
    • viewModel.fetchUser(uid): 사용자 ID를 이용해 해당 사용자의 정보를 가져온다.
    • viewModel.name.observe(): LiveData 객체인 viewModel.name의 변경을 관찰한다. 사용자 이름이 변경될 때마다 UI를 업데이트한다.
    • logoutButton.setOnClickListener(): 로그아웃 버튼이 클릭되면 사용자를 로그아웃시키고 HostActivity로 이동한다.
  3. onDestroy(): Fragment가 파괴될 때 호출되는 메소드이다. 여기서는 binding 객체를 null로 설정하여 메모리 누수를 방지한다.