[앱개발 종합반] 3주 3일차 개발일지(3주 3-5강)


📱TIL App.303

앱 필수 기초지식 응용

1. component와 state를 이용한 로딩화면 만들기

0) 지난 시간 복습

import React,{useState,useEffect} from 'react';
import data from '../data.json';

export default function MainPage() {
  const [state,setState] = useState([])
	
  useEffect(()=>{
    setState(data)
  },[])

  let tip = state.tip;

  return (
    ...
    {
      tip.map((content,i)=>{
        return(<Card content={content} key={i}/>)
      })
    }
  )

지난 시간에 useState()를 이용해 상태 관리를 할 때, 상태(state) 변수인 state의 초기값이 빈 리스트([])로 설정된 상태이고 상태값에 데이터가 채워지지 않았다. 그래서 화면이 그려지고 난 후 setState 함수로 데이터를 가져오려고 해도 불러올 데이터가 없다는 에러가 떴었다. 오늘은 이 문제를 해결하기 위해 데이터를 준비시키는 로딩 화면을 만든다.



1) 로딩화면 component/MainPage 코드

로딩 페이지라고 해서 pages 폴더에 파일을 생성하는 줄 알았는데, components 폴더에 Loading.js 파일을 생성한다. 다음으로 MainPage.js 파일을 다음과 같이 수정한다.

// MainPage.js
import React,{useState,useEffect} from 'react';
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';
export default function MainPage() {
  
  // tip을 관리하기 위한 상태
  const [state,setState] = useState([])
	
  // 준비 상태 여부를 관리하기 위한 상태
  const [ready,setReady] = useState(true)

  useEffect(()=>{
    // 뒤의 1000 숫자는 1초를 뜻함
    // 1초 뒤에 실행되는 코드들이 담겨 있는 함수
    setTimeout(()=>{
      setState(data)
      setReady(false)
    },1000)
  },[])

  let tip = state.tip;

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

const styles = StyleSheet.create({
  ... 
}]

코드 상단을 보면, components 폴더에 있는 Loading.js 파일을 불러온다. 또, tip array를 관리하기 위한 상태뿐 아니라 ready, 즉 데이터가 준비되었는지 여부의 상태를 관리하는 새로운 상태값이 추가되었다. 이처럼, 상태는 component 안에 무수히 많이 존재할 수 있다.



2) setTimeout 함수

useEffect 함수를 보면 setTimeout이라는 새로운 함수가 들어왔다. 30DaysOfJS에서 한번 다뤘었는데, setTimeout은 함수 내용이 실행되기까지 기다리는 시간(지연 시간)을 설정할 수 있다. 실행될 함수, 그리고 지연 시간을 parameter로 받는다. 지연 시간은 milliseconds를 기준으로 하며, 1000ms는 1초를 뜻한다.

useEffect(()=>{
  setTimeout(()=>{
    setState(data)
    setReady(false)
  }, 1000)
}

이 함수는 ‘1초 기다렸다가(1초 동안) 상태 데이터를 넣고, 준비가 끝났다는 뜻으로 ready 변수에 false를 할당하라.’는 뜻이다. 위의 MainPage.js 코드를 보면, 준비 상태를 관리하는 useState 함수의 초기값은 true로 설정되어 있으며, 이는 ‘준비 중’임을 뜻한다. 여기서 setTimeout 지연 함수를 쓴 이유는, 준비 화면을 보이게 하기 위해서이다. 지연 시간을 조정하면 준비 화면이 보이는 시간(=데이터가 로딩되는 시간)을 조절할 수 있다. 또한, 로딩 화면이 보이는 동안(지연 시간 동안) setState 함수를 통해 data가 상태 변수인 state에 모두 담기므로(상태값이 채워지므로) 화면이 그려졌을 때 state.tip으로 데이터에 접근할 수 있다.



3) 삼항 연산자와 상태(state)의 콜라보

또, MainPage.js 코드를 보면, return 부분에 삼항 연산자(ternary operator)가 사용된 것을 볼 수 있다. 조건의 값이 참일 경우 ? 뒤의 값이, 거짓일 경우 : 뒤의 값이 실행되는 조건문이었다. 이 코드에서는 readytrue의 값을 가질 경우 <Loading/> component를 렌더링하고, readyfalse의 값을 가질 경우 MainPage를 렌더링한다. useEffectreturn문이 실행되어 화면이 그려진 후 실행되기 때문에, 초기 ready값은 true이므로 <Loading/> component가 실행된다. 로딩 화면이 그려진 이후 useEffect 함수가 실행되면 ready 값이 false로 설정되므로 상태값의 변화에 따라 화면이 변경된다. 즉, return 문이 다시 실행되면서 원래의 메인 화면이 그려지는 것이다. 이렇듯 상태 관리를 사용하면 데이터가 변경될 때마다 return문이 실행되며 화면이 다시 그려지며, 주식 프로그램의 그래프처럼 실시간으로 바뀌는 데이터를 반영해 화면이 바뀌도록 할 수 있다.


2. state를 이용한 카테고리 기능 넣기

0) 코드 예측해보기

현재 만들고 있는 앱의 메인 화면에는 좌우 ScrollView 태그를 이용한 버튼들이 있었다. 버튼을 클릭하면, 해당 카테고리의 팁들만 나오도록 카테고리 기능을 추가해볼 것이다. 상태가 바뀌면 화면이 바뀌므로, 버튼을 클릭하면 상태가 바뀌도록 새로운 상태값을 설정할 수 있을 것이다. 아래 data.json 파일을 보면 category라는 key값이 보인다. 이 값을 구분하는 조건문을 사용하면, 각 버튼을 클릭했을 때 화면에 보이는 리스트들을 조정할 수 있을 것 같다.

// data.json
{
  "tip":[
    {
      "idx":0,
      "category":"생활",
      "title":"먹다 남은 피자를 촉촉하게!",
      "image":"https://storage.googleapis.com/sparta-image.appspot.com/lecture/pizza.png",
      "desc":"먹다 남은 피자는...",
      "date":"2020.09.09"
    },
    ...
]}


1) MainPage 코드

import React, { useState, useEffect } from 'react';
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';
export default function MainPage() {
  
  const [state,setState] = useState([])
  const [cateState,setCateState] = useState([])
  const [ready,setReady] = useState(true)

  useEffect(()=>{
    setTimeout(()=>{
      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
      }))
    }
  }

  return ready ? <Loading/> : (
    <ScrollView style={styles.container}>
      ...
      <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>
        ...
      </ScrollView>
      <View style={styles.cardContainer}>
        {
          cateState.map((content,i)=>{
            return (<Card content={content} key={i}/>)
          })
        }
      </View>
    </ScrollView>
  )
}

const styles = StyleSheet.create({
  ...
})


3) 새로운 상태값 추가

카테고리를 관리하는 새로운 상태값 cateState가 추가되었다. 초기값이 빈 리스트(Array) 형태인 이유는, 데이터에 data.tip, 즉 JSON 데이터 리스트가 추가될 것이기 때문이다. useEffect 안의 setTimeout 함수 안에도, 로딩 화면이 보이는 지연 시간 동안 cateState에 데이터(tip)를 채우는 setCateState 함수가 추가되었다.



4) 각 state 살펴보기

state 상태값은 전체 데이터의 상태 관리를 담당한다. 이 상태값은 ‘전체보기’ 버튼을 누를 때마다 데이터를 data.json에서 불러오는 대신, state 값에서 바로 불러오기 위해 존재한다. 그리고, 카테고리에 따라 달라지는 데이터 cateState도 데이터를 data.json 파일에서 불러오지 않고, state에서 꺼내 사용한다. ready 상태값은 데이터의 준비 상태를 관리하기 위해 존재한다.



5) 새로운 함수 생성, onPress로 연결하기

// category 함수
const category = (cate) => {
  if (cate == "전체보기"){
    setCateState(state)
  } else {
    setCateState(state.filter((d)=>{
      return d.category == cate
    }))
  }
}

<TouchableOpacity style={styles.middleButton01} onPress={()=>{category('생활')}}><Text style={styles.middleButtonText}>생활</Text></TouchableOpacity>

아래쪽에는 category라는 함수가 설정되었다. 각 카테고리 버튼을 클릭했을 때, 이 함수는 onPress 속성으로 버튼과 연결되어 실행된다. 각 버튼의 onPress 속성을 보면, category 함수에 각 카테고리의 이름을 parameter(cate)로 넘기고 있다.

이 값이 ‘전체보기’일 경우 cateState 상태값을 변경하는 setCateState 함수는 cateState 상태값에 state, 즉 전체 데이터를 할당한다. ‘전체보기’가 아닌 다른 버튼을 클릭하면, setCateStatestate, 즉 전체 array 데이터에 대해 filter를 실행해 조건을 만족하는 새로운 array를 구성한다.



6) filter 반복문

filter()는 자바스크립트 문법으로, array의 element를 돌면서 조건에 맞는 element만을 포함한 새로운 array를 구성하고 이를 반환한다. 각 element에 대해 조건(return 뒤의 코드)이 참인 경우 새로운 리스트에 해당 element를 추가하는 식이다. 30DaysOfJS 노트와 공식 문서를 참고하자! 코드에서는 state 데이터 array의 각 element를 변수 d로 설정해, 각 element의 category key 값이 함수로 넘겨진 argument와 일치하는 array 데이터만 반환하도록 했다.



7) map 반복문의 범위 변경

<View style={styles.cardContainer}>
  {
    cateState.map((content,i)=>{
      return (<Card content={content} key={i}/>)
    })
  }
</View>

return 문에서는 map 반복문이 돌아가는 array가 state(전체 데이터)에서 cateState(각 카테고리에 맞는 데이터만)로 바뀌었다. 따라서 category 변수에 의해 필터링된 array가 반복문을 돌면서 카드 형식으로 화면에 출력될 것이다.


3. Expo로 앱 상태 바(Status Bar) 관리하기

Expo SDK에서는 Expo에서 제공하는 여러 앱 기능 도구들을 확인하고, 필요한 기능들을 선택해 적용할 수 있다. 사이트에 들어가보면, 카메라나 달력, 오디오처럼 다양한 기능들이 있다. 앱 상태 바(Status Bar)란, 앱의 상태에 따라 모바일 화면 맨 위에서 배터리나 시간, 와이파이 등을 표시해주는 것이다. 앱 상태 바의 default값은 흰색이기 때문에, 실행 중인 앱 화면을 보면 앱 위쪽에 아무런 정보가 뜨지 않는다.


expo install expo-status-bar

Expo에서 제공하는 기능들을 사용하려면, Expo 도구를 설치해야 한다. 터미널을 열고 창을 분할한 다음, 이용하고 싶은 기능의 공식 문서의 ‘Installation’ 부분 코드를 입력하면 된다. Status Bar를 설치하려면 위 코드를 입력하면 된다. 도구 설치가 완료되면 해당 기능을 각 페이지에서 사용할 수 있다.


// MainPage.js
import { StatusBar } from 'expo-status-bar';

return ready ? <Loading/> : (
  <ScrollView style={styles.container}>
    <StatusBar style="black"/>
    ...
)

이용하고 싶은 기능의 공식 문서에서 ‘Usage’와 ‘API’ 부분을 참고하면, 해당 기능을 어떻게 코드로 가져올 것인지 알 수 있다. 아래의 ‘StatusBarProps’ 부분을 참고하면 속성 이름과 가능한 속성값들을 확인할 수 있다.


4. 네비게이터 사용하기

1) 네비게이터(Navigator)

앞에서 AboutPage, MainPage, DetailPage의 총 세 개의 페이지들을 만들었는데, 아직은 페이지 간 이동은 불가능한 상태다. 네비게이터는 앱에 페이지 개념을 입혀주고(Component들을 페이지화 시켜주고), 페이지들끼리 이동을 가능하게 해 준다.

네비게이터 또한 Expo에서 개발하고 지원하는 도구(라이브러리)로, 앱 하단의 탭 버튼, 슬라이딩 효과 등 다양한 페이지 이동 기술들을 제공한다. React Navigation 공식 문서를 참고하자! 아래 코드와 같이 yarn을 이용해 네비게이션을 사용하는 데 필요한 최소한의 도구들만 설치했다. 두 번째 코드처럼 라이브러리들을 띄어쓰기로 연결해 한번에 설치할 수 있다. 실제 앱에서 페이지끼리 이동하기 위한 여러 도구들(스택 네비게이션과 탭 네비게이션 등)은 따로 설치를 해야 한다.

yarn add @react-navigation/native

expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view



2) 스택 네비게이션(Stack Navigation)

스택 네비게이션은 Component에 페이지 기능을 부여해주고(Component를 페이지화시키고), Component끼리, 즉 페이지끼리의 이동을 가능하게 해 준다. 앱은 페이지가 쌓이는 구조이기 때문에, ‘쌓는다’라는 뜻의 스택(Stack)이 이름에 사용된다. 예를 들어, MainPage, AboutPage, DetailPage를 차례로 접속했다면 ‘뒤로 가기’를 누를 때마다 한 층씩 벗겨내는 구조인 것이다.



3) Stack.Screen과 Stack.Navigator

앞에서 만들었던 여러 페이지들을 각각 Stack.Screen에 연결시킨다. 이 페이지들이 모이면 하나의 책 형태가 되는데, 여기에서 페이지들 간 이동을 위해 책갈피 개념이 사용된다. 이 책갈피를 Stack.Navigator라고 부른다. 네비게이터에서 추가적인 기능을 사용하기 위해서는 도구를 따로 설치해야 하므로, 아래 코드로 스택 네비게이션(페이지 이동 도구)을 설치한다.

yarn add @react-navigation/stack



4) 네비게이션 폴더/JS 파일 생성하기

프로젝트 폴더에 navigation 폴더를 생성한다. 이 폴더에서는 페이지들을 이동시키는 기술인 네비게이션 파일들만 관리된다. 그 다음, 폴더에 StackNavigator.js 파일을 생성한다. 이 파일에는 페이지를 이동시키는 기술들을 담는다.

// 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();

// React의 모든 파일은 컴포넌트라고 생각하고
// 페이지 기능을 해주는 모든 기능이 담겨 있는 컴포넌트를 만든다고 생각하세요!
const StackNavigator = () =>{

  return (
    // 컴포넌트들을 페이지처럼 여기게끔 해주는 기능을 하는 네비게이터 태그를 선언합니다.
    // 위에서 선언한 Stack 변수에 들어있는 태그를 꺼내 사용합니다.
    // Stack.Navigator 태그 내부엔 페이지(화면)를 스타일링 할 수 있는 다양한 옵션들이 담겨 있습니다.
    <Stack.Navigator
      screenOptions={{
        headerStyle: {
          backgroundColor: "black",
          borderBottomColor: "black",
          shadowColor: "black",
          height:100
        },
        headerTintColor: "#FFFFFF",
        headerBackTitleVisible: false
      }}
    >

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

export default StackNavigator;

코드를 차근차근 살펴보자. 코드 최상단에는 아까 설치한 리액트 네비게이션 스택 도구에서 createStackNavigator 함수를 꺼내왔다. 이 코드는 StackNavigator 공식 문서에서 상단에 정의하도록 정해져 있다. 다음으로, createStackNavigator 함수가 반환한 값을 Stack이라는 변수에 담는다. 이 변수는 함수 밖에 위치한 전역 변수로, 함수 안에서 사용될 수 있다.


코드의 전체 구조는 Stack.NavigatorStack.Screen의 중첩 구조로 되어 있다. JSX 문법 규칙에 맞게 Stack.Navigator가 전체 태그를 감싸고 있다. Stack.Navigator는 페이지를 모아둔 책의 책갈피, Stack.Screen은 각 페이지를 가리킨다. 이 두 코드는 상단의 Stack 변수에서 각각 NavigatorScreen을 꺼내 사용한 것이다. 코드 상단을 보면, 앞에서 만든 페이지들을 import해왔다. 불러온 component(페이지)들을 아래 element에 끼워 넣으면 페이지화시킬 수 있다.

<Stack.Screen name="" component={}/>



5) App.js에 네비게이션 추가하기

코드 하단을 보면 export default를 이용해서 StackNavigator 함수를 밖에서 쓸 수 있게끔 내보내고 있다. 이렇듯 Component를 페이지화했고, 페이지 간 이동을 가능하게 하는 네비게이션도 준비됐다면 최상단 Component인 App.js에 네비게이션 기능을 달아야 한다. 앱의 최상위 코드에 네비게이션 기능을 달아야 어디서든 원하는 페이지로 이동이 가능하다.

// 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>);
}

app303 header

파일을 저장하고 실행하면, 위와 같이 화면 상단에 헤더(header)가 생성된다. 헤더의 ‘MainPage’는 StackNavigator.js 파일 Stack.Screen 태그의 name 속성값이다. 이 name 속성은 페이지 상단의 제목을 나타내기도 하고, 각 페이지에서의 flag, 즉 페이지를 지칭하는 역할도 한다. 앱 상태 바가 보이지 않아서, App.jsstyle 속성을 light로 바꿨는데 적용이 되지 않았다. MainPage에서의 앱 상태 바(StatusBar) 스타일은 App.js가 아닌 MainPage.js 파일 속성을 따른다.


import { NavigationContainer } from '@react-navigation/native';

위 코드는 스택 네비게이션 설치 전, 기본적으로 세팅한 네비게이션 도구에서 NavigationContainer라는 도구를 불러온다. 만든 네비게이션을 자동차에 설치하는 느낌으로 받아들이면 될 것 같다. 코드 하단 return 문을 보면, 원래 있던 return (<MainPage/>) 대신 <NavigationContainer>라는 네비게이션이 장착되어 있다. 앞서 말했듯이, App.js는 제일 선두에서 화면을 그리는 최상위 코드이기 때문에, 이 파일에 네비게이션을 주면 사용자가 원하는 페이지로 이동시킬 수 있다.


import StackNavigator from './navigation/StackNavigator'

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

또한, StackNavigator.js 파일에서 export default를 사용해 파일이 외부에서 사용될 수 있도록 내보냈기 때문에, App.js 파일에서 해당 파일을 불러와 태그 형식으로 사용할 수 있다. 현재 StackNavigator.js 파일에서 <Stack.Screen> 태그로 MainPage가 먼저 입력되어있기 때문에 MainPage 화면이 먼저 띄워진다.



6) header 스타일 수정/title 삭제

// StackNavigator.js
<Stack.Navigator
  screenOptions={{
    headerStyle: {
      backgroundColor: "black",
      borderBottomColor: "black",
      shadowColor: "black",
      height:100
    },
    headerTintColor: "#FFFFFF",
    headerBackTitleVisible: false
  }}
>

위 코드는 StackNavigator.js 파일에서 header의 스타일을 지정한다. 배경색이나 경계선, 그림자 모두 검정색으로 설정되어 있다. 색상값을 모두 “white”로 바꾸면, 검정색으로 보이지 않던 앱 상태 바가 다시 나타나고, 흰색으로 설정되었던 header 제목(headerTintColor)이 보이지 않게 된다. 이 속성값을 검정색(“#000”)으로 주면 제목만 상단에 뜨게 된다. headerTitleAlign이라는 속성도 있는데, 공식 문서를 찾아보니 leftcenter의 속성값을 가질 수 있다. 공식 문서를 찾아볼 때는 ‘React Navigation’ 사이트 왼쪽 탭에서 createStackNavigator를 클릭한 후 ‘Options’를 참고하면 된다. 다음으로, ‘MainPage’라는 페이지 제목이 생겼기 때문에 ‘나만의 꿀팁’ 태그를 삭제한다. 결과 화면은 아래와 같다.



7) 페이지 이동하기

MainPage.js 화면에서, 각 꿀팁 리스트(Card.js component)를 클릭하면 상세 페이지(DetailPage.js)로 이동하게 만들고 싶다. 이 기능을 위해서는 스택 네비게이션이 제공하는 함수를 사용해야 한다.

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

StackNavigator.js 파일을 보면, MainPage와 DetailPage가 각각 <Stack.Screen>으로 연결되어 있다. <Stack.Screen>에 연결(등록)된 페이지(component)들은 스택 네비게이션으로부터 함수를 전달받고, 언제든지 가져다 쓸 수 있다. 즉, navigationroute라는 딕셔너리(Object)를 속성으로 넘겨받아 사용할 수 있다.


navigation.setOptions({
  title:'나만의 꿀팁'
})

navigation.navigate("DetailPage")

navigation.navigate("DetailPage",{
  title: title
})


// 전달받은 데이터를 받는 route 딕셔너리
// 비구조 할당 방식으로 route에 params 객체 키로 연결되어 전달되는 데이터를 꺼내 사용
// navigate 함수로 전달되는 딕셔너리 데이터는 다음과 같은 모습이기 때문입니다.
/*
  {
		route : {
			params :{
				title: title
			}
		}
	}
*/
const {title} = route.params;

navigation 객체는 setOptionsnavigate의 두 함수를 가진다. setOptions 함수에서는 딕셔너리(Object) 형태의 parameter를 넘겨주며, 해당 페이지의 제목이나 화면 스타일을 페이지별로 설정할 수 있다.

navigate 함수는 <Stack.Screen>에서의 name 속성값을 parameter로 넘기면 해당 페이지로 이동시킨다. navigate 함수의 두 번째 parameter로 딕셔너리(Object) 데이터를 넘길 수도 있는데, 이는 해당 페이지로 이동하면서 그 데이터를 route 딕셔너리(Object)로 전달한다. 예를 들어, MainPage에서 DetailPage로 넘어갈 때, DetailPage 화면에 띄울 데이터를 페이지 이동 시 같이 건네줄 수 있다. 데이터를 받는 페이지는 해당 데이터를 route에서 꺼내 써야 하는데, 위 코드의 주석 처리된 부분이 route의 구조다. 구조가 복잡하기 때문에, 비구조 할당 방식으로 필요한 key의 값만 꺼내 사용한다.


① 데이터 없이 페이지 이동하기

navigation.navigate("DetailPage")
// 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;

  // 기존 꿀팁을 저장하고 있을 상태
  const [state,setState] = useState([])

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

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

  useEffect(()=>{
	
    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 = "흐림"

    return ready ? <Loading/> : (
    <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}>
        {
          cateState.map((content,i)=>{
            return (<Card content={content} key={i} navigation={navigation}/>)
          })
        }
      </View>
    </ScrollView>
  );
}

MainPage 함수의 parameter를 보면, {navigation, route}처럼 비구조 할당 방식으로 데이터가 넘겨지고 있다. <Stack.Screen> 태그로 스택 네비게이션에 연결되어 함수를 제공받고 있기 때문에, 두 기능을 바로 가져다 쓸 수 있다. 위 코드를 실행시키면 헤더 텍스트로 ‘MainPage’가 아닌 ‘나만의 꿀팁’이 뜬다. 이는 useEffect 함수에서 setOptions를 이용해 페이지 제목을 강제로 바꾸었기 때문이다. setOptions 함수 적용 전에는 StackNavigator.js 파일에서의 name 속성값이 화면의 제목으로 설정된다.



<Card> 태그에도 navigation 속성값으로 {navigation} 데이터가 전달되고 있다. 이는 각 꿀팁 리스트를 클릭했을 때 각각에 해당하는 상세 페이지로 이동하기 위한 도구다.

// 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 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>
  )
}

코드 변경 전에는 <TouchableOpacity> 태그가 아닌 <View> 태그였지만, 카드 자체가 버튼 역할을 하도록, 또 onPress 속성으로 클릭했을 때 navigation 함수를 사용할 수 있도록 태그를 변경했다. 또, Card 함수의 parameter가 기존 {content}에서, MainPage.js에서 넘겨받은 navigation 함수를 사용하기 위해 {content, navigation}으로 수정되었다. navigation.navigate 함수는 각 꿀팁 리스트의 onPress 속성값에 사용되었다. 해당 함수는 parameter로 이동할 페이지의 name 속성값을 받아 그 페이지로 이동하게끔 해 준다. 실제 화면을 실행시켰을 때, 각 꿀팁 리스트를 누르면 DetailPage로 이동하고, 화면 왼쪽 상단에 자동으로 뒤로가기 버튼도 생성된 것을 볼 수 있다.


② 데이터 전달하며 페이지 이동하기

// DetailPage.js
export default function DetailPage() {
  console.disableYellowBox = true;
  const tip = {
    "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"
  }
  return (
  <ScrollView style={styles.container}>
    <View style={styles.imageContainer}>
      <Image
        source={{uri:tip.image}}
        style={styles.detailImage}
      />
    </View>
    <View style={styles.textContainer}>
      <Text style={styles.titleText}>{tip.title}</Text>
      <Text style={styles.descText}>{tip.desc}</Text>
      <TouchableOpacity style={styles.button}><Text style={styles.buttonText}> 찜하기</Text></TouchableOpacity>
    </View>
  </ScrollView>
  )
}

현재 DetailPage.js 파일에서는 위 코드처럼 tip 데이터가 하나의 값으로 고정되어 있기 때문에, 어떤 카드 리스트를 눌러도 미리 만들어 둔 똑같은 DetailPage.js 화면으로만 이동한다. 이는 Card.js component에 리스트 데이터를 넘겨주고 있지 않기 때문이다.


navigation.navigate("Detail",{
  title: title
})

위 코드처럼 navigate 함수에 두 번째 parameter로 딕셔너리(Object) 값을 넘겨주면, 페이지를 이동하며 해당 데이터를 같이 넘겨줄 수 있다.


// Card.js
export default function Card({content,navigation}){
  return(
    <TouchableOpacity style={styles.card} onPress={()=>{navigation.navigate('DetailPage',content)}}>
      <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>
  )
}

수정된 Card.js component에서, navigate 함수는 이동할 페이지 이름과 함께 content를 두 번째 parameter로 받는다. contentMainPage.js 파일에서 cateState, 즉 데이터 Array의 각 element인 딕셔너리(Object) 데이터를 의미한다. 즉, 카드 리스트마다 다른 딕셔너리(Object) 형태의 데이터를 전달받는 것이다.



Card.js 파일에서 두 번째 parameter를 입력해 데이터를 받도록 했다면, 다음으로 DetailPage.js에서 고정되어 있는 데이터를 수정해야 한다.

// DetailPage.js
import React, { useState, useEffect } from 'react';
import { StyleSheet, Text, View, Image, ScrollView, TouchableOpacity, Alert } from 'react-native';
import { StatusBar } from 'expo-status-bar';

export default function DetailPage({navigation,route}) {
		
  // 초기 컴포넌트의 상태값을 설정
  // state, setState 뿐 아니라 이름을 마음대로 지정할 수 있음
  const [tip, setTip] = useState({
    "idx":9,
    "category":"재테크",
    "title":"렌탈 서비스 금액 비교해보기",
    "image": "https://storage.googleapis.com/sparta-image.appspot.com/lecture/money1.png",
    "desc":"요즘은 정수기, ... ",
    "date":"2020.09.09"
  })
  
  useEffect(()=>{
    console.log(route)

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

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

  return ( 
    <ScrollView style={styles.container}>
      <StatusBar style="light" />
      <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>
  )
}

DetailPage 함수를 보면, {navigation,route}를 parameter를 넘겨받는다. DetailPageStackNavigator.js 파일에서 <Stack.Screen> 태그로 연결되어 있기 때문에 스택 네비게이션으로부터 해당 값들을 받아 사용할 수 있다.

위 코드를 보면, tip이라는 상태 변수와 setTip이라는 상태 변수 변경 함수가 설정되어 있다. 얼핏 고정값처럼 보이는 딕셔너리(Object)는 useState 함수의 parameter로, 초기값으로 설정된 데이터다. 코드의 return문을 보면, MainPage.js 파일에는 있었던 로딩 화면이 구현되어 있지 않다. 앞에서 로딩 화면을 만든 이유는 상태변수의 초기값이 빈 값으로 설정되었기 때문에, 데이터가 없는 상태에서 화면을 그릴 수 없기 때문이었다. 위의 코드처럼, 아무 의미가 없는 값이라도 초기값에 설정해주면 로딩화면을 구현하지 않아도 된다. 아무 의미 없는 값이라도 데이터가 들어 있기만 하면, 오류 없이 화면이 그려진 후 받은 데이터로 화면이 다시 그려진다.

데이터가 없을 때 에러가 나는 것을 해결하기 위해서는?

1) 로딩 화면을 구현한다.
2) 초기 상태값을 의미 없는 값이라도 넣어둔다.



useEffect 함수에 console.log(route) 코드가 있다. console.log를 이용하면 VSCode 터미널 창에서 넘겨받은 데이터를 눈으로 확인할 수 있다.

다음으로, setOptions 함수를 이용해 route에서 불러온 데이터의 key-value 값을 사용해 헤더 스타일을 바꿨다. 앞에서, 데이터를 받는 페이지는 비구조 할당 방식을 통해 route에서 route.params의 형식으로 데이터를 꺼내 써야 한다고 했다. Card.js 파일에서 navigation.navigate 함수를 쓸 때, parameter로 content를 넘겨줬었다. 앞에서 설명했듯이 content 자체가 딕셔너리(Object) 형태의 데이터이므로 route.paramscontent와 동일하다. 따라서 route.params.titlecontenttitle key 값에 접근할 수 있다.

그 아래의 setTip(route.params)는 넘겨받은 데이터로 상태 데이터를 재설정하는 코드다. setTip은 상태 변수 함수로, parameter로 넘겨준 값으로 상태를 변경한다. useEffect 함수는 화면이 그려진 후 가장 먼저 실행되는 함수이므로, 초기 화면이 그려진 다음 새로 데이터를 받아 상태를 재조정해 화면을 다시 그린다.



마지막으로, DetailPage 함수 안에 popup이라는 화살표 함수가 정의되어 있다. 이 함수는 각 상세 화면의 버튼에 onPress 속성으로 연결되어 위 화면과 같이 실행된다.


Categories:

Updated:

Leave a comment