
작업 일정
- #40 [refactor] 화면 구성 리팩토링
- #26 [view] 소셜 가입 후 초기 설정
- #46 [notification] action에 대한 알림 보내기
다음으로 넘어가기 전에 기술 부채를 털고 가는 게 나을 것 같다고 판단했다. 로딩이 제대로 되지 않았을 때 다시 시도하는 버튼을 중간에 넣기 시작하면서 초반에는 작성한 코드에는 그게 빠져 있고 이후에 작성한 코드에는 일관성 없게 되어 있어 일관성 있는 코드로 정리하고 가는 게 나을 것 같았다.
그리고 렌더링 관점에서 비효율적으로 작성되어 있는 helper function이 몇 가지 발견되어 별도의 widget으로 빼기로 했다. 가볍게 넘어갈 수 있을 것 같아서 그렇게 했는데 생각보다 코드가 길어진 곳들이 좀 있더라.
소셜 로그인 기능 구현은 설정 화면 할 때 해놓을 거라 생각해서 [view] 태그를 붙였지만 소셜 로그인 기능부터 여기서 한다. 몇 가지 package를 설치해야 한다.
google_sign_in: Google OAuth 2.0 인증 토큰(idToken, accessToken) 획득sign_in_with_apple: Apple ID 토큰(identityToken) 및 권한 부여 코드 획득crypto: Apple 로그인 시 리플레이 공격(Replay Attack) 방지를 위한 SHA-256 Nonce 해싱에 필요
google_sign_in packgage는 버전 7로 넘어가면서 상당 부분 개편되었기 때문에 오래된 자료 참고 시 유의해야 한다는 모양이다.
소셜 로그인은 코드 구현뿐만 아니라 Firebase Console에서도 작업을 해주어야 한다.
- iOS용 Google 로그인 |
GoogleService-Info.plist를 다운로드 받아ios/Runner/GoogleService-Info.plist를 덮어씌운다.- Android용 Google 로그인 | 터미널에서 프로젝트의
android/디렉토리에 들어가./gradlew signingReport명령어를 통해 SHA-1 지문을 얻어내고, [프로젝트 설정 > 내 앱]의 [디지털 지문 추가]에 붙여 넣은 뒤google-services.json을 다운로드 받아android/app/google-services.json를 덮어씌운다.
- 터미널에 뜨는 여러 값들 중
Variant: debug라고 뜨는 영역에 있는SHA1을 사용하면 된다. 이건 디버그 키인데, 배포 시점에 릴리즈 키를 새로 발급받게 되겠지만 여러 개의 키를 등록할 수 있으므로 일단은 디버그 키를 넣어 두도록 한다. 애뮬레이터 실행 시 이 키를 사용하기 때문에 릴리즈 키만으로는 테스트를 할 수 없다나.- Google 로그인에는 SHA-1이 필수인데 그것과 별개로 SHA-1의 암호학적 충돌 취약점 우려로 인해 구글의 최신 보안 인프라는 더 강력한 SHA-256을 표준으로 요구하고 있기도 하니 SHA-256도 추가로 넣어주는 건 선택 사항.
- iOS용 Apple 로그인 |
ios/Runner.xcworkspace을 열어 루트Runner에서 [Targets] 에 있는Runner를 선택한 후, [Signing & Capabilities] 탭에서 [+ Capability] 버튼을 눌러 [Sign in with Apple] 더블클릭으로 추가한다.- Android용 Apple 로그인 | Apple Developer Console에서 Services ID를 등록하고 [Sign In with Apple] 를 활성화하여 [Configure]에서 Firebase 도메인을 등록한다. 저장 후 좌측 메뉴의 [Keys]에서 추가 버튼을 눌러 [Sign In with Apple] 를 위한 key를 생성한다. 10자리 Key ID를 잘 복사해 놓고
.p8파일을 다운로드한다. (다시 받을 수 없음 주의!!) 다시 Firebase Console로 돌아가 앞서 저장해 놓은 Service ID, Team ID(우상단 이름 부근에 뜨는 10자리), Key ID를 붙여 넣고, 다운로드된.p8파일의 본문도 비공개 키 부분에 붙여 넣는다.
소셜 로그인 구현 과정에서 Firestore Rules를 살짝 변경했다. 원래는 사용자의 username은 불변이라는 설정 때문에 /account_ids/{username} 문서에 대해 allow update: if false; 를 걸어 놨었는데, 이메일 연결 없이 소셜 로그인만 사용할 경우에는 email을 null로 두고 이메일을 연결하면 email을 업데이트하는 식으로 수정이 들어가게 되어 본인 계정에 한해 수정 권한을 주기로 했다.
소셜 가입 시 username과 nickname을 작성하는 화면을 추가하다 보니 계정 삭제 또한 아이디/비밀번호 로그인 기준으로 맞춰져 있다는 것을 인지했다. 연결되어 있는 로그인 제공업체가 하나일 경우에는 그 방식으로 제시하고 둘 이상일 경우에는 사용자가 계정 재인증에 사용할 수단을 선택하여 그 방식으로 인증하면 탈퇴를 할 수 있게 선택지를 주는 게 좋을 것 같다고 판단했다. 소셜 로그인도 최근 인증 세션이 있어야 탈퇴가 가능하므로 계정 삭제를 누르면 재인증 수단을 고르라고 한 뒤, 소셜 재인증이든 비밀번호 재인증이든 선택하여 수행하도록 작성하였다.
하면 할수록 edge-case가 발견되어 계속 수정하게 된다. 기본적으로 인증 수단이 하나밖에 없으면 그 유일한 인증 수단을 해제하지 못하는데, 이메일 인증을 아직 받지 않은 상태에서 소셜 로그인 해제가 가능하다거나. 이메일 인증 받기도 전에 그 이메일에 연결된 비밀번호로 계정 삭제를 시도할 수 있다거나. 탈퇴한 사용자의 댓글 처리 등 부가적인 요소들이 따라오더라. 그래도 이런 걸 외면하면 서비스의 질이 떨어지니까, 파악되는 것들은 최대한 잡고 가는 게 좋다.
알림 기능이 개념적으로는 존재하지만 친구 요청 및 수락, 응원, 댓글, 답글 등의 작업을 수행했을 때 실제로는 알림이 가지 않는 상태였다. Firebase를 통해 실제로 알림이 전달되도록 한다. 친구와 관련된 건 UseCase로 작성되어 있는데 피드와 관련된 건 ViewModel에 작성되어 있어 '알림을 어디에서 보내야 하는가'에 대한 일관성 이슈가 발생했다. 이를 해소하기 위해 피드와 관련된 것도 UseCase로 작성하기로 했다.
switch 문 안에 또 switch 문이 들어가는 경우가 발생하는데 이 때 case Ok(:final value): 의 value 가 겹치는 걸 어떻게 처리하나 했더니 case Ok(value: final customVariableName): 와 같이 할 수 있는 모양이다. 정확히는 property 이름을 동일하게 사용할 경우에만 앞 부분을 생략했던 거라고 하는 게 맞겠다.
알림을 보내는 것은 Firestore에 문서를 쌓는 것뿐이고 알림을 받는 건 그것과 별개의 구현이다. 인 앱 알림은 그냥 구현할 수 있지만 알림창에 뜨는 진짜 알림은 private key가 필요하기 때문에 모바일 기기 간에 직접 푸시 알림을 쏘지 못하므로 적절한 firebase package를 사용해야 한다. firebase_messaging package와 Firebase Cloud Functions을 통해 작업을 수행할 수 있다.
앱에 알림 권한을 부여한 사용자 기기 고유의 FCM(Firebase Cloude Messaging) 토큰을 발급받아 firestore에 저장해 놓고, 그 토큰을 통해 알림을 전송한다. 이 토큰은 로그아웃 시 지우거나 무효화하고 로그인 시 다시 저장하거나 활성화해야 잘못된 사용자에게 알림을 보내지 않을 수 있다. 이걸 빼먹을 경우 한 기기에서 여러 사용자 계정을 사용할 때 알림이 잘못 전송될 수 있다. 알림은 네이티브의 영역이므로 android/app/src/main/AndroidManifest.xml 파일과 ios/Runner/Info.plist 파일에 적절한 권한을 명시해 주어야 한다.
android/app/src/main/AndroidManifest.xml에 추가<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
ios/Runner/Info.plist에 추가<key>UIBackgroundModes</key> <array> <string>location</string> <string>fetch</string> <string>remote-notification</string> </array>
User 와 관련된 model, service, repository 전체적으로 FCM 토큰과 관련된 코드를 추가하고 알림 기능을 전담할 녀석을 새로 생성한다. 기존의 Notification 도메인이 알림을 생성하고 인앱 알림으로 보내주는 녀석이었다면 이번에 생성하는 PushNotification 도메인은 그렇게 생성된 알림을 Firebase Cloud Function으로부터 전달 받아 푸시 알림으로 내보내는 녀석이다.
앱이 종료되었거나 백그라운드 상태일 때 원격 푸시 메시지를 수신하는 최상위 엔트리포인트 핸들러는 백그라운드에서 알림에 대한 처리가 필요할 때 사용하는데, 그런 걸 구현하지 않을 경우에도 빈 핸들러로 등록해 두어야 네이티브 엔진 내부에서 알림을 제대로 처리한다고 한다. FlutterFire 라이브러리의 규격이니 안 쓸 거여도 작성을 해야 한다나. 알림에 대한 데이터를 로컬 DB에 쌓아두거나 [답장하기], [읽음 처리] 등이 포함된 커스텀 알림을 띄우고자 할 때 여기에 코드를 작성할 수 있다는 모양이다.
프로젝트 루트에서 Firebase Cloud Functions 환경을 셋업한다.
firebase init functions
JavaScript, TypeScript, Python 중에 선택할 수 있는데 TypeScript를 선택했다. 아무래도 타입 안정성이 확보되어 있는 편이 좋을 것 같다. 데이터 분석 관련된 게 필요하다면 Python이 충분히 매리트가 있지만 여기서는 굳이.
코드를 작성한 후에는 검증 및 빌드 후 Firebase 인프라에 배포해야 한다.
cd functions npm run lint npm run build cd .. firebase deploy --only functions
Functions 2세대는 Firestore의 이벤트를 수신하기 위해 내부적으로 Google Cloud의 Eventarc 및 Cloud Run을 사용하는데, Google Cloud의 보안 및 IAM 권한 시스템은 새로 생성된 서비스 계정의 권한이 분산 인프라 전역에 반영되기까지 약 1~3분의 전파 시간이 소요되기 때문에 최초 배포 시에는 권한 전파 지연으로 배포 실패가 뜰 수 있다고 한다. 그럴 땐 잠시 기다린 후 다시 시도하면 반영된다나.