리액트 네이티브 - 앱 페이지 적용 - 4 , 페이지 이동을 코드에 적용

하이루·2021년 10월 22일
0

App.js

  import React from 'react';
  
  //이제 모든 페이지 컴포넌트들이 끼워져있는 책갈피를 메인에 둘예정이므로
  //컴포넌트를 더이상 불러오지 않아도 됩니다.
  // import MainPage from './pages/MainPage';
  // import DetailPage from './pages/DetailPage';
  
  import { StatusBar } from 'expo-status-bar';

  //메인에 세팅할 네비게이션 도구들을 가져옵니다.
  import {NavigationContainer} from '@react-navigation/native';
  import StackNavigator from './navigation/StackNavigator'

  export default function App() {

    console.disableYellowBox = true;

    return (   
    <NavigationContainer>
      <StatusBar style="black" />
      <StackNavigator/>
   </NavigationContainer>);
  }

--> 처음에 시작되는 js 파일
--> 시작하면 스택네비게이터를 관리하기 위해 만든 StackNavigator.js 파일을 불러온다.
--> 위의 태그는 NavigationContainer로 감싸여있다.(Navigator를 감싸는 역할을 하는 태그)

StackNavigator.js

  import React from 'react';
  //설치한 스택 네비게이션 라이브러리를 가져옵니다
  import { createStackNavigator } from '@react-navigation/stack';

  //페이지로 만든 컴포넌트들을 불러옵니다
  import DetailPage from '../pages/DetailPage';
  import MainPage from '../pages/MainPage';

  //스택 네비게이션 라이브러리가 제공해주는 여러 기능이 담겨있는 객체를 사용합니다
  //그래서 이렇게 항상 상단에 선언하고 시작하는게 규칙입니다!
  const Stack = createStackNavigator();


  const StackNavigator = () =>{
      return (

          //컴포넌트들을 페이지처럼 여기게끔 해주는 기능을 하는 네비게이터 태그를 선언합니다.
          //위에서 선언한 const Stack = createStackNavigator(); Stack 변수에 들어있는 태그를 꺼내 사용합니다.
          //Stack.Navigator 태그 내부엔 페이지(화면)를 스타일링 할 수 있는 다양한 옵션들이 담겨 있습니다.
          <Stack.Navigator
              screenOptions={{
                  headerStyle: {
                      backgroundColor: "white",
                      borderBottomColor: "white",
                      shadowColor: "white",
                      height:100
                  },
                  //헤더의 텍스트를 왼쪾에 둘지 가운데에 둘지를 결정
                  headerTitleAlign:'left',
                  headerTintColor: "#000",
                  headerBackTitleVisible: false
              }}

          >

              {/* 컴포넌트를 페이지로 만들어주는 엘리먼트에 끼워 넣습니다. 이 자체로 이제 페이지 기능을 합니다*/}
              <Stack.Screen name="MainPage" component={MainPage}/>
              <Stack.Screen name="DetailPage" component={DetailPage}/>
          </Stack.Navigator>
      )
  }

  export default StackNavigator;

--> 스택 네비게이터를 관리하기 위한 js 파일이다.

--> 네비게이터를 통해 이동하기 위해서 앱에서 사용할 컴포넌트들을 페이지로서 등록해줘야한다.

          <Stack.Screen name="MainPage" component={MainPage}/>
          <Stack.Screen name="DetailPage" component={DetailPage}/>
          
     --> 이 때 MainPage쪽이 제일 위쪽에서 등록되었기 떄문에 앱에는 MainPage가 화면에 등장한다.(호출된다.)

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';

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)
        setReady(false)
    },1000)


  },[])

    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>
        <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={()=>{category('꿀팁 찜')}}><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
  },


});

StackNavigator.js에서 등록되었다는 것 만으로 해당 파일은 navigation과 route을 사용할 수 있음

	......
    import Loading from '../components/Loading';
    import { StatusBar } from 'expo-status-bar';

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

---> export default function MainPage({navigation,route}) { 부분에서 속성으로 navigation과 route를 받음

--> MainPage.js는 이 앱의 논리상 바로 위의 StackNavigator.js 파일에서 태그 형식으로 호출된다.
따라서 navigation과 route를 속성(props)으로서 받아 MainPage가 사용할 수 있게 된 것이다.

useEffect함수에서 navigator 또한 초기화 시켜준다.

	.....
  useEffect(()=>{

    //뒤의 1000 숫자는 1초를 뜻함
    //1초 뒤에 실행되는 코드들이 담겨 있는 함수
    setTimeout(()=>{
        //헤더의 타이틀 변경
        navigation.setOptions({
            title:'나만의 꿀팁'
        })
        ......

태그에 navigator를 속성을 사용하여 클릭시 해당 태그에 navigation을 전달해줌

--> 예) 아래의 card 태그에 navigator속성으로 navigator를 전달하여, card.js 파일에서 해당 컴포넌트를 클릭하면 다른 컴포넌트로 이동하는 설정할 할 수 있게 만듬

   <View style={styles.cardContainer}>
            {/* 하나의 카드 영역을 나타내는 View */}
            {
            cateState.map((content,i)=>{
                return (<Card content={content} key={i} navigation={navigation}/>)
            })
            }

        </View>
        

--> 즉, MainPage.js의 경우 StackNavigator의 Stack.Screen태그를 통해 직접 호출되었기 때문에 navigation을 속성으로 전달받아 사용할 수 있다.
--> 하지만 태그로 삽입된 js파일인 Card의 경우, navigation속성을 따로 받지 못했기 때문에 이대로는 내부적으로 스택 네비게이터를 통한 컴포넌트 이동설정을 해줄 수 없다.
--> 따라서 속성으로 navigation을 넘겨주어 스택네비게이션을 통한 컴포넌트 이동이 내부적으로 가능하도록 만든 것이다.

Card.js --> 1) (그냥 navigate) 컴포넌트로 이동설정

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

Card.js는 MainPage.js로부터 navigation을 속성으로 전달받아 사용할 수 있음

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

TouchableOpacity태그에 onPress속성을 이용하여 네비게이터 설정

    <TouchableOpacity style={styles.card} onPress={()=>{navigation.navigate('DetailPage')}}>
    

--> 해당 컴포넌트 클릭 시 navigation.navigate('DetailPage')함수를 실행하여 DetailPage라는 name속성으로 Stack.Screen에 등록된 컴포넌트로 이동함

Card.js --> 2) (navigate) 컴포넌트로 이동 + 데이터 전달 설정

 <TouchableOpacity style={styles.card} onPress={()=>{navigation.navigate('DetailPage',content)}}>

--> navigate함수에 전달할 데이터를 두번째 매개변수로 추가해주면 해당 매개변수를 통해 데이터가 전달된다.

      --> 해당 content는 card의 내용(제목,카테고리,내용 등등)에 해당하는 딕셔너리 데이터
                   --> 이것을 DetailPage에 전달하여 상세페이지가 해당 내용을 설명할 수 있게 할 것임
                   

[여기서 사용된 content 딕셔너리 중 하나 예시]

                          {
                  "idx":0,
                  "category":"생활",
                  "title":"먹다 남은 피자를 촉촉하게!",
                  "image":"https://firebasestorage.googleapis.com/v0/b/sparta-image.appspot.com/o/lecture%2Fpizza.png?alt=media&token=1a099927-d818-45d4-b48a-7906fd0d2ad3",
                  "desc":"먹다 남은 피자는 수분이 날라가기 때문에 처음처럼 맛있게 먹을 수 없는데요. 이럴 경우 그릇에 물을 받아 전자레인지 안에서 1분 30초에서 2분 정도 함께 돌려주면 촉촉하게 먹을 수 있습니다. 물이 전자레인지 안에서 수증기를 일으키고, 피자에 촉촉함을 더해줍니다.",
                  "date":"2020.09.09"
              },
              .......
              
              

DetailPage.js --> Card.js 2번 예시 기준(데이터도 전달됨)

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

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

                  //초기 컴포넌트의 상태값을 설정
                  //state, setState 뿐 아니라 이름을 마음대로 지정할 수 있음!
      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)

          //Card.js에서 navigation.navigate 함수를 쓸때 두번째 인자로 content를 넘겨줬죠?
    //content는 딕셔너리 그 자체였으므로 route.params에 고대~로 남겨옵니다.
    //즉, route.params 는 content죠!

          navigation.setOptions({
                                                  //setOptions로 페이지 타이틀도 지정 가능하고
              title:route.params.title,
                                                  //StackNavigator에서 작성했던 옵션을 다시 수정할 수도 있습니다. 
              headerStyle: {
                  backgroundColor: '#000',
                  shadowColor: "#000",
              },
              headerTintColor: "#fff",
          })
          setTip(route.params)
      },[])

      const popup = () => {
          Alert.alert("팝업!!")
      }
      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>
                  <TouchableOpacity style={styles.button} onPress={()=>popup()}><Text style={styles.buttonText}>팁 찜하기</Text></TouchableOpacity>
              </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"
      },
      button:{
          width:100,
          marginTop:20,
          padding:10,
          borderWidth:1,
          borderColor:'deeppink',
          borderRadius:7
      },
      buttonText:{
          color:'#fff',
          textAlign:'center'
      }
  })
  
  

Stack.Screen에서 등록된 컴포넌트이므로 navigation과 route를 사용할 수 있다.

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

navigation --> 해당 임포넌트에서 다른 임포넌트로 이동할 때 사용
route --> 다른 임포넌트에서 해당 임포넌트로 이동할 때 보내온 데이터가 있으면 그 데이터를 받는데 사용

Card.js에서 navigation.navigate 함수를 쓸때 두번째 인자로 content를 넘겨줬죠?
content는 딕셔너리 그 자체였으므로 route.params에 고대~로 남겨옵니다.
즉, route.params 는 content죠!

초기 컴포넌트의 상태값을 설정

              //state, setState 뿐 아니라 이름을 마음대로 지정할 수 있음!
              
  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"
  })

useState이용시 초기화 문제 해결방안 2가지

--> useState의 데이터값이 비어있는 상태에서 화면을 그리라고 하면 오류 발생

방법 1) 로딩화면 구현하여, 이후 바로 useEffect 를 실행시켜 초기화

방법 2) 의미없는 데이터를 이용하여 useState의 초기값을 직접 설정

--> 위에서는 2번 방법 사용중임

useEffect에서 헤더설정을 해주고, 이전 컴포넌트가 보낸 데이터(딕셔너리)를 route를 사용하여 받아온다

   useEffect(()=>{
          console.log(route)
          

     //Card.js에서 navigation.navigate 함수를 쓸때 두번째 인자로 content를 넘겨줬죠?
    //content는 딕셔너리 그 자체였으므로 route.params에 고대~로 남겨옵니다.
    //즉, route.params 는 content죠!
    

          navigation.setOptions({
                                                  //setOptions로 페이지 타이틀도 지정 가능하고
              title:route.params.title,
                                                  //StackNavigator에서 작성했던 옵션을 다시 수정할 수도 있습니다. 
              headerStyle: {
                  backgroundColor: '#000',
                  shadowColor: "#000",
              },
              headerTintColor: "#fff",
          })
          setTip(route.params)
      },[])

Card.js에서 navigation.navigate 함수를 쓸때 두번째 인자로 content를 넘겨줬죠?
content는 딕셔너리 그 자체였으므로 route.params에 고대~로 남겨옵니다.
즉, route.params 는 content죠!

이전 컴포넌트가 보낸 딕셔너리 데이터는 route.params에 담겨짐

--> 그 데이터를 useState의 데이터로 할당 --> 위 코드에는 tip이라는 이름으로 설정되어있음

   setTip(route.params)
 
 
  navigation.setOptions({
  
                   //setOptions로 페이지 타이틀도 지정 가능하고
                    title:route.params.title,
                    
                   //StackNavigator에서 작성했던 옵션을 다시 수정할 수도 있습니다. 
                    headerStyle: {
                        backgroundColor: '#000',
                        shadowColor: "#000",
                    },
                    headerTintColor: "#fff",
                })
                
                
                

route.params를 통해 tip(useState의 변수)에 넣은 데이터를 바탕으로 화면 설정

       <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>
                  <TouchableOpacity style={styles.button} onPress={()=>popup()}><Text style={styles.buttonText}>팁 찜하기</Text></TouchableOpacity>
              </View>

          </ScrollView>
          
        
        

중요 내용

navigation --> 해당 임포넌트에서 다른 임포넌트로 이동할 때 사용

navigation.navigate(이동하고자 하는 컴포넌트가 Stack.Screen에 등록할 때의 name속성명)

route --> 다른 임포넌트에서 해당 임포넌트로 이동할 때 보내온 데이터가 있으면 그 데이터를 받는데 사용 route.params에 저장되어있음

profile
ㅎㅎ

0개의 댓글