https://iamthejiheee.tistory.com/246
https://rnfirebase.io/database/usage
https://rnfirebase.io/firestore/usage
https://invertase.io/blog/getting-started-with-cloud-firestore-on-react-native
파이어베이스 콘솔의 해당 프로젝트 대쉬보드에서 데이터베이스 만들기를 수행한다. 우선 개발중인 경우에는 테스트 모드에서 시작 옵션을 선택한다.
데이터베이스 위치는 서울로 설정한다.
Cloud Firestore 보안규칙을 수정해서 모든 사용자가 읽기/쓰기 권한을 가질수 있도록 허용한다.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// This rule allows anyone with your Firestore database reference to view, edit,
// and delete all data in your Firestore database. It is useful for getting
// started, but it is configured to expire after 30 days because it
// leaves your app open to attackers. At that time, all client
// requests to your Firestore database will be denied.
//
// Make sure to write security rules for your app before that time, or else
// all client requests to your Firestore database will be denied until you Update
// your rules
match /{document=**} {
allow read, write: if true;
}
}
}
npm install moment
날짜/시간 포맷을 설정하기 위하여 moment 라이브러리를 설치한다.
* 파이어베이스 데이터베이스 조회/추가 api 구현하기
import firestore from '@react-native-firebase/firestore';
const getRef = (collections) => {
return firestore().collection(collections);
}
export const addData = async (collections, data) => {
await getRef(collections).add(data)
console.log(`${collections} : ${JSON.stringify(data)} added in firestore!`)
}
export const getCollection = (collections, onResult, onError, query, order, limit) => {
let ref = getRef(collections)
// 조건쿼리
if(query && query.exists && query.condition && query.condition.length !== 0){
ref = ref.where(...query.condition)
}
if(order && order.exists && order.condition && order.condition.length !== 0){
ref = ref.orderBy(...order.condition)
}
if(limit && limit.exists && limit.condition){
ref = ref.limit(limit.condition)
}
return ref.onSnapshot(onResult, onError)
}
export const getCurrentTime = () => {
return firestore.FieldValue.serverTimestamp()
}
루트 디렉터리에 apis 폴더를 생성하고, 그 안에 firebase.js 파일을 생성한 다음 위와 같이 작성한다.
const getRef = (collections) => {
return firestore().collection(collections);
}
getRef 함수는 Cloud Firestore 에서 쿼리를 하기 위한 참조값(reference)을 반환한다. 해당 함수는 외부에서 사용하지 않고, 파일 안에서만 사용된다.
export const addData = async (collections, data) => {
await getRef(collections).add(data)
console.log(`${collections} : ${JSON.stringify(data)} added in firestore!`)
}
addData 함수는 collections 에 data 를 추가한다. 현재 collections 는 "todos" 컬렉션이다. data 는 할일목록에 추가할 TODO 객체이다. 해당 함수는 외부에서 사용할 것이므로 export 한다.
export const getCollection = (collections, onResult, onError, query, order, limit) => {
let ref = getRef(collections)
// 조건쿼리
if(query && query.exists && query.condition && query.condition.length !== 0){
ref = ref.where(...query.condition)
}
if(order && order.exists && order.condition && order.condition.length !== 0){
ref = ref.orderBy(...order.condition)
}
if(limit && limit.exists && limit.condition){
ref = ref.limit(limit.condition)
}
return ref.onSnapshot(onResult, onError)
}
getCollection 함수는 collections 를 조회한다. 현재 collections 는 "todos" 컬렉션이다. 즉, 해당함수는 할일목록을 조회한다. 전체 할일목록이 저장된 초기 ref 에서 조건에 따라 검색/정렬/갯수제한을 순차적/선택적으로 수행한다. onSnapshot 메서드는 마지막 결과 ref (필터링된 할일목록) 을 onResult 함수로 전달한다. 해당 함수는 외부에서 사용할 것이므로 export 한다.
export const getCurrentTime = () => {
return firestore.FieldValue.serverTimestamp()
}
getCurrentTime 함수는 serverTimestamp 라는 static 메서드를 이용하여 새로운 할일이 Firestore 데이터베이스에 저장될때의 시각을 저장한다. 이렇게 하면 서로 다른 Timezone 에 사는 사용자들이 새로운 할일을 추가할때 자신의 로컬시간을 저장하지 않고, 동일한 서버 시각으로 통일할 수 있다.
import React, { useState, useEffect, useRef } from 'react' // 카테고리 저장을 위한 useRef 임포트
import {
addData,
getCollection,
getCurrentTime
} from '../apis/firebase' // api 함수 (추가)
import {
SafeAreaView,
View, Text,
StyleSheet,
StatusBar,
Keyboard,
FlatList,
TouchableHighlight
} from 'react-native'
import DateHeader from '../components/DateHeader'
import Default from '../components/Default'
import TodoInsert from '../components/TodoInsert'
import TodoList from '../components/TodoList'
import DropdownItem from '../components/DropdownItem'
function HomeScreen({ navigation, caretType, setCaretType }){
const date = new Date()
const categories = ['자기계발', '업무', '오락', '여행', '연애', 'IT', '취미']
const [todos, setTodos] = useState([]) // todos 초기화 (수정)
const [todoText, setTodoText] = useState('')
const [warning, setWarning] = useState(false)
const [loading, setLoading ] = useState(true) // 로딩상태 (추가)
// const [category, setCategory] = useState('')
const category = useRef('') // 카테고리 변수
const onInsertTodo = async (trimedText) => { // async (추가)
if(!category.current){ // 카테고리를 선택하지 않은 경우
setTodoText('카테고리를 먼저 선택해주세요!')
setWarning(true)
return
}
if(trimedText && trimedText.length > 3){ // 최소 글자수 제한
// const nextId = todos.length + 1 // 주석처리
// const todoContents = trimedText.split(',') // 주석처리
// const createdTime = new Date() // 주석처리
if(todos.filter(todo => todo.title === trimedText).length > 0){ // trimedText (수정)
setTodoText('중복된 할일입니다.')
setWarning(true)
}else{
const newTodo = { // 필드 변경 및 수정 (수정)
title: trimedText,
category: category.current || '자기계발', // 선택한 카테고리 설정
isDone: false,
createdAt: getCurrentTime(), // 클라이언트 기준이 아니라 서버기준 저장시각
// createdAt: `${createdTime.getFullYear()}-${createdTime.getMonth()+1}-${createdTime.getDate()}`
}
await addData('todos', newTodo) // Firestore 에 새로운 할일 추가 (추가)
Keyboard.dismiss() // 추가버튼 클릭시 키보드 감추기
setTodoText('') // 입력창 초기화
category.current = '' // 카테고리 초기화
}
}else{
console.log('3자 이상 입력하세요!')
setTodoText('3자 이상 입력하세요!')
setWarning(true)
}
}
const closeDropdown = () => {
caretType && setCaretType(false)
}
const selectCategory = (item, e) => { // 카테고리 드롭다운 선택시
console.log("카테고리: ", item)
closeDropdown()
category.current = item
}
const handleOutSideOfMenu = (e) => {
console.log('홈화면을 터치하셨습니다.')
closeDropdown()
}
useEffect(() => navigation.addListener('focus', () => console.log('페이지 로딩')), [])
useEffect(() => navigation.addListener('blur', () => console.log('페이지 벗어남')), [])
useEffect(() => { // 할일목록 조회 (추가)
function onResult(querySnapshot){
const list = []
querySnapshot.forEach(doc => {
console.log(doc.data())
list.push({
...doc.data(),
id: doc.id,
})
})
setTodos(list)
if (loading) {
setLoading(false)
}
}
function onError(error){
console.error(`${error} occured when reading todos`)
}
return getCollection('todos',
onResult, onError,
null,
{exists: true, condition: ['createdAt', 'asc']},
null)
}, [])
if (loading) { // 로딩화면 (추가)
return (
<View>
<Text>로딩중...</Text>
</View>
)
}
return (
<SafeAreaView
style={styles.block}
onTouchStart={handleOutSideOfMenu}>
<StatusBar backgroundColor="#a8c8ffff"></StatusBar>
{caretType
&& (
<View
style={styles.dropdownShadow}
onTouchStart={(e) => { // 터치 시작점 설정 : 캡쳐링 방지
console.log('여기를 지나침')
e.stopPropagation() // 터치 버블링 방지
}}
>
<FlatList
data={categories}
keyExtractor={item => item}
renderItem={({item}) => (
<DropdownItem category={item} selectCategory={(e) => selectCategory(item, e)}/> // 아이템 각각의 뷰 화면 : 카테고리 선택시 이벤트핸들러 함수 등록
)}
style={styles.dropdownList}
/>
</View>
)}
<DateHeader date={date}/>
{todos.length === 0 ? <Default/> : <TodoList todos={todos} />}
<TodoInsert
onInsertTodo={onInsertTodo}
todoText={todoText}
setTodoText={setTodoText}
warning={warning}
setWarning={setWarning}/>
</SafeAreaView>
)
}
const styles = StyleSheet.create({
block: {
flex: 1,
},
dropdownList: {
padding: 5
},
dropdownShadow: {
shadowOffset: { width: 0, height: 20 },
shadowColor: '#000',
shadowOpacity: 0.25,
backgroundColor : "#fff", // invisible color
zIndex: 1,
elevation: 1,
position: 'absolute',
top: -15,
borderRadius: 5,
margin: 15
}
})
export default HomeScreen
screens > HomeScreen.js 파일을 위와 같이 수정한다.
import {
addData,
getCollection,
getCurrentTime
} from '../apis/firebase'
Cloud Firestore API 를 사용하기 위한 함수를 임포트한다.
const [todos, setTodos] = useState([])
todos 배열을 빈 배열([])로 초기화한다.
const [loading, setLoading ] = useState(true)
로딩상태를 조회하기 위하여 loading 상태를 정의한다.
const onInsertTodo = async (trimedText) => {
// 중략
}
Cloud Firestore 데이터베이스에 비동기로 접속하기 우하여 async 키워드를 추가한다.
// const nextId = todos.length + 1
// const todoContents = trimedText.split(',')
// const createdTime = new Date()
새로운 할일에 대한 id 값은 Cloud Firestore 데이터베이스에서 자동으로 생성하므로 주석처리한다. 할일 생성시각도 데이터베이스에서 자동으로 생성해주므로 주석처리한다.
if(todos.filter(todo => todo.title === trimedText).length > 0){
setTodoText('중복된 할일입니다.')
setWarning(true)
}else{
// 중략
}
newTodo.title 을 trimedText 로 수정하였다. 이유는 할일이 중복되면 데이터베이스에 저장하지 않으므로 굳이 그전에 newTodo 객체를 생성할 필요가 없기 때문이다.
const newTodo = {
title: trimedText,
category: category.current || '자기계발', // 선택한 카테고리 설정 (수정)
isDone: false,
createdAt: getCurrentTime(), // 클라이언트 기준이 아니라 서버기준 저장시각
// createdAt: `${createdTime.getFullYear()}-${createdTime.getMonth()+1}-${createdTime.getDate()}`
}
await addData('todos', newTodo)
새로운 할일인 newTodo 객체에서 id 필드는 제거한다. 왜냐하면 데이터베이스에서 자동으로 생성해주기 때문이다. title 값은 trimedText 그대로 전달해준다. 이전에는 사용자가 입력한 텍스트(trimedText)에서 콤마(,)로 할일과 카테고리를 구분해서 저장했지만 카테고리는 드롭다운에서 선택하므로 더이상 그렇게 할 필요가 없다. isDone 필드는 디폴트값으로 false 로 설정한다. 할일 생성시각은 getCurrentTime 함수를 사용하여 자동으로 저장한다. 마지막으로 addData 함수를 이용하여 'todos' 컬렉션에 새 할일을 추가한다.
useEffect(() => {
function onResult(querySnapshot){
const list = []
querySnapshot.forEach(doc => {
console.log(doc.data())
list.push({
...doc.data(),
id: doc.id,
})
})
setTodos(list)
if (loading) {
setLoading(false)
}
}
function onError(error){
console.error(`${error} occured when reading todos`)
}
return getCollection('todos',
onResult, onError,
null,
{exists: true, condition: ['createdAt', 'asc']},
null)
}, [])
useEffect 안에 콜백함수를 등록한다. 콜백함수 안에서는 getCollection 함수를 이용하여 Cloud Firestore 에서 할일목록을 조회한다. onResult 는 쿼리에 성공한 경우에 실행할 함수이다. querySnapshot 은 할일목록 중 일부가 추가/변경/삭제될때마다 언제나 최근에 업데이트된 할일목록을 새롭게 조회한다. 이는 getCollection 함수가 Cloud Firestore 의 onSnapshot 메서드를 호출하기 때문이다. doc.data() 는 하나의 할일 정보이다. getCollection 의 4~6번째 파라미터는 할일목록을 조회할때 각각 검색/정렬/갯수 제한을 추가로 설정할 수 있다. 설정값은 exists 와 condition 을 프로퍼티로 가지는 객체를 설정하거나 null 을 설정하면 된다.
if (loading) {
return (
<View>
<Text>로딩중...</Text>
</View>
)
}
Cloud Firestore 에서 데이터를 조회하는 동안 보여줄 로딩화면이다.
<FlatList
data={todos}
style={styles.container}
keyExtractor={item => item.id}
ItemSeparatorComponent={() => <View style={styles.line}/>}
renderItem={({item}) => (
<TodoItem {...item}/> // 아이템 각각의 뷰 화면
)}
/>
components > TodoList.js 파일에서 해당 부분을 위와 같이 수정한다.
keyExtractor={item => item.id.toString()}
각 TODO 의 키 값 설정을 아래와 같이 수정한다.
keyExtractor={item => item.id}
<TodoItem item={item}/>
하나의 할일에 대한 정보를 담고 있는 item 객체를 그대로 props 로 전달하지 않고, 아래와 같이 풀어서 전달한다.
<TodoItem {...item}/> // 아이템 각각의 뷰 화면
import React from 'react'
import { View, Text, StyleSheet } from 'react-native'
import moment from 'moment' // 시간포맷 설정 (추가)
function TodoItem({ id, title, category, isDone, createdAt }){ // TODO 속성 풀어헤치기 (수정)
console.log("할일 생성시각: ", title, createdAt) // 디버깅용 (추가)
return (
<View style={styles.item}>
<View style={styles.titleMargin}>
<Text style={styles.title}>{title}</Text>
</View>
<View>
<Text>{category} ({isDone ? "종료": "진행중"})</Text>
<Text style={styles.dateText}>{createdAt && moment(createdAt.toDate()).format('hh:mm:ss')}</Text>
</View>
</View>
)
}
const styles = StyleSheet.create({
item: {
flexDirection: 'row',
alignItems: 'flex-start',
paddingLeft: 10,
paddingVertical: 10,
// backgroundColor: '#d6e3ffff',
// borderBottomWidth: 1,
// borderBottomColor: '#a8c8ffff',
},
titleMargin: {
marginRight: 10
},
title: {
fontWeight: 'bold',
fontSize: 20,
},
dateText: {
fontSize: 12
}
})
export default React.memo(TodoItem) // 컴포넌트 캐슁
components > TodoItem.js 파일을 위와 같이 작성한다.
import moment from 'moment'
할일의 생성시각에 대한 포맷을 설정하기 위하여 임포트한다.
function TodoItem({ id, title, category, isDone, createdAt }){
// 중략
}
할일에 대한 속성값(props)을 모두 풀어서 전달받는다.
<View style={styles.item}>
<View style={styles.titleMargin}>
<Text style={styles.title}>{title}</Text>
</View>
<View>
<Text>{category} ({isDone ? "종료": "진행중"})</Text>
<Text style={styles.dateText}>{createdAt && moment(createdAt.toDate()).format('hh:mm:ss')}</Text>
</View>
</View>
item.title 은 title 로, item.category 는 category 로, item.createdAt 은 createdAt 으로 수정한다. isDone 값에 따라 할일에 대한 종료여부를 표시한다. createdAt 은 Cloud Firestore 의 타임스탬프 포맷이다. 해당 포맷은 seconds 나 nanoseconds 단위의 숫자값이다. 타임스탬프에서 제공하는 toDate 함수를 이용하면 날짜 포맷으로 변경된다. 또한, 이를 다시 moment 라이브러리를 이용하여 특정 포맷으로 변경한다.
export default React.memo(TodoItem)
React.memo 를 이용하면 해당 할일에 대한 정보가 변경되지 않으면 Virtual DOM 에 새로 렌더링되지 않는다. 즉, 전체 할일목록에서 업데이트된 특정 할일에 대한 TodoItem 컴포넌트만 새로 렌더링되고 나머지 할일에 대한 TodoItem 컴포넌트는 캐슁된다.
* 파이어베이스에 새로운 할일 추가하고 조회하기 테스트
'프로젝트 > 할일목록 앱 (RN)' 카테고리의 다른 글
6. 할일목록 수정 및 삭제 기능 만들기 (0) | 2023.09.27 |
---|---|
5. 할일목록 날짜별로 필터링해서 보여주기 & 할일목록 최신순 정렬해서 보여주기 (0) | 2023.09.25 |
3. 파이어베이스 (Firebase) 연동하기 (0) | 2023.09.14 |
2. 홈화면 구현하기 - 할일목록 보여주기 (0) | 2023.08.20 |
1. 리액트 네비게이션으로 탭메뉴 만들기 (0) | 2023.08.20 |