스파르타코딩클럽 내일배움단 앱개발 종합반 4주차 개발일지

Bluewiz_YSH·2022년 5월 25일
0

1. 4주차 배울 내용

1주차부터 3주차까지는 앱 화면을 어떻게 채워넣고 꾸미고 기능을 부여하고 연결할것인가에 대해서 배우고 실습해왔다면 4주차에서는 강사님이 말씀하시길, 좀 더 서버에 관련해 배우고 앱과 서버간의 데이터를 어떻게 관리해야하는지에 대해 중점적으로 배운다고 하셨다. 그래서 실제로 서버를 구동을 해볼건데 수많은 서버 프로그램 중 간편하게 해볼수 있는 서버리스 프로그램, 구글이 만든 파이어베이스를 이용한다고 하셨다.

자세히는 앱에다가 모든 데이터를 저장하기에는 무겁고 번거롭기 때문에 데이터 수치들은 서버에 입력해놓고 요청이 있을때마다 앱 클라이언트에 전송하는 그런 관계 구조에 대해서 배우고, 그런 관계 구조를 직접 실천하기 위해 서버와 함께 데이터 생성/조회/삭제/수정 기능을 간편하게 제공하는 서버리스(serverless) 서비스인 파이어베이스를 다룰거라고 하셨다.

이런 내용을 간단히 그림으로 표현하면 아래와 같다고 한다.


2. [앱과 서버] 날씨 서버 외부 API

앱(클라이언트)에서 서버에 요청을 하거나 데이터를 받으려면 미리 약속된 요청(request)을 해야한다. 약속된 요청을 받은 서버는 정해진 반응(response)의 결과물을 앱(클라이언트)에 전송한다. 이를 API(Application Programming Interface)라고 한다. API 방식은 네이버 주소처럼 도메인을 사용할수도 있고 아니면 미리 정해둔 함수를 사용하는 방법도 있다고 강사님이 말씀하셨다.

그리고 또한, 지금까지 우리가 배웠었던 JSON 데이터 형태를 통해 서버는 데이터를 전달하며 앞으로 리액트 네이티브를 쓰는 우리가 눈여겨 봐야하는, 데이터 상태 시점은 데이터가 준비되는 시점인 useEffect, 그리고 우리가 앞으로 구현할 팁 찜하기에서 사용자가 찜한 데이터를 서버에 저장하는 시점 이 두가지 시점들이 중요하게 될거라고 강사님은 말씀하셨다.

일단 복습 겸 MainPage.js 기반 앱 화면 우측 상단에 위치한 우리가 만들어두었던 날씨 기온 컨텐츠를 날씨 서버 외부 API와 연동시켜 진짜 실제 날씨 기온을 핸드폰 위치에 따라 표시하도록 구현을 해볼거라고 하셨다. 그 API는 openweathermap api 라고 하셨다.

먼저 expo에서 핸드폰 현재 위치를 받아와야하기때문에 라이브러리 설치를 또 해야되서 아래와 같은 코드문을 터미널에 입력해야했다. (공식 문서 링크 참고)

expo install expo-location

그런 다음 아래와 같은 코드문을 MainPage.js에 입력,수정해야했다.

import React,{useState,useEffect} from 'react';
import main from '../assets/main.png';
import { StyleSheet, Text, View, Image, TouchableOpacity, ScrollView} from 'react-native';
import data from '../data.json';
import Card from '../components/Card';
import Loading from '../components/Loading';
import { StatusBar } from 'expo-status-bar';
import * as Location from "expo-location";

export default function MainPage({navigation,route}) {
  console.disableYellowBox = true;
  //return 구문 밖에서는 슬래시 두개 방식으로 주석

  //기존 꿀팁을 저장하고 있을 상태
  const [state,setState] = useState([])
  //카테고리에 따라 다른 꿀팁을 그때그때 저장관리할 상태
  const [cateState,setCateState] = useState([])

  //컴포넌트에 상태를 여러개 만들어도 됨
  //관리할 상태이름과 함수는 자유자재로 정의할 수 있음
  //초기 상태값으로 리스트, 참거짓형, 딕셔너리, 숫자, 문자 등등 다양하게 들어갈 수 있음.
  const [ready,setReady] = useState(true)

  useEffect(()=>{
	   
    //뒤의 1000 숫자는 1초를 뜻함
    //1초 뒤에 실행되는 코드들이 담겨 있는 함수
    setTimeout(()=>{
        //헤더의 타이틀 변경
        navigation.setOptions({
            title:'나만의 꿀팁'
        })
        //꿀팁 데이터로 모두 초기화 준비
        let tip = data.tip;
        setState(tip)
        setCateState(tip)
        getLocation()
        setReady(false)
    },1000)

    
  },[])

  const getLocation = async () => {
    //수많은 로직중에 에러가 발생하면
    //해당 에러를 포착하여 로직을 멈추고,에러를 해결하기 위한 catch 영역 로직이 실행
    try {
      //자바스크립트 함수의 실행순서를 고정하기 위해 쓰는 async,await
      await Location.requestPermissionsAsync();
      const locationData= await Location.getCurrentPositionAsync();
       console.log(locationData['coords']['latitude'])
      console.log(locationData['coords']['longitude'])

    } catch (error) {
      //혹시나 위치를 못가져올 경우를 대비해서, 안내를 준비합니다
      Alert.alert("위치를 찾을 수가 없습니다.", "앱을 껏다 켜볼까요?");
    }
  }

    const category = (cate) => {
        if(cate == "전체보기"){
            //전체보기면 원래 꿀팁 데이터를 담고 있는 상태값으로 다시 초기화
            setCateState(state)
        }else{
            setCateState(state.filter((d)=>{
                return d.category == cate
            }))
        }
    }


	let todayWeather = 10 + 17;
    let todayCondition = "흐림"

	//처음 ready 상태값은 true 이므로 ? 물음표 바로 뒤에 값이 반환(그려짐)됨
  //useEffect로 인해 데이터가 준비되고, ready 값이 변경되면 : 콜론 뒤의 값이 반환(그려짐)
  return ready ? <Loading/> :  (
    /*
      return 구문 안에서는 {슬래시 + * 방식으로 주석
    */
    <ScrollView style={styles.container}>
        <StatusBar style="black" />
        {/* <Text style={styles.title}>나만의 꿀팁</Text> */}
        <Text style={styles.weather}>오늘의 날씨: {todayWeather + '°C ' + todayCondition} </Text>
        <TouchableOpacity style={styles.aboutButton} onPress={()=>{navigation.navigate('AboutPage')}}>
          <Text style={styles.aboutButtonText}>소개 페이지</Text>
        </TouchableOpacity>
        <Image style={styles.mainImage} source={main}/>
        <ScrollView style={styles.middleContainer} horizontal indicatorStyle={"white"}>
            <TouchableOpacity style={styles.middleButtonAll} onPress={()=>{category('전체보기')}}><Text style={styles.middleButtonTextAll}>전체보기</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton01} onPress={()=>{category('생활')}}><Text style={styles.middleButtonText}>생활</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton02} onPress={()=>{category('재테크')}}><Text style={styles.middleButtonText}>재테크</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton03} onPress={()=>{category('반려견')}}><Text style={styles.middleButtonText}>반려견</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton04} onPress={()=>{navigation.navigate('LikePage')}}><Text style={styles.middleButtonText}>꿀팁 찜</Text></TouchableOpacity>
        </ScrollView>
        <View style={styles.cardContainer}>
            {/* 하나의 카드 영역을 나타내는 View */}
            {
            cateState.map((content,i)=>{
                return (<Card content={content} key={i} navigation={navigation}/>)
            })
            }
            
        </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    //앱의 배경 색
    backgroundColor: '#fff',
  },
  title: {
    //폰트 사이즈
    fontSize: 20,
    //폰트 두께
    fontWeight: '700',
    //위 공간으로 부터 이격
    marginTop:50,
    //왼쪽 공간으로 부터 이격
    marginLeft:20
  },
weather:{
    alignSelf:"flex-end",
    paddingRight:20
  },
  mainImage: {
    //컨텐츠의 넓이 값
    width:'90%',
    //컨텐츠의 높이 값
    height:200,
    //컨텐츠의 모서리 구부리기
    borderRadius:10,
    marginTop:20,
    //컨텐츠 자체가 앱에서 어떤 곳에 위치시킬지 결정(정렬기능)
    //각 속성의 값들은 공식문서에 고대로~ 나와 있음
    alignSelf:"center"
  },
  middleContainer:{
    marginTop:20,
    marginLeft:10,
    height:60
  },
  middleButtonAll: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#20b2aa",
    borderColor:"deeppink",
    borderRadius:15,
    margin:7
  },
  middleButton01: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#fdc453",
    borderColor:"deeppink",
    borderRadius:15,
    margin:7
  },
  middleButton02: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#fe8d6f",
    borderRadius:15,
    margin:7
  },
  middleButton03: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#9adbc5",
    borderRadius:15,
    margin:7
  },
  middleButton04: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#f886a8",
    borderRadius:15,
    margin:7
  },
  middleButtonText: {
    color:"#fff",
    fontWeight:"700",
    //텍스트의 현재 위치에서의 정렬 
    textAlign:"center"
  },
  middleButtonTextAll: {
    color:"#fff",
    fontWeight:"700",
    //텍스트의 현재 위치에서의 정렬 
    textAlign:"center"
  },
  cardContainer: {
    marginTop:10,
    marginLeft:10
  },
  aboutButton: {
    backgroundColor:"pink",
    width:100,
    height:40,
    borderRadius:10,
    alignSelf:"flex-end",
    marginRight:20,
    marginTop:10
  },
  aboutButtonText: {
    color:"#fff",
    textAlign:"center",
    marginTop:10
  }


});

import 문에서 또 *를 이용한 expo-location 기능 모두를 Location 변수에 담았고, 중간 getLocation 함수를 새롭게 만들어 useEffect에 당연히 올려놓았으며 함수 안에는 위치 탐지 기능을 허가 받을수 있는지 없는지 클라이언트에 물어보는 requestPermissionsAsync 기능을 먼저 올리고 그 다음 현재 위치를 탐지해 수치값을 리턴하는 getCurrentPositionAsync 기능을 사용해 LocationData 변수에 저장하고 마지막엔 console.log로 콘솔에 직접 출력하고 있다. 그리고 try ~ catch 구문은 자주 쓰이는 코드 문법으로 혹시나 에러가 날 만한 입력값을 받았거나 혹은 그런 상황에서 앱 화면이 멈추면 안되기에 먼저 try 안쪽 코드문을 시도해보고 오류가 났을시 catch 안쪽 코드문을 실행하는 구조로 되어있는 문법이다. 또한, async와 await가 왜 붙어있냐면 외부 API나 핸드폰 파일 시스템을 읽어오는거라던가 위치 정보 가져오기 등 이런 '무거운' 작업들이 진행된다면 리액트 기반인 자바스크립트 특성상 비동기로 인해 코드 순서로 진행될지 안될지 확실하지 않기에 그것을 강제로 정해주기 위해 async와 await를 쓴다고 하셨다. (바깥 함수 선언부 앞에 async, 안쪽 기능/함수들 앞에는 await를 쓴다.)

그러면 아래 그림과 같이 처음에는 위치 권한 허용 차단을 물어보고 허용이면 현재 위치에 따라 콘솔에도 위도, 경도가 출력된다고 한다.

이제 아까 말했던 날씨 외부 api, openweathermap api를 가져와야 하는데 (공식 설명 링크)

도메인 형식의 api이기때문에 axios라는 도구가 필요하다고 강사님은 말씀하셨다. (그리고 설치는 다음과 같은 코드를 터미널에 입력해야한다고 하셨다.)

yarn add axios

그리고 아래와 같은 코드문으로 MainPage.js를 수정해보고 바뀐 부분들을 강사님이 자세히 설명해주셨다.

import React,{useState,useEffect} from 'react';
import main from '../assets/main.png';
import { StyleSheet, Text, View, Image, TouchableOpacity, ScrollView} from 'react-native';
import data from '../data.json';
import Card from '../components/Card';
import Loading from '../components/Loading';
import { StatusBar } from 'expo-status-bar';
import * as Location from "expo-location";
import axios from "axios"

export default function MainPage({navigation,route}) {
  console.disableYellowBox = true;
  //return 구문 밖에서는 슬래시 두개 방식으로 주석

  //기존 꿀팁을 저장하고 있을 상태
  const [state,setState] = useState([])
  //카테고리에 따라 다른 꿀팁을 그때그때 저장관리할 상태
  const [cateState,setCateState] = useState([])

  //컴포넌트에 상태를 여러개 만들어도 됨
  //관리할 상태이름과 함수는 자유자재로 정의할 수 있음
  //초기 상태값으로 리스트, 참거짓형, 딕셔너리, 숫자, 문자 등등 다양하게 들어갈 수 있음.
  const [ready,setReady] = useState(true)

  useEffect(()=>{
	   
    //뒤의 1000 숫자는 1초를 뜻함
    //1초 뒤에 실행되는 코드들이 담겨 있는 함수
    setTimeout(()=>{
        //헤더의 타이틀 변경
        navigation.setOptions({
            title:'나만의 꿀팁'
        })
        //꿀팁 데이터로 모두 초기화 준비
        let tip = data.tip;
        setState(tip)
        setCateState(tip)
        getLocation()
        setReady(false)
    },1000)

    
  },[])

  const getLocation = async () => {
    //수많은 로직중에 에러가 발생하면
    //해당 에러를 포착하여 로직을 멈추고,에러를 해결하기 위한 catch 영역 로직이 실행
    try {
      //자바스크립트 함수의 실행순서를 고정하기 위해 쓰는 async,await
      await Location.requestPermissionsAsync();
      const locationData= await Location.getCurrentPositionAsync();
      const latitude = locationData['coords']['latitude']
      const longitude = locationData['coords']['longitude']
      const API_KEY = "api 마스터 키";
      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)

    } catch (error) {
      //혹시나 위치를 못가져올 경우를 대비해서, 안내를 준비합니다
      Alert.alert("위치를 찾을 수가 없습니다.", "앱을 껏다 켜볼까요?");
    }
  }

    const category = (cate) => {
        if(cate == "전체보기"){
            //전체보기면 원래 꿀팁 데이터를 담고 있는 상태값으로 다시 초기화
            setCateState(state)
        }else{
            setCateState(state.filter((d)=>{
                return d.category == cate
            }))
        }
    }


	let todayWeather = 10 + 17;
    let todayCondition = "흐림"

	//처음 ready 상태값은 true 이므로 ? 물음표 바로 뒤에 값이 반환(그려짐)됨
  //useEffect로 인해 데이터가 준비되고, ready 값이 변경되면 : 콜론 뒤의 값이 반환(그려짐)
  return ready ? <Loading/> :  (
    /*
      return 구문 안에서는 {슬래시 + * 방식으로 주석
    */
    <ScrollView style={styles.container}>
        <StatusBar style="black" />
        {/* <Text style={styles.title}>나만의 꿀팁</Text> */}
        <Text style={styles.weather}>오늘의 날씨: {todayWeather + '°C ' + todayCondition} </Text>
        <TouchableOpacity style={styles.aboutButton} onPress={()=>{navigation.navigate('AboutPage')}}>
          <Text style={styles.aboutButtonText}>소개 페이지</Text>
        </TouchableOpacity>
        <Image style={styles.mainImage} source={main}/>
        <ScrollView style={styles.middleContainer} horizontal indicatorStyle={"white"}>
            <TouchableOpacity style={styles.middleButtonAll} onPress={()=>{category('전체보기')}}><Text style={styles.middleButtonTextAll}>전체보기</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton01} onPress={()=>{category('생활')}}><Text style={styles.middleButtonText}>생활</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton02} onPress={()=>{category('재테크')}}><Text style={styles.middleButtonText}>재테크</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton03} onPress={()=>{category('반려견')}}><Text style={styles.middleButtonText}>반려견</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton04} onPress={()=>{navigation.navigate('LikePage')}}><Text style={styles.middleButtonText}>꿀팁 찜</Text></TouchableOpacity>
        </ScrollView>
        <View style={styles.cardContainer}>
            {/* 하나의 카드 영역을 나타내는 View */}
            {
            cateState.map((content,i)=>{
                return (<Card content={content} key={i} navigation={navigation}/>)
            })
            }
            
        </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    //앱의 배경 색
    backgroundColor: '#fff',
  },
  title: {
    //폰트 사이즈
    fontSize: 20,
    //폰트 두께
    fontWeight: '700',
    //위 공간으로 부터 이격
    marginTop:50,
    //왼쪽 공간으로 부터 이격
    marginLeft:20
  },
weather:{
    alignSelf:"flex-end",
    paddingRight:20
  },
  mainImage: {
    //컨텐츠의 넓이 값
    width:'90%',
    //컨텐츠의 높이 값
    height:200,
    //컨텐츠의 모서리 구부리기
    borderRadius:10,
    marginTop:20,
    //컨텐츠 자체가 앱에서 어떤 곳에 위치시킬지 결정(정렬기능)
    //각 속성의 값들은 공식문서에 고대로~ 나와 있음
    alignSelf:"center"
  },
  middleContainer:{
    marginTop:20,
    marginLeft:10,
    height:60
  },
  middleButtonAll: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#20b2aa",
    borderColor:"deeppink",
    borderRadius:15,
    margin:7
  },
  middleButton01: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#fdc453",
    borderColor:"deeppink",
    borderRadius:15,
    margin:7
  },
  middleButton02: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#fe8d6f",
    borderRadius:15,
    margin:7
  },
  middleButton03: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#9adbc5",
    borderRadius:15,
    margin:7
  },
  middleButton04: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#f886a8",
    borderRadius:15,
    margin:7
  },
  middleButtonText: {
    color:"#fff",
    fontWeight:"700",
    //텍스트의 현재 위치에서의 정렬 
    textAlign:"center"
  },
  middleButtonTextAll: {
    color:"#fff",
    fontWeight:"700",
    //텍스트의 현재 위치에서의 정렬 
    textAlign:"center"
  },
  cardContainer: {
    marginTop:10,
    marginLeft:10
  },
  aboutButton: {
    backgroundColor:"pink",
    width:100,
    height:40,
    borderRadius:10,
    alignSelf:"flex-end",
    marginRight:20,
    marginTop:10
  },
  aboutButtonText: {
    color:"#fff",
    textAlign:"center",
    marginTop:10
  }


});

최상단 임포트문에는 당연히 axios를 넣어놓았고 getLocation 함수 안 아까 코드문으로 구해진 위도 경도는 다 각자 따로 따로 latitude, longtitude 변수에 저장해놨고, axios.get 기능을 이용해 날씨 API 주소를 입력해 요청할수 있게 되어 주소 + 위도(latitude) + 경도(longtitude) + 그리고 API에 접속할수 있게 해주는 전용 키값(APP_KEY, api를 제공하는 업체에 가입후 받을수 있음)을 써준 코드문을 안에 넣고 돌리면 result 키값에 날씨 데이터가 저장된다. 그걸 console.log를 통해 콘솔로 출력했을때 아래와 같이 나온다.

이걸 날씨 API Response 공식 설명 링크와 같이 보면 data의 main의 temp 밸류 값이 기온이 되고, 똑같이 data의 weather 리스트의 0번째의 main 값이 현재 날씨 상황임을 알 수 있다. 코드로 변환하면 아래와 같다.

const temp = result.data.main.temp; 
const condition = result.data.weather[0].main

이걸 최종 반영해 수정한 MainPage.js의 모습은 아래와 같다.

import React,{useState,useEffect} from 'react';
import main from '../assets/main.png';
import { StyleSheet, Text, View, Image, TouchableOpacity, ScrollView} from 'react-native';
import data from '../data.json';
import Card from '../components/Card';
import Loading from '../components/Loading';
import { StatusBar } from 'expo-status-bar';
import * as Location from "expo-location";
import axios from "axios"

export default function MainPage({navigation,route}) {
  console.disableYellowBox = true;
  //return 구문 밖에서는 슬래시 두개 방식으로 주석

  //기존 꿀팁을 저장하고 있을 상태
  const [state,setState] = useState([])
  //카테고리에 따라 다른 꿀팁을 그때그때 저장관리할 상태
  const [cateState,setCateState] = useState([])

  //날씨 데이터 상태관리 상태 생성!
  const [weather, setWeather] = useState({
    temp : 0,
    condition : ''
  })

  //컴포넌트에 상태를 여러개 만들어도 됨
  //관리할 상태이름과 함수는 자유자재로 정의할 수 있음
  //초기 상태값으로 리스트, 참거짓형, 딕셔너리, 숫자, 문자 등등 다양하게 들어갈 수 있음.
  const [ready,setReady] = useState(true)

  useEffect(()=>{
	   
    //뒤의 1000 숫자는 1초를 뜻함
    //1초 뒤에 실행되는 코드들이 담겨 있는 함수
    setTimeout(()=>{
        //헤더의 타이틀 변경
        navigation.setOptions({
            title:'나만의 꿀팁'
        })
        //꿀팁 데이터로 모두 초기화 준비
        let tip = data.tip;
        setState(tip)
        setCateState(tip)
        getLocation()
        setReady(false)
    },1000)

    
  },[])

  const getLocation = async () => {
    //수많은 로직중에 에러가 발생하면
    //해당 에러를 포착하여 로직을 멈추고,에러를 해결하기 위한 catch 영역 로직이 실행
    try {
      //자바스크립트 함수의 실행순서를 고정하기 위해 쓰는 async,await
      await Location.requestPermissionsAsync();
      const locationData= await Location.getCurrentPositionAsync();
      const latitude = locationData['coords']['latitude']
      const longitude = locationData['coords']['longitude']
      const API_KEY = "api 마스터 키";
      const result = await axios.get(
        `http://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${API_KEY}&units=metric`
      );

      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("위치를 찾을 수가 없습니다.", "앱을 껏다 켜볼까요?");
    }
  }

    const category = (cate) => {
        if(cate == "전체보기"){
            //전체보기면 원래 꿀팁 데이터를 담고 있는 상태값으로 다시 초기화
            setCateState(state)
        }else{
            setCateState(state.filter((d)=>{
                return d.category == cate
            }))
        }
    }


  //실제 데이터를 넣을 예정이므로 주석!
	// let todayWeather = 10 + 17;
  // let todayCondition = "흐림"

	//처음 ready 상태값은 true 이므로 ? 물음표 바로 뒤에 값이 반환(그려짐)됨
  //useEffect로 인해 데이터가 준비되고, ready 값이 변경되면 : 콜론 뒤의 값이 반환(그려짐)
  return ready ? <Loading/> :  (
    /*
      return 구문 안에서는 {슬래시 + * 방식으로 주석
    */
    <ScrollView style={styles.container}>
        <StatusBar style="black" />
        {/* <Text style={styles.title}>나만의 꿀팁</Text> */}
        <Text style={styles.weather}>오늘의 날씨: {weather.temp + '°C   ' + weather.condition} </Text>
        <TouchableOpacity style={styles.aboutButton} onPress={()=>{navigation.navigate('AboutPage')}}>
          <Text style={styles.aboutButtonText}>소개 페이지</Text>
        </TouchableOpacity>
        <Image style={styles.mainImage} source={main}/>
        <ScrollView style={styles.middleContainer} horizontal indicatorStyle={"white"}>
            <TouchableOpacity style={styles.middleButtonAll} onPress={()=>{category('전체보기')}}><Text style={styles.middleButtonTextAll}>전체보기</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton01} onPress={()=>{category('생활')}}><Text style={styles.middleButtonText}>생활</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton02} onPress={()=>{category('재테크')}}><Text style={styles.middleButtonText}>재테크</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton03} onPress={()=>{category('반려견')}}><Text style={styles.middleButtonText}>반려견</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton04} onPress={()=>{navigation.navigate('LikePage')}}><Text style={styles.middleButtonText}>꿀팁 찜</Text></TouchableOpacity>
        </ScrollView>
        <View style={styles.cardContainer}>
            {/* 하나의 카드 영역을 나타내는 View */}
            {
            cateState.map((content,i)=>{
                return (<Card content={content} key={i} navigation={navigation}/>)
            })
            }
            
        </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    //앱의 배경 색
    backgroundColor: '#fff',
  },
  title: {
    //폰트 사이즈
    fontSize: 20,
    //폰트 두께
    fontWeight: '700',
    //위 공간으로 부터 이격
    marginTop:50,
    //왼쪽 공간으로 부터 이격
    marginLeft:20
  },
weather:{
    alignSelf:"flex-end",
    paddingRight:20
  },
  mainImage: {
    //컨텐츠의 넓이 값
    width:'90%',
    //컨텐츠의 높이 값
    height:200,
    //컨텐츠의 모서리 구부리기
    borderRadius:10,
    marginTop:20,
    //컨텐츠 자체가 앱에서 어떤 곳에 위치시킬지 결정(정렬기능)
    //각 속성의 값들은 공식문서에 고대로~ 나와 있음
    alignSelf:"center"
  },
  middleContainer:{
    marginTop:20,
    marginLeft:10,
    height:60
  },
  middleButtonAll: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#20b2aa",
    borderColor:"deeppink",
    borderRadius:15,
    margin:7
  },
  middleButton01: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#fdc453",
    borderColor:"deeppink",
    borderRadius:15,
    margin:7
  },
  middleButton02: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#fe8d6f",
    borderRadius:15,
    margin:7
  },
  middleButton03: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#9adbc5",
    borderRadius:15,
    margin:7
  },
  middleButton04: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#f886a8",
    borderRadius:15,
    margin:7
  },
  middleButtonText: {
    color:"#fff",
    fontWeight:"700",
    //텍스트의 현재 위치에서의 정렬 
    textAlign:"center"
  },
  middleButtonTextAll: {
    color:"#fff",
    fontWeight:"700",
    //텍스트의 현재 위치에서의 정렬 
    textAlign:"center"
  },
  cardContainer: {
    marginTop:10,
    marginLeft:10
  },
  aboutButton: {
    backgroundColor:"pink",
    width:100,
    height:40,
    borderRadius:10,
    alignSelf:"flex-end",
    marginRight:20,
    marginTop:10
  },
  aboutButtonText: {
    color:"#fff",
    textAlign:"center",
    marginTop:10
  }


});

자세히 보면 이제 최종적으로 API에서 데이터를 끌어왔기에 상태 관리(state)를 해야하기에 useState 구문을 또 넣어 변수로 weather, 상태를 수정할땐 setWeather로 두고 setWeather는 temp와 condition으로 구성되는, 객체 리터럴 방식으로 만든 딕셔너리로 만들어 넣으면 상태 관리는 완성되는 것이었다. 마지막으로 본문에 앱화면 우측 상단에 표시될 오늘의 날씨에 weather.temp와 weather.condition을 넣어주면 최종적으로 마무리가 된다. 그럼 아래 사진과 같이 앱 화면에 기온과 현재 날씨가 표시된다.


3. [파이어베이스] 파이어베이스(firebase) 소개

구글이 만든 서버 서비스 제공 프로그램, 파이어베이스는 서버리스 서비스 프로그램으로 사용자가 직접 서버를 만들기위해 코드를 짤 필요 없이 서버에 관한 모든 기능을 제공해주는 서비스를 뜻한다고 강사님이 말씀하셨다.

그러면서 강사님은 왜 이런 서버리스 프로그램을 이용해 서버를 만들어야 하는지 다시 한번 더 역설하셨는데, 첫번째는 앱 데이터 수정/변경으로 인한 업데이트를 간편화 하기 위해서라고 하셨다. 서버 없이 앱에 모든 파일을 저장하면 데이터 변경이 있을때마다 수정하고 사용자는 앱 업데이트를 해야하기 때문에 번거롭고 앱 용량 또한 무거워지기 때문에 핵심 내용인 데이터를 서버에 저장하고 데이터에 수정이 있을때마다 자동적으로 서버가 앱에 수정된 데이터만 전송하면 앱을 만든 개발자 및 사용자는 편하게 앱을 사용할수 있다.

두번째는 우리가 저번에 만든 꿀팁에서 찜하기 버튼이 있었는데 이 부분은 각 사용자마다 다른 데이터를 저장하게 될것이다. 그럼 이걸 서버에 따로 저장해두면 사용자는 다른 휴대폰에 로그인해도 그대로 사용자가 찜해둔, 저장해둔 데이터를 다시 받아서 볼수 있기 때문에 편의성이 증대된다.

그렇게 설명하신 뒤, 드디어 강사님이 파이어베이스를 다룰 차례라고 말씀하셨다.
(공식 파이어베이스 링크, 아래 사진은 파이어베이스가 제공하는 서비스들)

그렇게 공식 링크에 들어가서 구글 로그인이 안되어 있다면 로그인 후 시작하기 버튼 클릭, 프로젝트 만들기 들어가서 프로젝트를 만들면 일단 틀은 만들었다고 볼 수 있다.

(중간에 나오는 구글 애널리스틱스는 체크해두고 할것)


4. [파이어베이스] 앱 연동 및 파일 스토리지 사용

그렇게 파이어베이스도 시작을 했고 강사님이 말씀하시길 이제 이 파이어베이스와 우리가 지금까지 만들었던 앱을 연동시켜줘야 한다고 하셨다.

일단 지금까지 우리가 만들었던 앱은 자바스크립트 기반으로 만들었기 때문에 파이어베이스는 이를 웹으로 인식해서 웹 SDK를 이용해 서로 연결을 해줘야 한다고 강사님이 말씀하셨다.

일단 파이어베이스에 접속해서 시작하기 눌러 들어간뒤 우리가 아까 만들었던 프로젝트가 선택된 상황에서 아래 그림 순서대로 하면 SDK 코드단을 부여받는다. (SDK 코드단에 해당 파이어베이스 프로젝트 접속할수 있는 app key도 같이 들어있음)

이 코드단을 복사해 android visual studio code 프로그램에 우리가 만들었던 앱과 연동되도록 하면 완성된다.

혹시나 잊어버렸을 경우 아래 사진 순서와 같이 하면 다시 볼수 있다.

추가적으로, expo에도 firebase와 연동하는 기능도 설치해야하기에 android visual studio code 상 터미널에 아래와 같은 코드를 입력해 설치를 해야한다.

expo install firebase

(공식 문서 링크)

설치가 완료되었으면 firebaseConfig.js를 app.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: "API key",
    authDomain: "권한도메인",
    databseURL: "데이터베이스URL",
    projectId: "프로젝트id",
    storageBucket: "스토리지버켓",
    messagingSenderId: "메시징센더아이디",
    appId: "앱아이디",
    measurementId: "측정아이디" // Config 값은 사용자마다 다 다르니 따로 적지 않았음
};

//사용 방법입니다. 
//파이어베이스 연결에 혹시 오류가 있을 경우를 대비한 코드로 알아두면 됩니다.
if (!firebase.apps.length) {
    firebase.initializeApp(firebaseConfig);
}

export const firebase_db = firebase.database()

이렇게 firebaseConfig.js도 만들었으니 이제 앞으로 expo, android visual studio code에서 이 js 파일을 임포트해 파이어베이스 서버 기능을 요청할수 있다.

이렇게 앱 연동 사전 준비는 끝났고, 강사님이 말씀하시길 처음 사용하는 파이어베이스 서버 기능은 파일 저장소 스토리지(storage)라고 하셨다.

파일 저장소 스토리지는 이미지 혹은 파일을 서버에 올려두고 필요할때마다 꺼내 쓰는 기능이며 아래 사진 순서대로 스토리지 기능을 활성화 하면 된다.

이제 이미지는 파일 업로드 파란 버튼, 폴더를 추가하고 싶다면 옆 +폴더 그림을 누르면 폴더가 생성된다.

그렇게 위처럼 이미지 하나를 추가하면, 이미지 파일을 클릭한 상태로 오른쪽에 뜨는 디테일 패널에서 이름쪽에 파랗게 하이퍼링크가 활성화 된것을 볼 수 있는데, 이것을 클릭하면 새로운 웹 페이지가 뜨면서 이미지에 개별 웹 페이지 주소가 부여된다.

그러면서 강사님은 이제 이런 파일 저장소 스토리지 기능을 이용해서 앞서 우리가 만들었던 꿀팁 앱의 이미지를 이 방식으로 관리해주면 된다고 하셨다.


5. [파이어베이스] 리얼타임 데이터베이스 - 설정, 전체 데이터 읽기

강사님은 이번에는 1주차부터 우리가 줄곧 다루었던 리스트지만 내부 데이터들이 딕셔너리 형태를 띈, JSON 데이터들을 파이어베이스가 제공하는 데이터베이스 서비스로 관리하는 연습을 해볼거라고 하셨다. 명칭은 리얼타임 데이터베이스라고 하셨다.(플랫폼과 실시간으로 데이터를 주고 받기 때문에 붙여졌지만 우리는 이 기능을 쓰지는 않으실거라고 하셨다.)

리얼타임 데이터베이스를 시작하는 법은 아래 순서대로 하는것이었다.

(규칙-rules 딕셔너리 데이터 부분-read와 write를 "true"값으로 변경 필수! 왜냐하면 expo앱에서 데이터 조회 및 android visual studio code에서 데이터 수정도 할수 있으니까 true로 해야 가능!)

그리고 이제 우리가 꿀팁 앱에서 여태까지 써왔던 JSON 파일, data.json을 여기에 등록해야하기에 아래와 같은 사진 순서대로 파이어베이스에 올리면 된다.

이렇게 가져온 팁의 데이터들이 딕셔너리 구조로 저장되고, 데이터들끼리는 숫자 0 1 2 3 4처럼 리스트 형태로 마지막에 쌓임을 볼수있다. 이제 이렇게 tip을 파이어베이스 서버에 올렸기 때문에 image 칸에 이미지 주소를 변경해 이미지를 바꾸거나 나머지 키값에 대해 밸류값을 서버에 변경하면 그대로 앱에도 반영이 가능하다.

이제 data.json 파일을 서버 파이어베이스에 업로드했기 때문에 이를 앱에서 사용하기위해선 두가지만 남았는데 이를 사용하는 파이어베이스 함수 사용방법과 data.json이 지금 어떤 모습으로 파이어베이스에 저장되었는지만 (위치 주소) 알면 된다.

먼저 위치 주소 같은 경우에 서버 각각 다르므로 필자 본인 같은 경우에는 data.json 파일이 파이어베이스 실행창에 뜨는걸 기준으로 https://sparta-myhoneytip-(무작위 영어 숫자 조합)-default-rtdb.firebaseio.com/에서 tip 항목에 data.json이 위치해있다.

파이어베이스 사용 함수 부분은 공식 설명 링크를 알려주시면서 아래와 같은 코드로 데이터를 불러올수 있다고 강사님이 설명하셨다.

firebase_db.ref('/tip').once('value').then((snapshot) => {
   let tip = snapshot.val();
})

설명은 다음과 같이 설명해주셨다.

ref('/tip') 이 부분에서 /tip 영역에 가지고 오고 싶은 데이터의 주소를 넣어주면 됩니다. 이 주소 앞부분에는 https://sparta-myhoneytip.파이어베이스아이오.com/ 과 같은 기본 주소가 생략되어 있습니다.

firebaseConfig.js에서 이미 파이어베이스 계정을 세팅했기 때문에, 기본 주소와 정보들은 앱 내에서 사용하는 파이어베이스 함수들이 알고 있는 상태입니다

firebase_db.ref('/tip').once('value').then((snapshot) => {})
이 코드는 서버리스를 이용하여 데이터베이스를 조회하기 위해,
파이어베이스 측에서 정해놓은 API 사용방법입니다. 따라서 우린 공식 문서 그대로 사용 방법을 적용해야 합니다.

조회한 데이터는 snapshot 부분에 담겨서 {} 내부에서 사용할 수 있는데, 그 중 실제 우리에게 필요한 데이터는 snapshot.val()로 가져와 변수에 담아 사용할 수 있습니다.

그렇게 이 두 가지를 모두 적용해서 강사님 지시대로 실제 서버 데이터가 올라갈 MainPage.js를 수정하면 다음과 같은 코드로 수정했다.

import React,{useState,useEffect} from 'react';
import main from '../assets/main.png';
import { StyleSheet, Text, View, Image, TouchableOpacity, ScrollView} from 'react-native';
import data from '../data.json';
import Card from '../components/Card';
import Loading from '../components/Loading';
import { StatusBar } from 'expo-status-bar';
import * as Location from "expo-location";
import axios from "axios"
import {firebase_db} from "../firebaseConfig"

export default function MainPage({navigation,route}) {
  console.disableYellowBox = true;
  //return 구문 밖에서는 슬래시 두개 방식으로 주석

  //기존 꿀팁을 저장하고 있을 상태
  const [state,setState] = useState([])
  //카테고리에 따라 다른 꿀팁을 그때그때 저장관리할 상태
  const [cateState,setCateState] = useState([])

  //날씨 데이터 상태관리 상태 생성!
  const [weather, setWeather] = useState({
    temp : 0,
    condition : ''
  })

  //컴포넌트에 상태를 여러개 만들어도 됨
  //관리할 상태이름과 함수는 자유자재로 정의할 수 있음
  //초기 상태값으로 리스트, 참거짓형, 딕셔너리, 숫자, 문자 등등 다양하게 들어갈 수 있음.
  const [ready,setReady] = useState(true)

  useEffect(()=>{
	   
    //뒤의 1000 숫자는 1초를 뜻함
    //1초 뒤에 실행되는 코드들이 담겨 있는 함수
    setTimeout(()=>{
        //헤더의 타이틀 변경
        navigation.setOptions({
            title:'나만의 꿀팁'
        })
        firebase_db.ref('/tip').once('value').then((snapshot) => {
          console.log("파이어베이스에서 데이터 가져왔습니다!!")
          let tip = snapshot.val();
          setState(tip)
          setCateState(tip)
          getLocation()
          setReady(false)
        });
        // setTimeout(()=>{
        //     let tip = data.tip;
        //     setState(tip)
        //     setCateState(tip)
        //     getLocation()
        //     setReady(false)
        // },500)
    },1000)

    
  },[])

  const getLocation = async () => {
    //수많은 로직중에 에러가 발생하면
    //해당 에러를 포착하여 로직을 멈추고,에러를 해결하기 위한 catch 영역 로직이 실행
    try {
      //자바스크립트 함수의 실행순서를 고정하기 위해 쓰는 async,await
      await Location.requestPermissionsAsync();
      const locationData= await Location.getCurrentPositionAsync();
      const latitude = locationData['coords']['latitude']
      const longitude = locationData['coords']['longitude']
      const API_KEY = "api 마스터 키";
      const result = await axios.get(
        `http://api.openweathermap.org/data/2.5/weather?lat=${latitude}&lon=${longitude}&appid=${API_KEY}&units=metric`
      );

      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("위치를 찾을 수가 없습니다.", "앱을 껏다 켜볼까요?");
    }
  }

    const category = (cate) => {
        if(cate == "전체보기"){
            //전체보기면 원래 꿀팁 데이터를 담고 있는 상태값으로 다시 초기화
            setCateState(state)
        }else{
            setCateState(state.filter((d)=>{
                return d.category == cate
            }))
        }
    }


  //실제 데이터를 넣을 예정이므로 주석!
	// let todayWeather = 10 + 17;
  // let todayCondition = "흐림"

	//처음 ready 상태값은 true 이므로 ? 물음표 바로 뒤에 값이 반환(그려짐)됨
  //useEffect로 인해 데이터가 준비되고, ready 값이 변경되면 : 콜론 뒤의 값이 반환(그려짐)
  return ready ? <Loading/> :  (
    /*
      return 구문 안에서는 {슬래시 + * 방식으로 주석
    */
    <ScrollView style={styles.container}>
        <StatusBar style="black" />
        {/* <Text style={styles.title}>나만의 꿀팁</Text> */}
        <Text style={styles.weather}>오늘의 날씨: {weather.temp + '°C   ' + weather.condition} </Text>
        <TouchableOpacity style={styles.aboutButton} onPress={()=>{navigation.navigate('AboutPage')}}>
          <Text style={styles.aboutButtonText}>소개 페이지</Text>
        </TouchableOpacity>
        <Image style={styles.mainImage} source={main}/>
        <ScrollView style={styles.middleContainer} horizontal indicatorStyle={"white"}>
            <TouchableOpacity style={styles.middleButtonAll} onPress={()=>{category('전체보기')}}><Text style={styles.middleButtonTextAll}>전체보기</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton01} onPress={()=>{category('생활')}}><Text style={styles.middleButtonText}>생활</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton02} onPress={()=>{category('재테크')}}><Text style={styles.middleButtonText}>재테크</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton03} onPress={()=>{category('반려견')}}><Text style={styles.middleButtonText}>반려견</Text></TouchableOpacity>
            <TouchableOpacity style={styles.middleButton04} onPress={()=>{navigation.navigate('LikePage')}}><Text style={styles.middleButtonText}>꿀팁 찜</Text></TouchableOpacity>
        </ScrollView>
        <View style={styles.cardContainer}>
            {/* 하나의 카드 영역을 나타내는 View */}
            {
            cateState.map((content,i)=>{
                return (<Card content={content} key={i} navigation={navigation}/>)
            })
            }
            
        </View>
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: {
    //앱의 배경 색
    backgroundColor: '#fff',
  },
  title: {
    //폰트 사이즈
    fontSize: 20,
    //폰트 두께
    fontWeight: '700',
    //위 공간으로 부터 이격
    marginTop:50,
    //왼쪽 공간으로 부터 이격
    marginLeft:20
  },
weather:{
    alignSelf:"flex-end",
    paddingRight:20
  },
  mainImage: {
    //컨텐츠의 넓이 값
    width:'90%',
    //컨텐츠의 높이 값
    height:200,
    //컨텐츠의 모서리 구부리기
    borderRadius:10,
    marginTop:20,
    //컨텐츠 자체가 앱에서 어떤 곳에 위치시킬지 결정(정렬기능)
    //각 속성의 값들은 공식문서에 고대로~ 나와 있음
    alignSelf:"center"
  },
  middleContainer:{
    marginTop:20,
    marginLeft:10,
    height:60
  },
  middleButtonAll: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#20b2aa",
    borderColor:"deeppink",
    borderRadius:15,
    margin:7
  },
  middleButton01: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#fdc453",
    borderColor:"deeppink",
    borderRadius:15,
    margin:7
  },
  middleButton02: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#fe8d6f",
    borderRadius:15,
    margin:7
  },
  middleButton03: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#9adbc5",
    borderRadius:15,
    margin:7
  },
  middleButton04: {
    width:100,
    height:50,
    padding:15,
    backgroundColor:"#f886a8",
    borderRadius:15,
    margin:7
  },
  middleButtonText: {
    color:"#fff",
    fontWeight:"700",
    //텍스트의 현재 위치에서의 정렬 
    textAlign:"center"
  },
  middleButtonTextAll: {
    color:"#fff",
    fontWeight:"700",
    //텍스트의 현재 위치에서의 정렬 
    textAlign:"center"
  },
  cardContainer: {
    marginTop:10,
    marginLeft:10
  },
  aboutButton: {
    backgroundColor:"pink",
    width:100,
    height:40,
    borderRadius:10,
    alignSelf:"flex-end",
    marginRight:20,
    marginTop:10
  },
  aboutButtonText: {
    color:"#fff",
    textAlign:"center",
    marginTop:10
  }


});

최상단 임포트 문에서 firebaseConfig.js 파일에서 firebase_db를 임포트해오고, (firebase_db 변수는 firebase.database() 함수를 기반으로 한 expo 기능을 이용해 만든 해당 firebase 서버 접속을 위한 마스터 키를 담고 있음) useEffect 함수안에서 우리가 앞서 봤던 파이어베이스 데이터베이스의 데이터를 끌어오기 위한 함수 즉, firebase_db.ref ~ snapshot.val() 함수가 적절히 들어가 있음을 확인할수 있다.그리고 기존 흐름과 맞추기 위해 snapshot.val()로 담긴 데이터들은 tip 변수로 넘어가 그대로 쓰이면서 setTimeout은 파이어베이스 서버 API를 통해 불러올때 걸리는 시간이 있기에 더이상 쓰이지 않기에 주석처리를 했다.

결국 강사님이 보여준 아래 사진처럼 처음 앱이 어떻게 바뀌어 구동되는지 알수 있었다.

+) 2022-05-25일자 현재 터미널에 expo start 후 d 키를 입력해 개발자 모드에서 안드로이드 스마트폰으로 QR을 읽어내 안드로이드 폰에서 구현하려고 했을때 error code 500가 뜨면서 아래 사진처럼 오류가 난다.

이것은 현재 expo와 firebase간의 호환성 문제라 yarn을 쓰고 있는 지금 상황에서 터미널에 아래와 같은 코드를 입력해 파이어베이스 버젼을 낮춘 다음 expo를 다시 실행하면 된다.

yarn remove firebase
yarn add firebase@9.6.11

(npm 같은 경우는 remove 대신 uninstall을, add 대신 install을 하라고 하셨다.)

또는 위 사진처럼 metro.config.js 파일을 app.js와 같은 폴더상(프로젝트 폴더)에 만들어 아래와 같은 코드를 입력하고 서버를 실행하는것도 방법이라고 하셨다. (expo가 cjs 파일을 못 읽어 생기는 문제)

const { getDefaultConfig } = require("@expo/metro-config");

const defaultConfig = getDefaultConfig(__dirname);

defaultConfig.resolver.assetExts.push("cjs");

module.exports = defaultConfig;

6. [파이어베이스] 리얼타임 데이터베이스 - 특정 데이터 읽기, 쓰기

그렇게 전체 데이터를 서버로 옮겨서 앱에 불러오도록 하는것까지 해봤고, 강사님은 이제 서버에서 특정 데이터만 전달할수 있도록 앱 코드 구조를 바꿔본다고 하셨다.

여태까지 배웠던것처럼 우리는 꿀팁 앱에서 Card.js의 특정 카드 페이지 버튼을 눌러 DetailPage로 이동할때 naviagte 기능을 통해 전체 내용(content)을 함께 으로 그것을 일단 구현해놨다.

하지만, 강사님이 말씀하시길 이제는 그렇게 전체 내용이 아니라 인덱스 번호 즉, data.json에 보면 각 데이터마다 매겨져있는 idx 값만 주면 그 해당 데이터를 꺼내올수 있도록 만들어본다고 하셨다.

왜 굳이 이렇게 해야하냐면, 앱따라 상황따라 달라지겠지만 보통 무거운 데이터를 그렇게 통째로 옮기면 앱 속도가 느려질수도 있고 댓글 같이 실시간으로 변화하는 데이터들은 일일이 통째로 그렇게 옮기기가 힘들다. 그래서 서버에는 계속 변화된 데이터가 반영될테니까(리얼타임 데이터베이스), idx 번호를 기준으로 가장 최신인 서버 데이터를 가져와야 타당하기 때문이다.

그래서 일단 첫번째 작업으로 카드 페이지의 기반이 되는 Card.js를 아래와 같은 코드로 수정해야했다.

import React from 'react';
import {View, Image, Text, StyleSheet,TouchableOpacity} from 'react-native'

//MainPage로 부터 navigation 속성을 전달받아 Card 컴포넌트 안에서 사용
export default function Card({content,navigation}){
    return(
        //카드 자체가 버튼역할로써 누르게되면 상세페이지로 넘어가게끔 TouchableOpacity를 사용
        <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>
    )
}


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",
    }
});

잘보면 navigation.navigate 부분에서 마지막 두번째 인자 부분이 idx, 인덱스 번호로 바뀌었다.

그리고 이에 맞춰 idx 값을 받고 이를 기준 삼아 파이어베이스 서버에서 데이터 값을 찾아야 하는 DetailPage.js도 아래와 같은 코드로 수정한다.

import React,{useState,useEffect} from 'react';
import { StyleSheet, Text, View, Image, ScrollView,TouchableOpacity,Alert,Share } from 'react-native';
import * as Linking from 'expo-linking';
import {firebase_db} from "../firebaseConfig"

export default function DetailPage({navigation,route}) {

    const [tip, setTip] = useState({
        "idx":9,
        "category":"재테크",
        "title":"렌탈 서비스 금액 비교해보기",
        "image": "https://firebasestorage.googleapis.com/v0/b/sparta-image.appspot.com/o/lecture%2Frental.png?alt=media&token=97a55844-f077-4aeb-8402-e0a27221570b",
        "desc":"요즘은 정수기, 공기 청정기, 자동차나 장난감 등 다양한 대여서비스가 활발합니다. 사는 것보다 경제적이라고 생각해 렌탈 서비스를 이용하는 분들이 늘어나고 있는데요. 다만, 이런 렌탈 서비스 이용이 하나둘 늘어나다 보면 그 금액은 겉잡을 수 없이 불어나게 됩니다. 특히, 렌탈 서비스는 빌려주는 물건의 관리비용까지 포함된 것이기에 생각만큼 저렴하지 않습니다. 직접 관리하며 사용할 수 있는 물건이 있는지 살펴보고, 렌탈 서비스 항목에서 제외해보세요. 렌탈 비용과 구매 비용, 관리 비용을 여러모로 비교해보고 고민해보는 것이 좋습니다. ",
        "date":"2020.09.09"
    })
    
    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)
        });
    },[])

    const popup = () => {
        Alert.alert("팝업!!")
    }

    const share = () => {
        Share.share({
            message:`${tip.title} \n\n ${tip.desc} \n\n ${tip.image}`,
        });
    }

    const link = () => {
        Linking.openURL("https://spartacodingclub.kr")
    }
    return ( 
        // ScrollView에서의 flex 숫자는 의미가 없습니다. 정확히 보여지는 화면을 몇등분 하지 않고
        // 화면에 넣은 컨텐츠를 모두 보여주려 스크롤 기능이 존재하기 때문입니다. 
        // 여기선 내부의 컨텐츠들 영역을 결정짓기 위해서 height 값과 margin,padding 값을 적절히 잘 이용해야 합니다. 
        <ScrollView style={styles.container}>
            <Image style={styles.image} source={{uri:tip.image}}/>
            <View style={styles.textContainer}>
                <Text style={styles.title}>{tip.title}</Text>
                <Text style={styles.desc}>{tip.desc}</Text>
                <View style={styles.buttonGroup}>
                    <TouchableOpacity style={styles.button} onPress={()=>popup()}><Text style={styles.buttonText}>팁 찜하기</Text></TouchableOpacity>
                    <TouchableOpacity style={styles.button} onPress={()=>share()}><Text style={styles.buttonText}>팁 공유하기</Text></TouchableOpacity>
                    <TouchableOpacity style={styles.button} onPress={()=>link()}><Text style={styles.buttonText}>외부 링크</Text></TouchableOpacity>
                </View>
                
            </View>
            
        </ScrollView>
    
    )
}

const styles = StyleSheet.create({
    container:{
        backgroundColor:"#000"
    },
    image:{
        height:400,
        margin:10,
        marginTop:40,
        borderRadius:20
    },
    textContainer:{
        padding:20,
        justifyContent:'center',
        alignItems:'center'
    },
    title: {
        fontSize:20,
        fontWeight:'700',
        color:"#eee"
    },
    desc:{
        marginTop:10,
        color:"#eee"
    },
    buttonGroup: {
        flexDirection:"row",
    },
    button:{
        width:90,
        marginTop:20,
        marginRight:10,
        marginLeft:10,
        padding:10,
        borderWidth:1,
        borderColor:'deeppink',
        borderRadius:7
    },
    buttonText:{
        color:'#fff',
        textAlign:'center'
    }
})

최상단 임포트 문에서 firebase_db를 임포트 해왔음은 물론, useEffect 함수 안에서 Card.js가 넘겨준 인덱스 값은 route.params로 넘어갔고 이를 다시 idx에 대입해준 다음에 snapshot 함수에 idx를 넣어 idx를 기준으로 데이터 값을 불러오는 코드로 수정되어있음을 알 수 있었다.

이제 마지막으로 남은 것은 찜하기 버튼을 구현해서 내가 찜한 팁을 서버에 데이터로 보내 어디서든 다시 볼 수 있게 하는것이다.

그런데 이제 만약 사용자가 다수라면 그 사용자 각각 찜한 팁들이 다를거기에 각각의 사용자마다 id값을 주고 차별점을 두고 찜한 팁들을 관리하는 방향으로 나가야한다고 강사님은 덧붙이셨다.

그래서 expo에서 이 기능을 지원하고 있기에(신버젼 공식 설명 링크, 구버젼 공식 설명 링크) 공식 설명을 참조하여 아래와 같은 코드를 또 터미널에 입력해야했다.

expo install expo-constants // 구버젼
expo install expo-application // 신버젼

구버젼의 경우, 아래와 같은 코드가 이제 DetailPage.js에 들어가야 사용자마다 고유 id값이 부여될것이다.

import Constants from 'expo-constants';

console.log(Constants.installationId)

신버젼의 경우, 아래와 같은 코드가 이제 DetailPage.js에 들어가야 사용자마다 고유 id값이 부여될것이다.

import * as Application from 'expo-application';
const isIOS = Platform.OS === 'ios';


...

const like = async () => {

		let uniqueId;
		if(isIOS){
		  let iosId = await Application.getIosIdForVendorAsync();
		  uniqueId = iosId
		}else{
		  uniqueId = Application.androidId
		}
		
		console.log(uniqueId)

}

그리고 찜하기로 저장될 팁 내용, 데이터는 꿀팁 번호(idx)-이미지(image)-제목(title)-내용(desc)이므로 전체 내용이라고 할수 있다. (그리고 이 전체 내용은 useState 함수로 tip 변수로 저장되어 상태 관리가 되고 있다.)

또한, 마지막으로 꿀팁 찜하기 버튼을 눌렀을때 해당 페이지가 저장되어야 하므로 해당 버튼의 onPress 함수에 따로 데이터를 저장하는 파이어베이스 함수를 만들고 선언해줘야 한다. (현재 popup 함수만 설정되어있는 상태)

그래서 이 모든걸 합쳐 DetailPage.js를 수정하면 아래와 같다.(구버젼 기준)

import React,{useState,useEffect} from 'react';
import { StyleSheet, Text, View, Image, ScrollView,TouchableOpacity,Alert,Share } from 'react-native';
import * as Linking from 'expo-linking';
import {firebase_db} from "../firebaseConfig"
import Constants from 'expo-constants';

export default function DetailPage({navigation,route}) {
    let user_idx = Constants.installationId
    console.log(user_idx)
    const [tip, setTip] = useState({
        "idx":9,
        "category":"재테크",
        "title":"렌탈 서비스 금액 비교해보기",
        "image": "https://firebasestorage.googleapis.com/v0/b/sparta-image.appspot.com/o/lecture%2Frental.png?alt=media&token=97a55844-f077-4aeb-8402-e0a27221570b",
        "desc":"요즘은 정수기, 공기 청정기, 자동차나 장난감 등 다양한 대여서비스가 활발합니다. 사는 것보다 경제적이라고 생각해 렌탈 서비스를 이용하는 분들이 늘어나고 있는데요. 다만, 이런 렌탈 서비스 이용이 하나둘 늘어나다 보면 그 금액은 겉잡을 수 없이 불어나게 됩니다. 특히, 렌탈 서비스는 빌려주는 물건의 관리비용까지 포함된 것이기에 생각만큼 저렴하지 않습니다. 직접 관리하며 사용할 수 있는 물건이 있는지 살펴보고, 렌탈 서비스 항목에서 제외해보세요. 렌탈 비용과 구매 비용, 관리 비용을 여러모로 비교해보고 고민해보는 것이 좋습니다. ",
        "date":"2020.09.09"
    })
    
    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)
        });
    },[])

    const like = () => {
        
        // like 방 안에
        // 특정 사용자 방안에
        // 특정 찜 데이터 아이디 방안에
        // 특정 찜 데이터 몽땅 저장!
        // 찜 데이터 방 > 사용자 방 > 어떤 찜인지 아이디
        const user_id = Constants.installationId;
        firebase_db.ref('/like/'+user_id+'/'+ tip.idx).set(tip,function(error){
            console.log(error)
            Alert.alert("찜 완료!")
        });
    }

    const share = () => {
        Share.share({
            message:`${tip.title} \n\n ${tip.desc} \n\n ${tip.image}`,
        });
    }

    const link = () => {
        Linking.openURL("https://spartacodingclub.kr")
    }
    return ( 
        // ScrollView에서의 flex 숫자는 의미가 없습니다. 정확히 보여지는 화면을 몇등분 하지 않고
        // 화면에 넣은 컨텐츠를 모두 보여주려 스크롤 기능이 존재하기 때문입니다. 
        // 여기선 내부의 컨텐츠들 영역을 결정짓기 위해서 height 값과 margin,padding 값을 적절히 잘 이용해야 합니다. 
        <ScrollView style={styles.container}>
            <Image style={styles.image} source={{uri:tip.image}}/>
            <View style={styles.textContainer}>
                <Text style={styles.title}>{tip.title}</Text>
                <Text style={styles.desc}>{tip.desc}</Text>
                <View style={styles.buttonGroup}>
                    <TouchableOpacity style={styles.button} onPress={()=>like()}><Text style={styles.buttonText}>팁 찜하기</Text></TouchableOpacity>
                    <TouchableOpacity style={styles.button} onPress={()=>share()}><Text style={styles.buttonText}>팁 공유하기</Text></TouchableOpacity>
                    <TouchableOpacity style={styles.button} onPress={()=>link()}><Text style={styles.buttonText}>외부 링크</Text></TouchableOpacity>
                </View>
                
            </View>
            
        </ScrollView>
    
    )
}

const styles = StyleSheet.create({
    container:{
        backgroundColor:"#000"
    },
    image:{
        height:400,
        margin:10,
        marginTop:40,
        borderRadius:20
    },
    textContainer:{
        padding:20,
        justifyContent:'center',
        alignItems:'center'
    },
    title: {
        fontSize:20,
        fontWeight:'700',
        color:"#eee"
    },
    desc:{
        marginTop:10,
        color:"#eee"
    },
    buttonGroup: {
        flexDirection:"row",
    },
    button:{
        width:90,
        marginTop:20,
        marginRight:10,
        marginLeft:10,
        padding:10,
        borderWidth:1,
        borderColor:'deeppink',
        borderRadius:7
    },
    buttonText:{
        color:'#fff',
        textAlign:'center'
    }
})

최상단 임포트문에는 사용자마다 고유한 id값을 부여하는 expo-application 기능을 임포트 해왔음을 알수 있고, 중간에 like 함수를 따로 만들어 아까 고유한 id 값 부여하는 코드문을 적고 마지막에 firebase.ref로 시작해 like(찜 데이터 총집합)-uniqueId(고유한 아이디값)-tip.idx(어떤 팁 찜한건지 알려주는 인덱스넘버)로 데이터베이스 주소를 만들고 .set 기능을 이용, tip(전체 데이터)를 변수 받아 결국 해당 주소에 저장되는 구조로 진행되고 있음을 알 수 있다. (두번째 인자로 받은 function(error)는 혹여나 에러 상황 발생을 대비해 준비해둠)

그렇게 찜이 완료되면 alert 기능으로 "찜 완료!" 알림창까지 뜨게 만드는것까지 구현해두었다.

이 like 함수를 팁 찜하기 버튼에 onPress 함수로 달아놓은것까지 확인하면 다 완성되었음을 알 수 있을것이다.

이렇게 다 완성하면 아래 그림과 같이 팁 찜하기까지 구현된 앱의 모습을 볼 수 있다.

또한, 아래와 같이 파이어베이스에 찜한 팁까지 데이터로 저장되어있음을 확인할수 있었다.


7. 4주차 숙제

강사님은 이제 앱을 만드는 모든 절차를 다 다루어보았다면서 1주차부터 앱 다루는 기초 문법부터 시작해 앱 프론트 화면을 만드는 2,3주차를 거쳐 4주차 서버에 이미지, 데이터 올리는 작업까지 이렇게 하면 앱이 일반적으로 완성되는 순서라고 하셨다. 그러면서 복습 겸 우리가 3주차 숙제때 만들었던 LikePage.js를 4주차 숙제로서 채우고 제출해볼 차례라고 하셨다.

먼저, LikePage.js에 찜 데이터 전부 보여주는 기능을 구현해야 했다. 이건 조금 쉬운 숙제로 우리가 앞서 해봤던 firebase.db의 snapshot 기능을 이용하면 가능했다. 아래 코드로 LikePage.js를 수정하기 시작했다.

import React,{useState, useEffect} from 'react';
import {ScrollView, Text, StyleSheet} from 'react-native';
import LikeCard from '../components/LikeCard';
import Card from '../components/Card';
import Constants from 'expo-constants';
import {firebase_db} from "../firebaseConfig"

export default function LikePage({navigation,route}){
    
    const [tip, setTip] = useState([])

    useEffect(()=>{
        navigation.setOptions({
            title:'꿀팁 찜'
        })
        const user_id = Constants.installationId;
        firebase_db.ref('/like/'+user_id).once('value').then((snapshot) => {
            console.log("파이어베이스에서 데이터 가져왔습니다!!")
            let tip = snapshot.val();
            setTip(tip)
        })
    },[])

    return (
        <ScrollView style={styles.container}>
           {
               tip.map((content,i)=>{
                   return(<LikeCard key={i} content={content} navigation={navigation}/>)
               })
           }
        </ScrollView>
    )
}

const styles = StyleSheet.create({
    container:{
        backgroundColor:"#fff"
    }
})

그럼 아래 그림과 같이 찜한 팁들이 꿀팁 찜 페이지에 저장된다.

그리고 두번째 숙제는 찜한 데이터가 없을 경우를 대비한 코드를 추가하는것이었다.
찜한 데이터가 없으면 오류가 나기에, 이를 방지하기 위해서 우리가 앞서 썼던 로딩창과 useState 추가, 그리고 어떤 코드를 추가해야했는데 그것은 tip.length라고 살짝 힌트를 주셨다.

그래서 그 부분까지 반영한 LikePage.js는 다음과 같다.

import React,{useState, useEffect} from 'react';
import {ScrollView, Text, StyleSheet} from 'react-native';
import LikeCard from '../components/LikeCard';
import Loading from '../components/Loading';
import Constants from 'expo-constants';
import {firebase_db} from "../firebaseConfig"

export default function LikePage({navigation,route}){
    
    const [tip, setTip] = useState([])
    const [ready,setReady] = useState(true)
    
    useEffect(()=>{
        navigation.setOptions({
            title:'꿀팁 찜'
        })
        const user_id = Constants.installationId;
        firebase_db.ref('/like/'+user_id).once('value').then((snapshot) => {
            console.log("파이어베이스에서 데이터 가져왔습니다!!")
            let tip = snapshot.val();
            console.log(tip)
            let tip_list = Object.values(tip)
            if(tip_list.length > 0){
                setTip(tip_list)
                setReady(false)
            }
           
        })
    },[])

    return ready ? <Loading/> : (
        <ScrollView style={styles.container}>
           {
               tip.map((content,i)=>{
                   return(<LikeCard key={i} content={content} navigation={navigation}/>)
               })
           }
        </ScrollView>
    )
}

const styles = StyleSheet.create({
    container:{
        backgroundColor:"#fff"
    }
})

(내 개발 환경 같은 경우에는 tip.length > 0을 그냥 넣으면 잘 작동하지 않았기 때문에 (리스트 형태로 데이터가 tip 안에 들어가지 않았기 때문에) 여기서도 힌트를 주셨는데 그것은 딕셔너리 안에 딕셔너리로 이루어진 데이터일 경우 Object.values()를 이용하면 키에 연결된 value(값)만으로 리스트를 만들수가 있는것이었다. 이를 이용하니 꿀팁 찜하기가 저장된 찜이 있든 없든 에러 없이 잘 나타났다.

그러면 아래 그림과 같이 처음 꿀팁 찜한 데이터가 없을땐 로딩창만 뜨고, 찜한 데이터가 있을땐 정상적으로 찜한 데이터들이 나오는 화면이 구현되었다.

세번째,네번째,다섯번째는 꿀팁 찜 페이지에 기능 버튼 두개, 자세히보기와 찜 해제를 구현하는것이었다.

자세히보기는 내가 찜한 페이지(DetailPage)에 이동하는것이기에 이것은 navigation 기능을 이용하면 되었고, 찜 해제는 데이터 삭제(공식 설명 링크, 스택 오버 플로우 참고 링크)를 이용해야했는데 remove()함수를 firebase_db.ref 경로와 적절히 사용하면 될듯 하였다. 추가로 찜한 뒤 꿀팁 찜 페이지도 다시 새로고침 해야했기에 찜 해제(데이터 삭제)뒤 navigation 기능으로 LikePage를 다시 불러오는 방향도 넣으라고 하셨다.

그런 결과가 LikeCard.js를 먼저 아래 코드와 같이 수정하고,

import React from 'react';
import {View, Image, Text, StyleSheet,TouchableOpacity, Alert} from 'react-native'
import {firebase_db} from "../firebaseConfig"
import Constants from 'expo-constants';


//MainPage로 부터 navigation 속성을 전달받아 Card 컴포넌트 안에서 사용
export default function LikeCard({content,navigation,reload}){

    const detail = () => {
        navigation.navigate('DetailPage',{idx:content.idx})
    }

    const remove = () => {
        const user_id = Constants.installationId;
        firebase_db.ref('/like/'+user_id+'/'+content.idx).remove().then(function(){
            Alert.alert("삭제 완료");
            // navigation.navigate('LikePage')
            reload()
        })
    }
    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()}><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:80,
      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:100,
        height:45,
        marginTop:20,
        marginRight:10,
        marginLeft:10,
        padding:10,
        borderWidth:1,
        borderColor:'deeppink',
        borderRadius:7
    },
    buttonText:{
        color:'deeppink',
        textAlign:'center'
    }
});

(보면 마지막 View 태그로 따로 영역을 준 뒤(buttonGroup) TouchableOpacity 두개를 입력, 각각 자세히보기-detail 함수, 찜 해제-remove 함수를 onPress로 연결하고 detail 함수에는 navigation 기능으로 idx까지 전달하도록 잘 구현했지만, remove에서는 firebase_db.ref와 remove() 함수와 결합해 데이터 삭제는 가능했지만, 리액트와 navigation 버전 문제로 내가 작업한 환경에선 마지막에 앞서 말한 navigation으로 LikePage를 새로고침을 하는 작동은 안되었다. 그래서 reload 함수를 대신 넣고 이 함수는 새로고침이 되어야 하는 LikePage에서 선언되어 그것을 받아와서 쓰는 방향으로 코드를 수정했다.(export default function LikeCard({content,navigation,reload})

그리고 마지막, LikePage.js는 최종적으로 아래와 같이 코드를 수정하여 마무리지었다.

import React,{useState, useEffect} from 'react';
import {ScrollView, Text, StyleSheet} from 'react-native';
import LikeCard from '../components/LikeCard';
import Loading from '../components/Loading';
import Constants from 'expo-constants';
import {firebase_db} from "../firebaseConfig"

export default function LikePage({navigation,route}){
    
    const [tip, setTip] = useState([])
    const [ready,setReady] = useState(true)
    
    useEffect(()=>{
        navigation.setOptions({
            title:'꿀팁 찜'
        })
        const user_id = Constants.installationId;
        firebase_db.ref('/like/'+user_id).once('value').then((snapshot) => {
            console.log("파이어베이스에서 데이터 가져왔습니다!!")
            let tip = snapshot.val();
            console.log(tip)
            let tip_list = Object.values(tip)
            if(tip_list.length > 0){
                setTip(tip_list)
                setReady(false)
            }
           
        })
    },[])

    const reload = () =>{
        const user_id = Constants.installationId;
        firebase_db.ref('/like/'+user_id).once('value').then((snapshot) => {
						//snapshot에 값이 있는지 없는지 체크하는 exists 함수 사용
            if(snapshot.exists()){
                let tip = snapshot.val();
                let tip_list = Object.values(tip)
                setTip(tip_list)
            }else{
                setReady(true)
                setTip([])
            }
            
        })
    }

    return ready ? <Loading/> : (
        <ScrollView style={styles.container}>
           {
               tip.map((content,i)=>{
                   return(<LikeCard key={i} reload={reload} content={content} navigation={navigation}/>)
               })
           }
        </ScrollView>
    )
}

const styles = StyleSheet.create({
    container:{
        backgroundColor:"#fff"
    }
})

문제의 reload 함수를 보면, 전반적으로 useEffect에 쓰였던 함수 내용과 같지만, snapshot의 값을 기준으로 exists 기능을 이용하여 값이 있으면 tip => tip_list로 넘기고 이걸 setTip으로 넘겨줘 로딩창을 벗어나 LikePage 새로고침이 되게 하고, 값이 없다면(else) 로딩창에서 머무르며 tip에는 어떠한 값도 들어가지 않고 빈 데이터만 남는다. 또한, LikePage.js에서 LikeCard로 reload 함수를 전달해야하니, 아래 tip.map 반복문을 돌릴때 reload 함수도 같이 전달한다.

그러면 아래 그림과 같이 자세히보기, 팁 해제등을 포함한 모든 숙제가 구현된 모습으로 앱이 작동한다.

그리고 5주차부터는 앱에 광고를 다는 연습을 해볼거기에 구글 애드몹 (AdMob)을 미리 가입해두라고 하셨다. (승인이 1~2일 정도 걸리기 때문에)

공식 링크에 들어가서 파이어베이스에 가입했던 계정 그대로 가입하고 대한민국 기준으로 내 계정 설정을 맞추었고 광고를 올릴 앱을 이름만이라도 일단 명단에 올리고 지급에 내 결제 및 인적사항까지 적고 가입 절차를 끝냈다.

0개의 댓글