이번에는 Firebase 세팅을 마치고 Firebase Auth 함수를 이용해 로그인, 회원가입, 프로필 변경 기능을 직접 구현해볼 예정이다. 메인(홈) 화면에서 하단에 배치된 네비게이션의 마이 탭을 눌렀을 때 현재 로그인 상태에 따라 로그인 화면 혹은 프로필을 확인할 수 있는 마이 탭 화면을 표시한다.
회원가입 화면은 로그인 화면에서 비밀번호 하단에 배치된 회원가입의 텍스트 버튼을 눌렀을 때 진입할 수 있는 화면이다. 첫 번째 단계로는 이메일을 입력하고 두 번째 단계로는 비밀번호를 입력하도록 하였습니다. 모두 입력을 마치면 회원가입이 완료되었다는 텍스트와 함께 다시 로그인 화면으로 돌아가도록 구현하였다. 초기에는 각 단계마다 유효성 검사를 할 수 있도록 구현하고 싶었으나 Firebase에서 제공하는 함수로 각 단계마다 유효성 검사를 할 수 없어 입력한 이메일과 비밀번호가 모두 유효한 값이라는 가정 하에 구현하였다.
SignUpViewModel 클래스에는 다음과 같이 구성하였다. 입력할 이메일과 비밀번호, 그리고 현재 입력 단계에 대한 상태를 관리하기 위한 StateFlow 변수를 선언하여 관리하였다. signUp 함수는 Firebase auth의 createUserWithEmailAndPassword 함수, 입력한 이메일과 비밀번호를 통해 회원가입을 할 수 있도록 구현하였다.
로그인 화면은 마이 탭에서 현재 사용자가 로그인하지 않은 상태에서 보여지는 화면이다. 로그인에 성공하면 홈 화면으로 다시 이동한다. 로그인에 실패할 경우 로그인 버튼 상단에 로그인 실패에 대한 메시지를 표시한다.
LoginViewModel은 로그인할 이메일과 비밀번호, 로그인 실패시 표시할 에러 메시지, 로그인 성공 여부를 StateFlow로 관리하도록 구현하였다. login 함수는 Firebase auth의 signWithEmailAndPassword 함수, 입력한 이메일과 패스워드를 통해 로그인을 시도할 수 있도록 구현하였다. 여기에 성공, 실패 시 리스너를 각각 설정하여 실패시에는 에러 메시지를, 성공시에는 로그인 성공 여부를 true로 설정하도록 하였다.
로그인에 성공하게 되면 자동적으로 Firebase가 사용자 정보를 앱 내에 저장하고 있어서 앱을 종료한 후에도 이전에 사용자 계정 정보를 바탕으로 로그인되어 있는 상태에서 앱을 사용할 수 있는 것 같았다. 그래서 따로 자동 로그인을 구현하지는 않았다.
사용자가 로그인한 상태에서 마이 탭을 눌렀을 때 사용자의 프로필 이미지와 닉네임을 포함한 프로필을 확인할 수 있도록 하였다. 닉네임 옆에 배치되어 있는 펜 모양의 버튼을 눌렀을 때 닉네임을 수정할 수 있게 하였다. 프로필 이미지를 눌렀을 때 프로필 이미지를 포함한 다이얼로그가 표시되고 다이얼로그 내의 프로필 이미지를 눌렀을 때 앨범 내 사진에 접근하여 프로필 이미지를 변경할 수 있도록 구현하였다.
닉네임 우측에 펜 버튼을 누르게 되면 닉네임을 새로 설정할 수 있는 화면이 나타나게 하고, 닉네임을 새로 입력하여 변경할 수 있도록 하였다. Firebase에서는 사용자마다 고유한 닉네임이 아니라 단순한 display name이기 때문에 중복성 체크 없이 닉네임을 변경할 수 있도록 하였다.
Firebase의 auth 객체의 currentUser를 바탕으로 user를 관리하고 user의 displayName으로 nickname을 관리하였다. updateNickname 함수는 새로 설정할 닉네임과 updateProfile을 바탕으로 닉네임 변경을 시도할 수 있도록 구현하였다. 각각의 리스너를 통해 성공 시에는 viewModel의 nickname 변수 값을 변경하고 실패 시에는 UI로 표시할 에러 메시지 값을 변경한다.
프로필 이미지는 현재 프로필 이미지를 눌렀을 때 다이얼로그를 통해서 변경할 수 있도록 하였다. 현재 사용자의 설정된 이미지를 관리하는 profileImage 변수를 관리하고 닉네임 변경과 같은 방식으로 updateProfileImage 함수를 통해 변경할 이미지로 프로필 이미지를 변경할 수 있도록 구현하였다.
firebase에 저장된 url을 바탕으로 이미지를 로드하기 위해서 이미지 로드 라이브러리가 필요했다. 그래서 coil이라는 경량화되어 있는 이미지 로드 라이브러리를 사용하였다. compose 기반 안드로이드 앱에서도 상당히 간단하게 로드할 이미지 UI를 생성할 수 있기 때문에 사용하기 편하다.
앨범 내 사진에 접근하기 위해서는 접근 권한이 필요하다. XML 기반 앱에서는 필요한 권한 요청과 함께 권한 승인 시 적절한 처리를 하도록 구현해보았지만 Compose 기반에서는 어떻게 해야 할 지 모르겠어서 찾아보았다. Compose 기반 UI에서 권한 요청과 동작 처리를 쉽게 할 수 있도록 Accompanist에서 개발한 Permissions 라이브러리가 개발된 걸 확인할 수 있었다. 그래서 Accompanist의 Permissions 라이브러리를 사용해서 Compose 기반에서 권한 요청과 동작 처리를 구현해보았다.
https://google.github.io/accompanist/permissions/
accompanist에서 제안하는 Permission WorkFlow
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun FeatureThatRequiresCameraPermission() {
// Camera permission state
val cameraPermissionState = rememberPermissionState(
android.Manifest.permission.CAMERA
)
if (cameraPermissionState.status.isGranted) {
Text("Camera permission Granted")
} else {
Column {
val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
// If the user has denied the permission but the rationale can be shown,
// then gently explain why the app requires this permission
"The camera is important for this app. Please grant the permission."
} else {
// If it's the first time the user lands on this feature, or the user
// doesn't want to be asked again for this permission, explain that the
// permission is required
"Camera permission required for this feature to be available. " +
"Please grant the permission"
}
Text(textToShow)
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("Request permission")
}
}
}
}
각 안드로이드 버전마다 앨범의 사진에 접근하는 권한이 다르다. 그래서 각 버전에 따라 permissionState의 권한을 다르게 설정하였다. 현재 permissionState의 상태가 승인된 상태라면 activityResult의 Launcher인 result를 launch해서 selectedImage에 선택한 이미지의 uri 값을 할당하도록 하였다. 승인되지 않은 상태라면 permissionState의 launchPermssionRequest 함수를 호출하여 권한 요청을 할 수 있도록 하였다.
UI를 구성하면서 아직 원하는대로 커스텀하게 UI를 만드는 게 쉽지 않음을 느꼈고, 앱을 사용하면서 처리해야 할 동작들을 잘 처리하기 위해서는 여러 연습들이 필요함을 느꼈다.
질문이나 피드백 주실 게 있으시면 부담없이 댓글로 적어주시면 감사하겠습니다!