리액트 네이티브(React Native) 앱 만들기,
구글 파이어베이스(Firebase)를 활용한 서버 만들기
들어가면서..
앱을 운용하기 위해 사용/발생되는 데이터를 관리하기 위한 '서버(Server)',
그리고 직접 서버를 구축하지 않는 '서버리스(Serverless)'를 이해하고 활용해본다.
1. 서버 기본 개념
1) 앱에 모든 데이터를 담게되면 앱 용량이 너무 커지거나,
새로운 데이터 발생시 데이터 배포에 불편함이 발생하기 때문에
데이터를 서버에 담고 요청(Request)/응답(Response) 상호작용을 통해
데이터를 가져오거나(Get) 변경할 수 있다.
2) 서버리스(Serverless)
서버 직접 구축없이 데이터 생성/조회/삭제/수정이 가능토록 제공해주는 서비스
2. 앱과 서버의 동작
1) 앱에서는 서버에서 정한 규칙에 따라 요청(Request)하고,
서버에서는 정해진 규칙에 부합할 시 응답(Response)한다.
이 때, 서버에서 정한 규칙을 API(Application Programming Interface)라고 부른다.
규칙의 형태
(1) 서버가 제공하는 도메인일 경우
www.sparta.com/getdata ←- 데이터 조회 API
www.sparta.com/setData ←- 데이터 저장 API
(2) 서버가 만들어놓은 함수를 이용할 경우
db.ref('/like/').on('value') ←- 데이터 조회 API
db.ref('/like/').set(new_like); <-- 데이터 저장 API
2) 서버에서 앱으로 데이터 전달시 JSON 형태로 전달한다.
JSON : 리스트와 딕셔너리의 복합구조
3) React Native에서 서버로부터 앱으로 데이터를 받아올 때,
(1) useEffect 명령어를 통해 초기 앱 화면이 그려진 후
(2) 서버로 부터 제공되는 API를 이용해 데이터를 준비
(3) 초기화면 종료
4) 외부 API를 활용해 날씨 데이터 입력하기
(1) 방법 : 현재 위치 데이터 가져오기 → 위치 데이터를 이용해 현재 위치 날씨 데이터 가져오기
(2) Expo에서 제공하는 위치 데이터 도구를 활용한다(※ 공식문서 링크)
expo install expo-location
(3) MainPage.js 날씨 가져오기 도구 적용
//Expo에서 위치데이터 관련 라이브러리 가져오기
import * as Location from "expo-location";
...
const getLocation = async () => {
//수많은 로직중에 에러가 발생하면
//해당 에러를 포착하여 로직을 멈추고,에러를 해결하기 위한 catch 영역 로직이 실행
try {
//자바스크립트 함수의 실행순서를 고정하기 위해 쓰는 async,await
await Location.requestForegroundPermissionsAsync();
const locationData= await Location.getCurrentPositionAsync();
console.log(locationData)
} catch (error) {
//혹시나 위치를 못가져올 경우를 대비해서, 안내를 준비합니다
Alert.alert("위치를 찾을 수가 없습니다.", "앱을 껏다 켜볼까요?");
}
}
...
let todayWeather = 10 + 17;
let todayCondition = "흐림"
return ready ? <Loading/> : (
<ScrollView style={styles.container}>
<StatusBar style="light" />
{/* <Text style={styles.title}>나만의 꿀팁</Text> */}
<Text style={styles.weather}>오늘의 날씨: {todayWeather + '°C ' + todayCondition} </Text>
- 외부 API 요청작업은 try / catch
try{ ... } : API 요청과 같은 작업 코드
catch{ ... } : 에러 발생시 실행할 코드
- 함수 실행순서를 정할 때에는 async / await
함수 선언부 앞에 async를 쓰고, 사용하는 함수들 앞에 순차적으로 await를 입력하여 사용한다.
(4) 위도, 경도 입력
locationData에는 위치 좌표가 딕셔너리 안에 들어있어 latitude, longitude를 아래와 같이 추출한다.
const getLocation = async () => {
//수많은 로직중에 에러가 발생하면
//해당 에러를 포착하여 로직을 멈추고,에러를 해결하기 위한 catch 영역 로직이 실행
try {
//자바스크립트 함수의 실행순서를 고정하기 위해 쓰는 async,await
await Location.requestForegroundPermissionsAsync();
const locationData= await Location.getCurrentPositionAsync();
console.log(locationData['coords']['latitude'])
console.log(locationData['coords']['longitude'])
} catch (error) {
//혹시나 위치를 못가져올 경우를 대비해서, 안내를 준비합니다
Alert.alert("위치를 찾을 수가 없습니다.", "앱을 껏다 켜볼까요?");
}
}
(6) 날씨 API 사용
- axios : 서버가 제공하는 도메인 형식의 API 사용을 위한 도구 (※ 공식문서 링크)
yarn add axios
- 좌표값(위도, 경도)을 통해 날씨 데이터를 전달하는 API 사용
import * as Location from "expo-location";
import axios from "axios"
export default function MainPage({navigation,route}) {
const [state,setState] = useState([])
const [cateState,setCateState] = useState([])
//날씨 데이터 상태관리 상태 생성!
const [weather, setWeather] = useState({
temp : 0,
condition : ''
})
...
const getLocation = async () => {
//수많은 로직중에 에러가 발생하면
//해당 에러를 포착하여 로직을 멈추고,에러를 해결하기 위한 catch 영역 로직이 실행
try {
//자바스크립트 함수의 실행순서를 고정하기 위해 쓰는 async,await
await Location.requestForegroundPermissionsAsync();
const locationData= await Location.getCurrentPositionAsync();
console.log(locationData)
console.log(locationData['coords']['latitude'])
console.log(locationData['coords']['longitude'])
const latitude = locationData['coords']['latitude']
const longitude = locationData['coords']['longitude']
const API_KEY = "cfc258c75e1da2149c33daffd07a911d";
const result = await axios.get(
`http://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${API_KEY}&units=metric`
);
console.log(result)
const temp = result.data.main.temp;
const condition = result.data.weather[0].main
console.log(temp)
console.log(condition)
//오랜만에 복습해보는 객체 리터럴 방식으로 딕셔너리 구성하기!!
//잘 기억이 안난다면 1주차 강의 6-5를 다시 복습해보세요!
setWeather({
temp,condition
})
} catch (error) {
//혹시나 위치를 못가져올 경우를 대비해서, 안내를 준비합니다
Alert.alert("위치를 찾을 수가 없습니다.", "앱을 껏다 켜볼까요?");
}
}
...
<Text style={styles.weather}>오늘의 날씨: {weather.temp + '°C ' + weather.condition} </Text>
- 날씨 API에서 필요한 데이터만 추출하여 가져오기
API 관련 링크(https://openweathermap.org/current)에서 필요한 데이터가 무엇인지 살펴본다.
3. 서버리스(Serverless)
1) 서버리스 : 서버를 직접 만들지 않고, 서버기능을 제공하는 곳을 활용하는 방식
2) 내 앱서비스에서 어떻게 활용하지?
(1) 서버로부터 제공할 데이터 추출
(2) 사용자 편의를 위한 데이터 저장, 관리
4. 구글 파이어베이스(Firebase) 접속 환경 설정
1) 구글 파이어베이스(Firebase)
- 구글에서 제공하는 서버리스 서비스
- 하나의 서버를 구축하는 데 위와 같은 여러 기능들을 제공하고 있다.
- 파이어베이스(Firebase)는 크게보면 간단히 가입 → 프로젝트 생성 → 서비스 활성화 순으로 사용하게 된다.
가입과 프로젝트 생성은 구글 파이어베이스에서도 잘 안내되어 있으며, 다른 구글 포스팅을 통해 잘 설명된
자료들이 많아 참고하면 된다.
2) 파이어베이스 라이브러리 설치 및 연결
- 프로젝트 생성 후에는 파이어베이스에 개발중인 프로젝트가 웹인지, Android인지, iOS인지 전달해야
파이어베이스를 코드단에서 연결할 수 있는 연결정보를 준다.
현재 포스팅에서는 자바스크립트를 활용하고 있기 때문에 웹 SDK를 이용한다.
웹 SDK는 </> 모습의 흰색 버튼을 눌러서 앱을 추가한다.
앱 닉네임을 작성하고 등록하면 아래와 같이 연결정보를 준다.
앱이 완성되면 콘솔로 이동 후 톱니바퀴 설정 버튼을 누르면 '프로젝트 설정' 이 있다. Click!
아래쪽에 사용할 접속 정보가 코드로 존재하는 것을 확인할 수 있다.
- 다시 터미널로 돌아와 파이어베이스를 이용할 수 있도록 도와주는 expo 도구를 설치한다.
expo install firebase
- 설치 완료 후 firebaseConfig.js 파일을 생성하고 아래와 같이 접속정보 코드를 입력한다.
//firebaseConfig.js
import firebase from "firebase/compat/app";
// 사용할 파이어베이스 서비스 주석을 해제합니다
//import "firebase/compat/auth";
import "firebase/compat/database";
//import "firebase/compat/firestore";
//import "firebase/compat/functions";
import "firebase/compat/storage";
// Initialize Firebase
//파이어베이스 사이트에서 봤던 연결정보를 여기에 가져옵니다
const firebaseConfig = {
apiKey: ///
authDomain: ///
databaseURL: ///
projectId: ///
storageBucket: ///
messagingSenderId: ///
appId: ///
measurementId: ///
};
//사용 방법입니다.
//파이어베이스 연결에 혹시 오류가 있을 경우를 대비한 코드로 알아두면 됩니다.
if (!firebase.apps.length) {
firebase.initializeApp(firebaseConfig);
}
export const firebase_db = firebase.database()
5. 구글 파이어베이스 파일 스토리지(Storage)
1) 파일 스토리지(Storage) : 파일 저장소
- 사용할 이미지, 파일을 업로드하여 필요할 때마다 꺼내 쓰는 용도
스토리지는 위 그림처럼 왼쪽메뉴 → 빌드 → Storage 에 위치하고 있다.
'시작하기' 버튼을 클릭하고 아래 절차에 따라 설정을 완료한다
(Cloud Storage 위치는 국내인 경우 가깝다고 생각되는 asia-notrheast3으로 선택하였다.)
기본 버킷 생성 중... 이라는 준비가 끝나면 스토리지를 사용할 수 있는 준비가 다 되었다.
파일을 업로드하면 저장되면서 저장된 주소까지 제공해준다.
6. 구글 파이어베이스 리얼타임 데이터베이스 설정
- JSON 형태로 저장/관리되는 데이터베이스 서비스를 사용할 땐
'리얼타임 데이터베이스'에서 파이어베이스에서 제공하는 함수를 이용하면 데이터 저장/수정/삭제가 가능하다.
- 마찬가지로 데이터베이스 만들기 버튼을 클릭해 데이터베이스를 생성한다.
붉은색 박스의 URL을 복사하여 firebaseConfig.js 설정코드의 storageBucket 항목에 넣어주어야 한다.
다음 규칙탭으로 넘어와서 권한을 모두 공개로 바꿔준다.
- 이제 앱에서 사용하는 데이터 변경시에는
파이어베이스의 리얼타임 데이터베이스로 들어와서 내용을 변경관리하면
앱을 다운 받은 사용자 모두에게 적용된다.
- JSON 가져오기를 통해 내가 관리하는 JSON을 리얼타임 데이터베이스에 업로드한다.
- 각 데이터들이 리스트/딕셔너리 구조로 저장이 된다.
7. App Mainpage.js 수정하여 앱에 적용하기
import React, {useState, useEffect} from 'react';
import { Alert, LogBox, SafeAreaView, ScrollView, Image, StyleSheet, Text, View, TouchableOpacity } from 'react-native';
const mainIMG = 'https://firebasestorage.googleapis.com/v0/b/sparta-myhoneytip-jay-a915b.appspot.com/o/images%2Flogo_jithub.jpg?alt=media&token=0779fad0-20d4-4d20-9da4-cdc752417184'
import data from '../data.json'; // ◀◀◀◀ 필요데이터 가져오기
...
export default function MainPage({navigation, route}) {
const [state,setState] = useState([]) // ◀◀◀◀ 꿀팁 데이터를 관리하는 상태
const [cateState,setCateState] = useState([]) // ◀◀◀◀ 선택한 카테고리에 맞는 문제 데이터를 저장/관리하는 상태
const [ready,setReady] = useState(true)
...
useEffect(()=>{ // ◀◀◀◀ useEffect에서 데이터 실제 상태관리 시작
navigation.setOptions({
title:'My Tips'
})
setTimeout(()=>{
firebase_db.ref('/tip').once('value').then((snapshot) => { //tip에서 값을 가져와서 snapshot이라는 매개변수에 저장
console.log("파이어베이스에서 데이터 가져왔습니다!!")
let tip = snapshot.val(); //value를 가져와서 tip이라는 변수에 담는다.
setState(tip)
setCateState(tip)
getLocation()
setReady(false)
});
//getLocation()
//setState(data.tip)
//setCateState(data.tip)
//setReady(false)
},1000) // 지연시간 : 자연스러운 앱하면의 구성을 위해 일부러 개발자가 주는 시간
}, [])
...
- 여기서 앞으로는 무조건 지연시간을 주기보단 파이어베이스의 API 실행완료시간에 맡겨두기 위해
setTimeout함수를 사용하지 않도록 한다.
useEffect(()=>{
navigation.setOptions({
title:'My Tips'
})
//setTimeout(()=>{
firebase_db.ref('/tip').once('value').then((snapshot) => { //tip에서 값을 가져와서 snapshot이라는 매개변수에 저장
console.log("파이어베이스에서 데이터 가져왔습니다!!")
let tip = snapshot.val(); //value를 가져와서 tip이라는 변수에 담는다.
setState(tip)
setCateState(tip)
getLocation()
setReady(false)
});
//getLocation()
//setState(data.tip)
//setCateState(data.tip)
//setReady(false)
//},1000) // 지연시간 : 자연스러운 앱하면의 구성을 위해 일부러 개발자가 주는 시간
}, [])
8. 리얼타임 데이터베이스를 활용해 특정 데이터 읽기
- 리얼타임 데이터베이스에 가져올 JSON 파일이 저장되어있고,
앱에서 작동하게 될 각 Card가 항상 모든 데이터를 가져오지 않도록 index(idx)로 가져오도록 변경한다.
Card.js에서 아래 코드의 셋째줄의 {idx:content.idx}와 같이 변경한다.
export default function Card({content, navigation}) {
return (
<TouchableOpacity style={styles.card} onPress={()=>{navigation.navigate('DetailPage', {idx:content.idx})}}>
<Image style={styles.cardImage} source={{uri:content.image}}/>
<View style={styles.cardText}>
<Text style={styles.cardTitle} numberOfLines={1}>{content.title}</Text>
<Text style={styles.cardDesc} numberOfLines={3}>{content.desc}</Text>
<Text style={styles.cardDate}>{content.date}</Text>
</View>
</TouchableOpacity>
)
}
DetailPage.js에서는 아래와 같이 변경해준다.
useEffect(()=>{
console.log(route)
navigation.setOptions({
title:route.params.title,
headerStyle: {
backgroundColor: '#000',
shadowColor: "#000",
},
headerTintColor: "#fff",
})
//넘어온 데이터는 route.params에 저장됨.
const { idx } = route.params;
firebase_db.ref('/tip/'+idx).once('value').then((snapshot) => {
let tip = snapshot.val();
setTip(tip)
});
},[])
9. 리얼타임 데이터베이스에 사용자 기록(찜하기) 데이터 저장하기
1) 사용자별 기록은 다르기 때문에, 사용자 구분이 필요하다.
이를 위해 expo에서 제공하는 expo-application 을 통해 사용자 고유 ID를 생성하여 데이터를 관리할 수 있다.
expo install expo-application
import * as Application from 'expo-application';
const isIOS = Platform.OS === 'ios';
let uniqueId;
if(isIOS){
let iosId = await Application.getIosIdForVendorAsync();
uniqueId = iosId
}else{
uniqueId = Application.androidId
}
console.log(uniqueId)
2) 저장하는 시점은 사용자가 꿀팁 찜하기 버튼을 눌렀을 때 저장해야 한다.
이는 DetailPage.js의 onPress부분에 파이어베이스 기능을 추가하면 된다.
현재 찜 데이터는 const [tip, setTip] = useState() 를 확인해보면
tip 상태에 저장되어 관리되고 있으므로 이 tip을 저장하면 된다.
...
import * as Application from 'expo-application';
const isIOS = Platform.OS === 'ios';
...
const like = async () => { // await를 사용하기 위해 async 사용
// like 방 안에
// 특정 사용자 방안에
// 특정 찜 데이터 아이디 방안에
// 특정 찜 데이터 몽땅 저장!
// 찜 데이터 방 > 사용자 방 > 어떤 찜인지 아이디
let userUniqueId;
if(isIOS){
let iosId = await Application.getIosIdForVendorAsync();
userUniqueId = iosId
}else{
userUniqueId = await Application.androidId
}
console.log(userUniqueId)
firebase_db.ref('/like/'+user_id+'/'+ tip.idx).set(tip,function(error){ // like방 안에, user방 안에, 찜id방 안에
console.log(error)
Alert.alert("찜 완료!")
});
}
10. 실습.
1) LikePage에 찜 데이터 모두 보여주기
2) 찜한 데이터가 없을 때 조회하려는 에러 처리
3) LikeCard에서 '자세히보기', '찜 해제' 버튼 만들기
- 자세히 보기 누르면 DetailPage로
- 찜 해제 누르면 찜 삭제
※ LikePage.js
import React,{useState, useEffect} from 'react';
import {ScrollView, Text, StyleSheet,Platform} from 'react-native';
import LikeCard from '../components/LikeCard';
import Loading from '../components/Loading';
import * as Application from 'expo-application';
const isIOS = Platform.OS === 'ios';
import {firebase_db} from "../firebaseConfig"
export default function LikePage({navigation,route}){
const [tip, setTip] = useState([])
const [ready,setReady] = useState(true)
useEffect(()=>{
navigation.setOptions({
title:'꿀팁 찜'
})
getLike()
},[])
const getLike = async () => {
let userUniqueId;
if(isIOS){
let iosId = await Application.getIosIdForVendorAsync();
userUniqueId = iosId
}else{
userUniqueId = await Application.androidId
}
console.log(userUniqueId)
firebase_db.ref('/like/'+userUniqueId).once('value').then((snapshot) => {
console.log("파이어베이스에서 데이터 가져왔습니다!!")
let tip = snapshot.val();
let tip_list = Object.values(tip)
if(tip_list && tip_list.length > 0){
setTip(tip_list)
setReady(false)
}
})
return (
<ScrollView style={styles.container}>
{
tip.map((content,i)=>{
// LikeCard에서 꿀팀 상태 데이터(==tip)과 꿀팁 상태 데이터를 변경하기 위한
// 상태 변경 함수(== setTip)을 건네준다.
//즉 자기 자신이 아닌, 자식 컴포넌트에서도 부모의 상태를 변경할 수 있다.
return(<LikeCard key={i} content={content} navigation={navigation} tip={tip} setTip={setTip}/>)
})
}
</ScrollView>
)
}
const styles = StyleSheet.create({
container:{
backgroundColor:"#fff"
}
})
}
※ LikeCard.js
import React from 'react';
import {Alert,View, Image, Text, StyleSheet,TouchableOpacity,Platform} from 'react-native'
import {firebase_db} from "../firebaseConfig"
const isIOS = Platform.OS === 'ios';
import * as Application from 'expo-application';
//MainPage로 부터 navigation 속성을 전달받아 Card 컴포넌트 안에서 사용
export default function LikeCard({content,navigation,tip, setTip}){
const detail = () => {
navigation.navigate('DetailPage',{idx:content.idx})
}
const remove = async (cidx) => {
let userUniqueId;
if(isIOS){
let iosId = await Application.getIosIdForVendorAsync();
userUniqueId = iosId
}else{
userUniqueId = await Application.androidId
}
console.log(userUniqueId)
firebase_db.ref('/like/'+userUniqueId+'/'+cidx).remove().then(function(){
Alert.alert("삭제 완료");
//내가 찝 해제 버튼을 누른 카드 idx를 가지고
//찝페이지의 찜데이터를 조회해서
//찜해제를 원하는 카드를 제외한 새로운 찜 데이터(리스트 형태!)를 만든다
let result = tip.filter((data,i)=>{
return data.idx !== cidx
})
//이렇게 만들었으면!
//LikePage로 부터 넘겨 받은 tip(찜 상태 데이터)를
//filter 함수로 새롭게 만든 찜 데이터를 구성한다!
console.log(result)
setTip(result)
})
}
return(
//카드 자체가 버튼역할로써 누르게되면 상세페이지로 넘어가게끔 TouchableOpacity를 사용
<View style={styles.card}>
<Image style={styles.cardImage} source={{uri:content.image}}/>
<View style={styles.cardText}>
<Text style={styles.cardTitle} numberOfLines={1}>{content.title}</Text>
<Text style={styles.cardDesc} numberOfLines={3}>{content.desc}</Text>
<Text style={styles.cardDate}>{content.date}</Text>
<View style={styles.buttonGroup}>
<TouchableOpacity style={styles.button} onPress={()=>detail()}><Text style={styles.buttonText}>자세히보기</Text></TouchableOpacity>
<TouchableOpacity style={styles.button} onPress={()=>remove(content.idx)}><Text style={styles.buttonText}>찜 해제</Text></TouchableOpacity>
</View>
</View>
</View>
)
}
const styles = StyleSheet.create({
card:{
flex:1,
flexDirection:"row",
margin:10,
borderBottomWidth:0.5,
borderBottomColor:"#eee",
paddingBottom:10
},
cardImage: {
flex:1,
width:100,
height:100,
borderRadius:10,
},
cardText: {
flex:2,
flexDirection:"column",
marginLeft:10,
},
cardTitle: {
fontSize:20,
fontWeight:"700"
},
cardDesc: {
fontSize:15
},
cardDate: {
fontSize:10,
color:"#A6A6A6",
},
buttonGroup: {
flexDirection:"row",
},
button:{
width:90,
marginTop:20,
marginRight:10,
marginLeft:10,
padding:10,
borderWidth:1,
borderColor:'deeppink',
borderRadius:7
},
buttonText:{
color:'deeppink',
textAlign:'center'
}
});
'DEV > App 개발' 카테고리의 다른 글
React Native 앱 만들기 :: React Native, Expo 앱 화면 만들기(2) (0) | 2022.07.28 |
---|---|
React Native 앱 만들기 :: React Native, Expo 앱 화면 만들기 (0) | 2022.07.22 |
React Native 앱 만들기 :: 자바스크립트 기본 개념 및 문법 (0) | 2022.07.12 |
댓글