1. 개요
목표
이 Codelab에서는 Cloud Firestore를 기반으로 Android에서 레스토랑 추천 앱을 빌드합니다. 다음을 수행하는 방법을 배우게 됩니다.
- Android 앱에서 Firestore에 데이터 읽기 및 쓰기
- 실시간으로 Firestore 데이터 변경사항 수신 대기
- Firebase 인증 및 보안 규칙을 사용하여 Firestore 데이터 보호
- 복잡한 Firestore 쿼리 작성
기본 요건
이 Codelab을 시작하기 전에 다음을 갖추었는지 확인하세요.
- Android 스튜디오 Flamingo 이상
- API 19 이상을 사용하는 Android Emulator
- Node.js 버전 16 이상
- Java 버전 17 이상
2. Firebase 프로젝트 만들기
- Google 계정으로 Firebase Console에 로그인합니다.
- Firebase Console에서 프로젝트 추가를 클릭합니다.
- 아래 스크린샷과 같이 Firebase 프로젝트의 이름(예: 'Friendly Eats')을 입력하고 계속을 클릭합니다.
- Google 애널리틱스를 사용 설정하라는 메시지가 표시될 수 있지만 이 Codelab의 목적상 선택은 중요하지 않습니다.
- 1분 정도 후에 Firebase 프로젝트가 준비됩니다. 계속을 클릭합니다.
3. 샘플 프로젝트 설정
코드 다운로드
다음 명령어를 실행하여 이 Codelab의 샘플 코드를 클론합니다. 이렇게 하면 컴퓨터에 friendlyeats-android
라는 폴더가 생성됩니다.
$ git clone https://github.com/firebase/friendlyeats-android
컴퓨터에 git이 없는 경우 GitHub에서 직접 코드를 다운로드할 수도 있습니다.
Firebase 구성 추가
- Firebase Console의 왼쪽 탐색 메뉴에서 프로젝트 개요를 선택합니다. Android 버튼을 클릭하여 플랫폼을 선택합니다. 패키지 이름을 입력하라는 메시지가 표시되면
com.google.firebase.example.fireeats
사용
- 앱 등록을 클릭하고 안내에 따라
google-services.json
파일을 다운로드한 후 방금 다운로드한 코드의app/
폴더로 이동합니다. 그런 다음 다음을 클릭합니다.
프로젝트 가져오기
Android 스튜디오를 엽니다. File > New > Import Project를 클릭하고 friendlyeats-android 폴더를 선택합니다.
4. Firebase 에뮬레이터 설정
이 Codelab에서는 Firebase 에뮬레이터 도구 모음을 사용하여 Cloud Firestore 및 기타 Firebase 서비스를 로컬에서 에뮬레이션합니다. 이를 통해 앱을 빌드할 수 있는 안전하고 빠르며 무료인 로컬 개발 환경을 제공합니다.
Firebase CLI 설치
먼저 Firebase CLI를 설치해야 합니다. macOS 또는 Linux를 사용하는 경우 다음 cURL 명령어를 실행할 수 있습니다.
curl -sL https://firebase.tools | bash
Windows를 사용하는 경우 설치 안내를 읽고 독립형 바이너리를 가져오거나 npm
를 통해 설치하세요.
CLI를 설치한 후 firebase --version
를 실행하면 9.0.0
이상의 버전이 보고됩니다.
$ firebase --version 9.0.0
로그인
firebase login
를 실행하여 CLI를 Google 계정에 연결합니다. 새 브라우저 창이 열리고 로그인 절차가 완료됩니다. 앞서 Firebase 프로젝트를 만들 때 사용한 것과 동일한 계정을 선택해야 합니다.
프로젝트 연결
friendlyeats-android
폴더 내에서 firebase use --add
를 실행하여 로컬 프로젝트를 Firebase 프로젝트에 연결합니다. 메시지에 따라 이전에 만든 프로젝트를 선택하고 별칭을 선택하라는 메시지가 표시되면 default
를 입력합니다.
5. 앱 실행
이제 Firebase 에뮬레이터 도구 모음과 FRIENDEats Android 앱을 처음으로 실행해 보겠습니다.
에뮬레이터 실행
터미널에서 friendlyeats-android
디렉터리 내에서 firebase emulators:start
를 실행하여 Firebase 에뮬레이터를 시작합니다. 다음과 같은 로그가 표시됩니다.
$ firebase emulators:start i emulators: Starting emulators: auth, firestore i firestore: Firestore Emulator logging to firestore-debug.log i ui: Emulator UI logging to ui-debug.log ┌─────────────────────────────────────────────────────────────┐ │ ✔ All emulators ready! It is now safe to connect your app. │ │ i View Emulator UI at http://localhost:4000 │ └─────────────────────────────────────────────────────────────┘ ┌────────────────┬────────────────┬─────────────────────────────────┐ │ Emulator │ Host:Port │ View in Emulator UI │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Authentication │ localhost:9099 │ http://localhost:4000/auth │ ├────────────────┼────────────────┼─────────────────────────────────┤ │ Firestore │ localhost:8080 │ http://localhost:4000/firestore │ └────────────────┴────────────────┴─────────────────────────────────┘ Emulator Hub running at localhost:4400 Other reserved ports: 4500 Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
이제 머신에서 완전한 로컬 개발 환경이 실행되었습니다. Codelab의 나머지 부분에서는 이 명령어를 계속 실행해야 합니다. Android 앱을 에뮬레이터에 연결해야 합니다.
에뮬레이터에 앱 연결
Android 스튜디오에서 util/FirestoreInitializer.kt
및 util/AuthInitializer.kt
파일을 엽니다. 이 파일에는 애플리케이션 시작 시 Firebase SDK를 머신에서 실행 중인 로컬 에뮬레이터에 연결하는 로직이 포함되어 있습니다.
FirestoreInitializer
클래스의 create()
메서드에서 다음 코드를 살펴봅니다.
// Use emulators only in debug builds
if (BuildConfig.DEBUG) {
firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
}
BuildConfig
를 사용하여 앱이 debug
모드에서 실행 중일 때만 에뮬레이터에 연결합니다. release
모드에서 앱을 컴파일하면 이 조건은 false가 됩니다.
useEmulator(host, port)
메서드를 사용하여 Firebase SDK를 로컬 Firestore 에뮬레이터에 연결하는 것을 확인할 수 있습니다. 앱 전체에서 FirebaseUtil.getFirestore()
를 사용하여 이 FirebaseFirestore
인스턴스에 액세스하므로 debug
모드에서 실행할 때는 항상 Firestore 에뮬레이터에 연결됩니다.
앱 실행
google-services.json
파일을 올바르게 추가했다면 이제 프로젝트가 컴파일됩니다. Android 스튜디오에서 Build > Rebuild Project를 클릭하고 남은 오류가 없는지 확인합니다.
Android 스튜디오에서 Android Emulator로 앱을 실행합니다. 처음에는 '로그인' 화면이 표시됩니다. 이메일과 비밀번호를 사용하여 앱에 로그인할 수 있습니다. 이 로그인 프로세스에서 Firebase 인증 에뮬레이터에 연결 중이므로 실제 사용자 인증 정보가 전송되지 않습니다.
이제 웹브라우저에서 http://localhost:4000으로 이동하여 에뮬레이터 UI를 엽니다. 그런 다음 Authentication 탭을 클릭하면 방금 만든 계정이 표시됩니다.
로그인 절차가 완료되면 앱 홈 화면이 표시됩니다.
곧 홈 화면에 채울 데이터가 추가될 예정입니다.
6. Firestore에 데이터 쓰기
이 섹션에서는 현재 비어 있는 홈 화면을 채울 수 있도록 Firestore에 일부 데이터를 작성합니다.
앱의 기본 모델 객체는 레스토랑입니다(model/Restaurant.kt
참고). Firestore 데이터는 문서, 컬렉션, 하위 컬렉션으로 분할됩니다. 각 식당을 "restaurants"
라는 최상위 컬렉션에 문서로 저장합니다. Firestore 데이터 모델에 대해 자세히 알아보려면 문서에서 문서 및 컬렉션에 관해 읽어보세요.
데모를 위해 오버플로 메뉴에서 'Add Random Items'(무작위 항목 추가) 버튼을 클릭하면 10개의 레스토랑을 무작위로 생성하는 기능을 앱에 추가합니다. MainFragment.kt
파일을 열고 onAddItemsClicked()
메서드의 콘텐츠를 다음으로 바꿉니다.
private fun onAddItemsClicked() {
val restaurantsRef = firestore.collection("restaurants")
for (i in 0..9) {
// Create random restaurant / ratings
val randomRestaurant = RestaurantUtil.getRandom(requireContext())
// Add restaurant
restaurantsRef.add(randomRestaurant)
}
}
위의 코드와 관련해 유의해야 할 몇 가지 중요한 사항은 다음과 같습니다.
- 먼저
"restaurants"
컬렉션에 대한 참조를 가져옵니다. 컬렉션은 문서가 추가될 때 암시적으로 생성되므로 데이터를 쓰기 전에 컬렉션을 만들 필요가 없었습니다. - 각 레스토랑 문서를 만드는 데 사용하는 Kotlin 데이터 클래스를 사용하여 문서를 만들 수 있습니다.
add()
메서드는 자동 생성된 ID로 컬렉션에 문서를 추가하므로 각 레스토랑의 고유 ID를 지정할 필요가 없었습니다.
이제 앱을 다시 실행하고 'Add Random Items(무작위 항목 추가)'를 클릭합니다. 버튼을 클릭하여 방금 작성한 코드를 호출합니다.
이제 웹브라우저에서 http://localhost:4000으로 이동하여 에뮬레이터 UI를 엽니다. 그런 다음 Firestore 탭을 클릭하면 방금 추가한 데이터가 표시됩니다.
이 데이터는 100% 사용자 컴퓨터에 로컬로 저장됩니다. 실제로는 실제 프로젝트에 아직 Firestore 데이터베이스가 포함되어 있지 않습니다. 즉, 이 데이터를 수정하고 삭제해도 아무런 결과가 없으므로 안전하게 실험할 수 있습니다.
수고하셨습니다. 방금 데이터를 Firestore에 작성했습니다. 다음 단계에서는 이 데이터를 앱에 표시하는 방법을 알아봅니다.
7. Firestore의 데이터 표시
이 단계에서는 Firestore에서 데이터를 검색하여 앱에 표시하는 방법을 알아봅니다. Firestore에서 데이터를 읽는 첫 번째 단계는 Query
를 만드는 것입니다. MainFragment.kt
파일을 열고 onViewCreated()
메서드의 시작 부분에 다음 코드를 추가합니다.
// Firestore
firestore = Firebase.firestore
// Get the 50 highest rated restaurants
query = firestore.collection("restaurants")
.orderBy("avgRating", Query.Direction.DESCENDING)
.limit(LIMIT.toLong())
이제 일치하는 모든 문서를 가져오고 향후 업데이트에 관한 알림을 실시간으로 받을 수 있도록 쿼리를 리슨하려고 합니다. 최종 목표는 이 데이터를 RecyclerView
에 결합하는 것이므로 데이터를 수신 대기할 RecyclerView.Adapter
클래스를 만들어야 합니다.
이미 부분적으로 구현된 FirestoreAdapter
클래스를 엽니다. 먼저 Firestore 쿼리 업데이트를 수신할 수 있도록 어댑터에서 EventListener
를 구현하도록 하고 onEvent
함수를 정의합니다.
abstract class FirestoreAdapter<VH : RecyclerView.ViewHolder>(private var query: Query?) :
RecyclerView.Adapter<VH>(),
EventListener<QuerySnapshot> { // Add this implements
// ...
// Add this method
override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {
// Handle errors
if (e != null) {
Log.w(TAG, "onEvent:error", e)
return
}
// Dispatch the event
if (documentSnapshots != null) {
for (change in documentSnapshots.documentChanges) {
// snapshot of the changed document
when (change.type) {
DocumentChange.Type.ADDED -> {
// TODO: handle document added
}
DocumentChange.Type.MODIFIED -> {
// TODO: handle document changed
}
DocumentChange.Type.REMOVED -> {
// TODO: handle document removed
}
}
}
}
onDataChanged()
}
// ...
}
초기 로드 시 리스너는 새 문서마다 하나의 ADDED
이벤트를 수신합니다. 시간이 지남에 따라 쿼리의 결과 집합이 변경되면 리스너는 변경사항이 포함된 더 많은 이벤트를 수신합니다. 이제 리스너 구현을 완료하겠습니다. 먼저 onDocumentAdded
, onDocumentModified
, onDocumentRemoved
라는 세 가지 새 메서드를 추가합니다.
private fun onDocumentAdded(change: DocumentChange) {
snapshots.add(change.newIndex, change.document)
notifyItemInserted(change.newIndex)
}
private fun onDocumentModified(change: DocumentChange) {
if (change.oldIndex == change.newIndex) {
// Item changed but remained in same position
snapshots[change.oldIndex] = change.document
notifyItemChanged(change.oldIndex)
} else {
// Item changed and changed position
snapshots.removeAt(change.oldIndex)
snapshots.add(change.newIndex, change.document)
notifyItemMoved(change.oldIndex, change.newIndex)
}
}
private fun onDocumentRemoved(change: DocumentChange) {
snapshots.removeAt(change.oldIndex)
notifyItemRemoved(change.oldIndex)
}
그런 다음 onEvent
에서 다음과 같은 새 메서드를 호출합니다.
override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {
// Handle errors
if (e != null) {
Log.w(TAG, "onEvent:error", e)
return
}
// Dispatch the event
if (documentSnapshots != null) {
for (change in documentSnapshots.documentChanges) {
// snapshot of the changed document
when (change.type) {
DocumentChange.Type.ADDED -> {
onDocumentAdded(change) // Add this line
}
DocumentChange.Type.MODIFIED -> {
onDocumentModified(change) // Add this line
}
DocumentChange.Type.REMOVED -> {
onDocumentRemoved(change) // Add this line
}
}
}
}
onDataChanged()
}
마지막으로 startListening()
메서드를 구현하여 리스너를 연결합니다.
fun startListening() {
if (registration == null) {
registration = query.addSnapshotListener(this)
}
}
이제 앱이 Firestore에서 데이터를 읽도록 완전히 구성되었습니다. 앱을 다시 실행하면 이전 단계에서 추가한 레스토랑이 표시됩니다.
이제 브라우저의 에뮬레이터 UI로 돌아가서 식당 이름 중 하나를 수정합니다. 앱에서 변경되는 것을 거의 즉시 확인할 수 있습니다.
8. 데이터 정렬 및 필터링
현재 앱은 전체 컬렉션에서 평점이 가장 높은 레스토랑을 표시하지만 실제 레스토랑 앱에서는 사용자가 데이터를 정렬하고 필터링하려고 할 것입니다. 예를 들어 앱은 '필라델피아 최고의 해산물 레스토랑'을 표시할 수 있어야 합니다. 또는 '가장 비싼 피자' 같은 검색어를 사용할 수 있습니다.
앱 상단의 흰색 막대를 클릭하면 필터 대화상자가 표시됩니다. 이 섹션에서는 Firestore 쿼리를 사용하여 이 대화상자를 작동시킵니다.
MainFragment.kt
의 onFilter()
메서드를 수정해 보겠습니다. 이 메서드는 필터 대화상자의 출력을 캡처하기 위해 만든 도우미 객체인 Filters
객체를 허용합니다. 필터에서 쿼리를 구성하도록 이 메서드를 변경합니다.
override fun onFilter(filters: Filters) {
// Construct query basic query
var query: Query = firestore.collection("restaurants")
// Category (equality filter)
if (filters.hasCategory()) {
query = query.whereEqualTo(Restaurant.FIELD_CATEGORY, filters.category)
}
// City (equality filter)
if (filters.hasCity()) {
query = query.whereEqualTo(Restaurant.FIELD_CITY, filters.city)
}
// Price (equality filter)
if (filters.hasPrice()) {
query = query.whereEqualTo(Restaurant.FIELD_PRICE, filters.price)
}
// Sort by (orderBy with direction)
if (filters.hasSortBy()) {
query = query.orderBy(filters.sortBy.toString(), filters.sortDirection)
}
// Limit items
query = query.limit(LIMIT.toLong())
// Update the query
adapter.setQuery(query)
// Set header
binding.textCurrentSearch.text = HtmlCompat.fromHtml(
filters.getSearchDescription(requireContext()),
HtmlCompat.FROM_HTML_MODE_LEGACY
)
binding.textCurrentSortBy.text = filters.getOrderDescription(requireContext())
// Save filters
viewModel.filters = filters
}
위 스니펫에서는 지정된 필터와 일치하도록 where
및 orderBy
절을 연결하여 Query
객체를 빌드합니다.
앱을 다시 실행하고 다음 필터를 선택하여 가장 인기 있는 저가 식당을 표시합니다.
이제 저가 옵션만 포함된 필터링된 레스토랑 목록이 표시됩니다.
여기까지 완료했다면 이제 Firestore에서 완전히 작동하는 레스토랑 추천 보기 앱을 빌드한 것입니다. 이제 실시간으로 음식점을 정렬하고 필터링할 수 있습니다. 다음 몇 개의 섹션에서는 레스토랑에 리뷰를 추가하고 앱에 보안 규칙을 추가합니다.
9. 하위 컬렉션에서 데이터 정리
이 섹션에서는 사용자가 좋아하는 레스토랑(또는 가장 선호하지 않는 레스토랑)을 리뷰할 수 있도록 앱에 평점을 추가합니다.
컬렉션 및 하위 컬렉션
지금까지는 모든 레스토랑 데이터를 'restaurants'라는 최상위 컬렉션에 저장했습니다. 사용자가 식당을 평가할 때 새 Rating
객체를 식당에 추가하려고 합니다. 이 작업에서는 하위 컬렉션을 사용합니다. 하위 컬렉션은 문서에 연결된 컬렉션이라고 생각하면 됩니다. 따라서 각 식당 문서에는 평점 문서로 가득 찬 평점 하위 컬렉션이 있습니다. 하위 컬렉션을 사용하면 문서를 부풀리거나 복잡한 쿼리를 수행하지 않고도 데이터를 정리할 수 있습니다.
하위 컬렉션에 액세스하려면 상위 문서에서 .collection()
를 호출합니다.
val subRef = firestore.collection("restaurants")
.document("abc123")
.collection("ratings")
최상위 컬렉션과 마찬가지로 크기 제한이나 성능 변경 없이 하위 컬렉션에 액세스하고 쿼리할 수 있습니다. Firestore 데이터 모델에 관한 자세한 내용은 여기를 참고하세요.
트랜잭션으로 데이터 쓰기
적절한 하위 컬렉션에 Rating
를 추가하려면 .add()
를 호출하기만 하면 되지만 새 데이터를 반영하도록 Restaurant
객체의 평균 평점과 평점 수를 업데이트해야 합니다. 별도의 작업을 사용하여 이 두 가지를 변경하는 경우 여러 경합 상태로 인해 오래되거나 잘못된 데이터가 발생할 수 있습니다.
평점이 제대로 추가되도록 Google은 트랜잭션을 사용하여 식당에 평점을 추가합니다. 이 거래는 몇 가지 작업을 수행합니다.
- 레스토랑의 현재 평점을 읽고 새 평점을 계산합니다.
- 하위 컬렉션에 평점 추가
- 레스토랑의 평균 평점 및 평점 수 업데이트
RestaurantDetailFragment.kt
를 열고 addRating
함수를 구현합니다.
private fun addRating(restaurantRef: DocumentReference, rating: Rating): Task<Void> {
// Create reference for new rating, for use inside the transaction
val ratingRef = restaurantRef.collection("ratings").document()
// In a transaction, add the new rating and update the aggregate totals
return firestore.runTransaction { transaction ->
val restaurant = transaction.get(restaurantRef).toObject<Restaurant>()
?: throw Exception("Restaurant not found at ${restaurantRef.path}")
// Compute new number of ratings
val newNumRatings = restaurant.numRatings + 1
// Compute new average rating
val oldRatingTotal = restaurant.avgRating * restaurant.numRatings
val newAvgRating = (oldRatingTotal + rating.rating) / newNumRatings
// Set new restaurant info
restaurant.numRatings = newNumRatings
restaurant.avgRating = newAvgRating
// Commit to Firestore
transaction.set(restaurantRef, restaurant)
transaction.set(ratingRef, rating)
null
}
}
addRating()
함수는 전체 트랜잭션을 나타내는 Task
를 반환합니다. onRating()
함수에서 리스너가 작업에 추가되어 트랜잭션 결과에 응답합니다.
이제 앱을 다시 실행하고 레스토랑 중 하나를 클릭하면 레스토랑 세부정보 화면이 표시됩니다. + 버튼을 클릭하여 리뷰를 추가합니다. 별표 수를 선택하고 텍스트를 입력하여 리뷰를 추가합니다.
제출을 클릭하면 거래가 시작됩니다. 거래가 완료되면 아래에 리뷰가 표시되고 식당의 리뷰 수가 업데이트됩니다.
축하합니다. 이제 Cloud Firestore를 기반으로 소셜, 지역, 모바일 레스토랑 리뷰 앱을 빌드했습니다. 요즘 아주 인기가 많다고 들었어요.
10. 데이터 보안
지금까지는 이 애플리케이션의 보안은 고려하지 않았습니다. 사용자가 올바른 자신의 데이터만 읽고 쓸 수 있는지 어떻게 알 수 있나요? Firestore 데이터베이스는 보안 규칙이라는 구성 파일로 보호됩니다.
firestore.rules
파일을 열면 다음과 같이 표시됩니다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
//
// WARNING: These rules are insecure! We will replace them with
// more secure rules later in the codelab
//
allow read, write: if request.auth != null;
}
}
}
원치 않는 데이터 액세스 또는 변경을 방지하기 위해 이러한 규칙을 변경해 보겠습니다. firestore.rules
파일을 열고 콘텐츠를 다음으로 바꿉니다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Determine if the value of the field "key" is the same
// before and after the request.
function isUnchanged(key) {
return (key in resource.data)
&& (key in request.resource.data)
&& (resource.data[key] == request.resource.data[key]);
}
// Restaurants
match /restaurants/{restaurantId} {
// Any signed-in user can read
allow read: if request.auth != null;
// Any signed-in user can create
// WARNING: this rule is for demo purposes only!
allow create: if request.auth != null;
// Updates are allowed if no fields are added and name is unchanged
allow update: if request.auth != null
&& (request.resource.data.keys() == resource.data.keys())
&& isUnchanged("name");
// Deletes are not allowed.
// Note: this is the default, there is no need to explicitly state this.
allow delete: if false;
// Ratings
match /ratings/{ratingId} {
// Any signed-in user can read
allow read: if request.auth != null;
// Any signed-in user can create if their uid matches the document
allow create: if request.auth != null
&& request.resource.data.userId == request.auth.uid;
// Deletes and updates are not allowed (default)
allow update, delete: if false;
}
}
}
}
이러한 규칙은 클라이언트가 안전한 변경만 수행할 수 있도록 액세스를 제한합니다. 예를 들어 레스토랑 문서를 업데이트할 때는 이름이나 다른 불변 데이터가 아닌 평점만 변경할 수 있습니다. 사용자 ID가 로그인한 사용자와 일치하는 경우에만 평점을 생성할 수 있으므로 스푸핑을 방지합니다.
보안 규칙에 관한 자세한 내용은 문서를 참고하세요.
11. 결론
이제 Firestore를 기반으로 모든 기능을 갖춘 앱을 만들었습니다. 다음과 같은 가장 중요한 Firestore 기능에 대해 알아봤습니다.
- 문서 및 컬렉션
- 데이터 읽기 및 쓰기
- 쿼리를 사용한 정렬 및 필터링
- 하위 컬렉션
- 트랜잭션
자세히 알아보기
Firestore를 계속 배우려면 다음부터 시작하는 것이 좋습니다.
이 Codelab의 레스토랑 앱은 'Friendly Eats' 예시 애플리케이션을 기반으로 합니다. 여기에서 앱의 소스 코드를 찾아볼 수 있습니다.
선택사항: 프로덕션에 배포
지금까지 이 앱은 Firebase 에뮬레이터 도구 모음만 사용했습니다. 이 앱을 실제 Firebase 프로젝트에 배포하는 방법을 알아보려면 다음 단계로 진행하세요.
12. (선택사항) 앱 배포
지금까지 이 앱은 완전히 로컬이었고 모든 데이터가 Firebase 에뮬레이터 도구 모음에 포함되어 있었습니다. 이 섹션에서는 이 앱이 프로덕션에서 작동하도록 Firebase 프로젝트를 구성하는 방법을 알아봅니다.
Firebase 인증
Firebase Console에서 인증 섹션으로 이동하여 시작하기를 클릭합니다. 로그인 방법 탭으로 이동하여 기본 제공업체에서 이메일/비밀번호 옵션을 선택합니다.
이메일/비밀번호 로그인 방법을 사용 설정하고 저장을 클릭합니다.
Firestore
데이터베이스 만들기
Console의 Firestore 데이터베이스 섹션으로 이동하여 데이터베이스 만들기를 클릭합니다.
- 보안 규칙에 관한 메시지가 표시되면 프로덕션 모드로 시작하도록 선택합니다. 그러면 곧 규칙이 업데이트됩니다.
- 앱에 사용할 데이터베이스 위치를 선택합니다. 데이터베이스 위치 선택은 영구적인 결정이며 이를 변경하려면 새 프로젝트를 만들어야 합니다. 프로젝트 위치 선택에 관한 자세한 내용은 문서를 참고하세요.
규칙 배포
앞서 작성한 보안 규칙을 배포하려면 Codelab 디렉터리에서 다음 명령어를 실행합니다.
$ firebase deploy --only firestore:rules
이렇게 하면 firestore.rules
의 콘텐츠가 프로젝트에 배포되며 콘솔의 규칙 탭으로 이동하여 확인할 수 있습니다.
색인 배포
FRIENDEats 앱에는 여러 맞춤 복합 색인이 필요한 복잡한 정렬 및 필터링이 있습니다. Firebase Console에서 직접 만들 수도 있지만 firestore.indexes.json
파일에 정의를 작성하고 Firebase CLI를 사용하여 배포하는 것이 더 간단합니다.
firestore.indexes.json
파일을 열면 필수 색인이 이미 제공된 것을 확인할 수 있습니다.
{
"indexes": [
{
"collectionId": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "city", "mode": "ASCENDING" },
{ "fieldPath": "avgRating", "mode": "DESCENDING" }
]
},
{
"collectionId": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "category", "mode": "ASCENDING" },
{ "fieldPath": "avgRating", "mode": "DESCENDING" }
]
},
{
"collectionId": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "price", "mode": "ASCENDING" },
{ "fieldPath": "avgRating", "mode": "DESCENDING" }
]
},
{
"collectionId": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "city", "mode": "ASCENDING" },
{ "fieldPath": "numRatings", "mode": "DESCENDING" }
]
},
{
"collectionId": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "category", "mode": "ASCENDING" },
{ "fieldPath": "numRatings", "mode": "DESCENDING" }
]
},
{
"collectionId": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "price", "mode": "ASCENDING" },
{ "fieldPath": "numRatings", "mode": "DESCENDING" }
]
},
{
"collectionId": "restaurants",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "city", "mode": "ASCENDING" },
{ "fieldPath": "price", "mode": "ASCENDING" }
]
},
{
"collectionId": "restaurants",
"fields": [
{ "fieldPath": "category", "mode": "ASCENDING" },
{ "fieldPath": "price", "mode": "ASCENDING" }
]
}
],
"fieldOverrides": []
}
이러한 색인을 배포하려면 다음 명령어를 실행합니다.
$ firebase deploy --only firestore:indexes
색인 생성은 즉시 이루어지지 않으며 Firebase Console에서 진행 상황을 모니터링할 수 있습니다.
앱 구성
util/FirestoreInitializer.kt
및 util/AuthInitializer.kt
파일에서 디버그 모드일 때 에뮬레이터에 연결하도록 Firebase SDK를 구성했습니다.
override fun create(context: Context): FirebaseFirestore {
val firestore = Firebase.firestore
// Use emulators only in debug builds
if (BuildConfig.DEBUG) {
firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
}
return firestore
}
실제 Firebase 프로젝트로 앱을 테스트하려면 다음 중 하나를 수행하면 됩니다.
- 출시 모드로 앱을 빌드하고 기기에서 실행합니다.
- 일시적으로
BuildConfig.DEBUG
를false
로 바꾸고 앱을 다시 실행합니다.
프로덕션에 제대로 연결하려면 앱에서 로그아웃한 다음 다시 로그인해야 할 수 있습니다.