본문 바로가기
DEV/App 개발

React Native 앱 만들기 :: 구글 파이어베이스(Firebase)를 활용한 서버 구성

by 올커 2022. 8. 11.

 

리액트 네이티브(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)

※ 구글 파이어베이스 접속 화면 (https://firebase.google.com/)

     - 구글에서 제공하는 서버리스 서비스

     - 하나의 서버를 구축하는  데 위와 같은 여러 기능들을 제공하고 있다.

     - 파이어베이스(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'
    }
});

 

 

반응형

댓글