VidDish를 개발하던 중, 앱에는 익명 사용자로 로그인돼 있는데 레시피 검색과 탐색 요청이 거절되는 일이 있었습니다. 당시 개발 환경의 App Check 디버그 토큰이 어긋나 있었지만 앱은 “로그인이 필요합니다”라고 안내했습니다. 다시 로그인해도 디버그 토큰 설정은 고쳐지지 않습니다. 사용자가 할 수 없는 행동을 해결책처럼 보여 준 셈입니다.
이 글은 그 안내를 왜 바꿨고, 같은 unauthenticated 오류가 왔을 때 무엇을 확인하도록 정했는지 설명합니다. 개발 환경에서 확인한 사례와 코드·테스트를 바탕으로 하며, 운영 사용자에게 같은 장애가 얼마나 발생했는지는 확인하지 않았습니다. 대상은 Flutter 앱에서 Firebase HTTPS callable 함수를 호출하는 경우입니다.
로그인 상태인데 왜 로그인 오류가 나올까?
앱의 currentUser는 클라이언트가 알고 있는 로그인 상태입니다. 서버가 이번 요청의 토큰을 정상적으로 검증했다는 보장은 아닙니다. HTTPS callable 요청에는 사용 가능한 Firebase Auth와 App Check 토큰이 첨부될 수 있고, 서버는 사용자 인증과 앱 확인을 각각 처리합니다. 로그인은 돼 있어도 App Check 확인에서 막힐 수 있습니다. Firebase callable 안내, Cloud Functions App Check 안내
문제는 거절 이유가 사용자에게 같은 unauthenticated 코드로 보일 수 있다는 점입니다. 처음 구현은 그 코드만 보고 로그인 안내를 띄웠습니다. 검색과 탐색 모두 같은 방식이어서, 앱 확인이 실패해도 두 화면에서 사용자에게 로그인을 요구했습니다.
| 확인한 상황 | 사용자에게 필요한 안내 |
|---|---|
| 서버 함수가 로그인하지 않은 요청을 직접 거절함 | 로그인이 필요하다고 안내 |
| 서버가 요청 토큰을 검증하는 단계에서 거절함 | 로그인 문제로 단정하지 않고 요청 확인 실패를 안내 |
두 번째 경우를 무조건 App Check 실패라고 부를 수도 없습니다. 유효하지 않은 Auth 토큰 등 다른 검증 문제도 가능하기 때문입니다. 그래서 사용자 안내와 개발자의 원인 진단을 분리해야 했습니다.
왜 오류 코드만으로 판단하지 않았나
unauthenticated를 전부 로그인 문제로 처리하면, 이미 로그인한 사용자에게 소용없는 재로그인을 권하게 됩니다. 반대로 로그인 사유가 없다는 이유만으로 전부 App Check 문제라고 표시하면 원인을 또 잘못 단정할 수 있습니다.
VidDish에서는 서버 함수가 직접 “로그인이 필요하다”고 판단한 경우에만 응답에 reason: auth_required를 붙이기로 했습니다. 이것은 Firebase가 모든 함수에 자동으로 제공하는 표준 사유가 아니라 앱에서 정한 약속입니다. 클라이언트는 이 사유가 있을 때만 로그인 안내를 보여 줍니다. 사유가 없는 unauthenticated는 더 넓은 요청 검증 실패로 다룹니다.
이 선택은 오류의 정확한 원인을 클라이언트에서 모두 알아내기 위한 방법이 아닙니다. 확실하지 않은 상황에서 틀린 행동을 권하지 않기 위한 기준입니다. App Check 검증이 함수 본문보다 먼저 요청을 거절하면 우리가 정한 로그인 사유를 붙일 기회도 없다는 점을 함께 고려했습니다.
검색과 탐색의 안내를 함께 바꿨다
서버가 직접 로그인 거절을 할 때 사유를 붙이고, 앱에는 그 사유를 읽는 공통 분류를 두었습니다. 검색 화면만 바꾸면 같은 오류가 탐색 화면에서는 여전히 로그인 문제로 보일 수 있어 두 경로에 같은 판단을 적용했습니다. 관련 서버 함수 중 로그인 거절 사유를 빠뜨린 곳도 함께 확인했습니다.
바뀐 안내는 다음과 같습니다.
| 요청 결과 | 이전 안내 | 변경 후 안내 |
|---|---|---|
서버가 auth_required를 명시함 |
로그인 필요 | 로그인 필요 |
| 같은 오류 코드지만 로그인 사유가 없음 | 로그인 필요 | 연결·요청 확인 실패 |
현재 앱의 두 번째 안내는 네트워크 확인과 앱 재실행을 권합니다. 일반적인 재시도 방법이지만, 개발 환경의 디버그 토큰 등록이 틀렸다면 재실행만으로 해결되지는 않습니다. 개발자는 실패한 시각의 함수·SDK 로그와 App Check 설정을 확인해야 합니다. 사용자에게 노출할 문구와 개발자가 조사할 원인을 같은 것으로 취급하지 않는 이유입니다.
비슷한 상황이라면 이렇게 확인한다
- 로그인이 준비된 뒤 호출했는지 봅니다. 로그인 직전의 요청이라면 먼저 호출 순서를 고칩니다.
currentUser가 있다는 사실만으로 서버 검증 성공을 확정하지는 않습니다. - 앱과 함수가 같은 Firebase 프로젝트를 쓰는지 확인합니다. 개발·운영 환경, 함수 이름과 리전, 빌드 모드를 구분합니다. 이 값들이 틀리면 다른 오류가 날 수도 있으므로
unauthenticated의 단일 원인으로 묶지 않습니다. - 디버그 빌드라면 App Check 설정을 봅니다. 개발용 공급자 활성화와 해당 Firebase 프로젝트의 디버그 토큰 등록을 확인합니다. 운영 빌드에서는 운영 공급자와 앱 등록을 확인해야 하며, 장애를 감추려고 App Check 강제를 끄는 것으로 끝내지 않습니다. Flutter 디버그 공급자 안내
- 서버가 명시한 사유와 로그를 함께 봅니다.
auth_required가 있으면 함수가 직접 로그인 필요로 거절한 경로를 확인합니다. 사유가 없다면 App Check만으로 단정하지 말고 같은 시각의 인증 검증 로그를 대조합니다. ID 토큰, App Check 토큰 원문이나 사용자 입력 전체를 로그·문의 글에 붙이지 않습니다.
무엇을 확인했고, 무엇은 아직 확인하지 못했나
검색과 탐색의 저장소 테스트에서는 서버가 auth_required를 보낸 경우와 사유 없이 unauthenticated를 보낸 경우가 서로 다른 오류 안내로 분류되는 것을 확인했습니다. 이는 테스트 대역으로 전달한 오류에 대한 앱의 판단을 확인한 결과입니다. 실제 기기에서 App Check 등록이 성공했거나, 운영 환경의 모든 인증 오류가 해결됐다는 증거는 아닙니다.
자신의 앱에 적용한다면 실제 개발 기기에서도 로그인된 정상 요청, 서버가 직접 거절한 미로그인 요청, App Check 등록이 잘못된 요청을 나눠 확인해야 합니다. 서버 함수가 여러 개라면 로그인 거절 사유를 같은 기준으로 붙였는지도 확인해야 합니다. 이 글의 기준은 HTTPS callable 함수에 대한 것이며 일반 HTTP 함수나 Firestore 보안 규칙 오류에 그대로 옮길 수는 없습니다.
로그인한 사용자에게 다시 로그인하라고 말하기 전에, 요청을 어느 단계에서 거절했는지부터 확인하세요. 오류 안내를 고치는 일은 장애 자체를 해결하는 일과 다르지만, 적어도 사용자를 잘못된 해결책으로 보내지 않게 합니다.