본문 바로가기
프로그래밍/REACT-NATIVE

REACT-NATIVE(EXPO) SNS 프로젝트 마무리 (8)

by Lihano 2021. 8. 1.
반응형

서론

드디어 길었던 EXPO 앱 개발 프로젝트가 끝났다.

지금 만든 앱은 기존에 작성한 nwitter 프로젝트의 모든 기능을 그대로 구현한다.

앱 개발에 대한 이해를 심화시킬 수 있는 좋은 경험이었다고 생각한다.

 

현재 결과물은 이 상태로 두진 않을 것이다.

앞으로 연습 프로젝트를 몇 번을 거친 다음에 거기에 얻은 지식을 바탕으로 현재 앱을 지속적으로 업데이트 해나갈 생각이다.

 

목표는 실제로 앱 스토어에 등록해 배포까지해보는 것이다.

오늘은 프로젝트를 마무리하며 마지막 지식을 정리하도록 하자.

 

앱의 디자인

나는 디자인의 기초가 없어서 몰랐지만, 생각해보면 처음부터 레이아웃을 정하고 디자인을 했다면 훨씬 수월하지 않았을까 싶다.

이런 식으로 말이다.

심지어 이런 레이아웃을 정하는 방법도 전혀 어렵지 않다.

flex 라는 간편화된 기술이 있으니 그걸로 비율을 정해주면 끝날 일이다.

 

이런 레이아웃도 없이 그저 margin으로 위치를 정하려 했으니 전혀 유동성이 없어지고 쉽게 뭉개지는 앱이 나온게 아닐까 싶다.

다음 프로젝트부터는 꼭 이 레이아웃을 준수해가며 디자인을 구상할 계획이다.

 

그리고 전에도 언급했지만 Dimensions를 사용하면 현재 기기의 픽셀수 자체를 얻어와서 훨씬 유동성 있는 디자인을 할 수가 있다.

처음에는 %와 차이가 뭐지? 라고 생각했지만 이 기능을 사용하면 일정 픽셀 이상의 넓이와 높이를 갖는 기기에는 별도의 디자인을 적용할 수 있어 좋다고 한다.

width: Dimensions.get('window').width / 4

이런 식으로 말이다.

 

그리고 추가적으로 가로화면에 대해서도 언급하고 싶다.

당연하지만 모든 앱이 가로 화면을 지원하는 건 아니다.가로 화면을 지원하려면 앱 개발자가 가로 화면을 별도로 설정을 해두어야 한다.

 

app.json이란 코드에 속하지 않는 앱의 일부를 구성하기 위한 파일이다.임의의 구성 데이터를 앱에 전달할 수 있다.앱 이름에서 아이콘, 시작 화면 및 일부 서비스에 사용할 딥 링크 스키마 및 API key에 이르기까지 다양한 항목을 구성한다.

 

이 app.json의 expo 항목에서 가로화면의 설정 여부를 적용할 수 있다.orientation : portrait가 세로 화면이며 landscape가 가로 화면이다. default로 설정하면 양쪽 모두를 적용시킬 수 있다.

 

그리고 Dimensions를 이용하면 가로 화면의 전환 시의 이벤트를 감지할 수 있다.

Dimensions.addEventListener('change', updateLayout);

change가 말그대로 화면의 비율의 변화를 의미한다.

다만, 이 경우엔 가로와 세로의 전환 일수도 있고 iOS의 경우 앱의 소형화일 수도 있다.

 

여러 상황을 고려하면서 코딩하도록 하자.

 

화면 페이지 스크롤 넘기기 (ViewPager)

버튼 만으로 화면을 이동하는건 뭔가 쿨하지 않다.

별도의 작업이 아니라면 사용자에게 단순히 정보를 보여줄 뿐인 페이지들은 버튼이 아니라 스크롤로 관람이 가능하도록 하는 게 쿨하다.

 

그래서 선택한게 ViewPager다.

이 패키지는 사용자가 데이터 페이지를 좌우로 스크롤 할 수 있도록 한다.

사용법도 간단하고 정말로 많은 기능을 지원한다.

 

arn add react-native-pager-view

 

우선은 이 이름으로 다운을 받으면 된다.

 

패키지 설명란에 나온 샘플 코드는 다음과 같다.

 

import React from 'react';
import { StyleSheet, View, Text } from 'react-native';
import PagerView from 'react-native-pager-view';

const MyPager = () => {
  return (
    <PagerView style={styles.pagerView} initialPage={0}>
      <View key="1">
        <Text>First page</Text>
      </View>
      <View key="2">
        <Text>Second page</Text>
      </View>
    </PagerView>
  );
};

const styles = StyleSheet.create({
  pagerView: {
    flex: 1,
  },
});

 

한눈에 봐도 사용법을 직감적으로 알 수 있는 좋은 패키지라고 생각한다.

보다시피 PagerView는 컨테이너 내부의 컴포넌트들을 별도의 페이지로 구성하여 스크롤 시킨다.

 

이렇게만 해도 만족할만한 기능을 얻을 수 있다.

하지만 추가적으로 내가 사이트에서 나름 쓸만하겠다고 생각한 속성들을 추가적으로 나열해 보겠다.

 

  • initialPage : 선택해야 하는 초기 페이지 색인
  • ScrollEnabled : pager View가 스크롤을 활성화 시킨다.
  • onPageScroll : 페이지 간 전환시 실행되는 함수
  • showPageIndicator : 뷰 하단에 점 표시기가 표시
  • offscreenPageLimit  : 현재 볼 수 있는 페이지의 양쪽에 유지할 페이지 수. 이 수를 초과하는 페이지는 필요할 때 재 생성
  • setPage(index) : 특정 페이지로 스크롤하는 기능

 

 

Splash Screen 사용

앱이 실행될 때 처음 로딩이 시작하면서 화면이 등장하기까지 시간이 걸린다.

이때 보통 앱을 로딩하는 중에 대체 화면이 대신 화면에 표시되는 데 이게 Splash Screen이다.

 

expo는 이 Splash Screen을 쉽게 사용하게 하는 패키지를 제공한다.

하지만 문제는 이 패키지가 이미지로만 이 기능을 지원한다는 점이다.

임의의 컴포넌트를 Screen으로 설정할 수가 없다.

 

심지어 이미지 타입이 png가 아니라면 오류까지 띄운다.나는 jpg로 실험해봤는데 오류가 떴지만 작동은 잘됐다.

 

이 스플래쉬스크린 모듈은 스플래시 스크린에게 hide 명령이 지시될 때까지 계속 보이도록 한다.이 기능은 앱을 표시하기 전에 뒤에서 작업을 수행할 때 유용하다.

 

스플래쉬 스크린을 애니메이션화 하는 방법이 따로 있지만 그건 내 관심 밖의 일이라서 보지 않았다.일단 사용하는 방법에 집중하도록 하자.

 

import React, { useCallback, useEffect, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Entypo } from '@expo/vector-icons';
import * as SplashScreen from 'expo-splash-screen';
import * as Font from 'expo-font';

export default function App() {
  const [appIsReady, setAppIsReady] = useState(false);

  useEffect(() => {
    async function prepare() {
      try {
        // Keep the splash screen visible while we fetch resources
        await SplashScreen.preventAutoHideAsync();
        // Pre-load fonts, make any API calls you need to do here
        await Font.loadAsync(Entypo.font);
        // Artificially delay for two seconds to simulate a slow loading
        // experience. Please remove this if you copy and paste the code!
        await new Promise(resolve => setTimeout(resolve, 2000));
      } catch (e) {
        console.warn(e);
      } finally {
        // Tell the application to render
        setAppIsReady(true);
      }
    }

    prepare();
  }, []);

  const onLayoutRootView = useCallback(async () => {
    if (appIsReady) {
      // This tells the splash screen to hide immediately! If we call this after
      // `setAppIsReady`, then we may see a blank screen while the app is
      // loading its initial state and rendering its first pixels. So instead,
      // we hide the splash screen once we know the root view has already
      // performed layout.
      await SplashScreen.hideAsync();
    }
  }, [appIsReady]);

  if (!appIsReady) {
    return null;
  }

  return (
    <View
      style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}
      onLayout={onLayoutRootView}>
      <Text>SplashScreen Demo! 👋</Text>
      <Entypo name="rocket" size={30} />
    </View>
  );
}

 

우선 공식 사이트의 샘플 코드를 보자.

이 코드는 앱 리소스를 로드하는 동안 스플래시 스크린을 띄우고 앱이 초기 콘텐츠를 렌더링했을 때 스플래시 스크린을 숨기는 방법을 보여준다.

 

우선 appIsReady state에 집중하자.

이 변수가 앱이 로딩되었는지 로딩 되지 않았는지를 알려준다.

그리고 이 변수가 false라면 아무것도 렌더링 하지 않는다.

 

APP이 처음 렌더링 될 때 비동기 함수를 실행하여 resource를 다운받는다.

그 과정을 await로 기다린 후, 모든 과정이 끝나고 나서야 finally를 실행하여 appIsReady를 true로 바꿔 화면을 렌더링 시킨다.

 

주목할 부분은 스플래쉬 스크린을 숨기는데 onLayout을 사용했다는 점이다.

expo에서 onLayout은 레이아웃이 변경될 시에 호출되는 트리거다.

 

onLayout를 이용하여 Splash Screen을 숨기는 이유는,

appIsReady가 이미 true로 되어있는 상태에서 layout의 변화가 생기면,

Splash Screen이 실행되지 않고 빈 화면이 출력되기 때문이다.

 

그렇기 때문에 layout의 변화가 생길 시에 splash screen을 바로 즉시 숨겨줄 필요가 있다.

스플래쉬 스크린에 대해서는 다음 프로젝트에서 좀 더 자세하게 다뤄보도록 하자.

 

FlatList vs Scroll View

FlatList는 리액트 네이티브에서 인피니트 스크롤을 구현할 수 있는 제일 좋은 방법이다.

인피니트 스크롤이란 처음부터 모든 콘텐츠를 렌더링하지 않고, 스크롤이 바닥에 닿을 때 추가적으로 다음 콘텐츠를 렌더링하는 기능이다.

페이스북이나 인스타그램에 가면 다 인피니트 스크롤을 사용하고 있다.

 

FlatList는 언뜻 보면 Scroll View와 닮았는데 작동법은 완전 다르다.

 

Scroll View는 단순히 화면을 벗어난 부분을 스크롤을 통해서 볼 수 있도록 하는데 목적이 있다.

하지만 FlatList는 스크롤을 통해 데이터를 추가적으로 로드한다는 데 목적이 있다.

데이터의 길이가 가변적이고 그 양을 예측할 수 없는 경우에 사용한다.

 

import {
  Text,
  View,
  StyleSheet,
  TouchableOpacity,
  FlatList,
  Dimensions,
} from "react-native";

const Home = (props) => {
  const [nweets, setNweets] = useState([]);
  const [n, setN] = useState(10);
  const [last, setLast] = useState(0);
  const [first, setFirst] = useState(true);

  const _renderItem = ({ item }) => {
    console.log("렌더링 시작");
    return (
      <View style={styles.nweets}>
        <Nweet
          key={item.id}
          nweetObj={item}
          isOwner={item.creatorId === props.user.uid}
        />
      </View>
    );
  };

  const _headerComponent = () => {
    return (
      <View
        style={{
          paddingTop: 40,
          paddingBottom: 20,
          justifyContent: "center",
          alignItems: "center",
        }}
      >
        <Text
          style={{
            textAlign: "center",
            fontSize: 30,
            fontWeight: "bold",
            color: "#190309",
            textTransform: "uppercase",
            marginBottom: Dimensions.get("window").height / 30,
            marginTop: Dimensions.get("window").height / 12,
          }}
        >
          Welcome my App
        </Text>
        <TouchableOpacity
          style={styles.barButton}
          onPress={() => props.navigation.push("Enter")}
        >
          <Feather name="plus-circle" size={60} color="#ff5722" />
        </TouchableOpacity>
      </View>
    );
  };

  const _getData = async () => {
    if (first) {
      await dbService
        .collection("nweets")
        .orderBy("createAt", "desc")
        .limit(n)
        .onSnapshot((snapshot) => {
          const nweetArray = snapshot.docs.map((doc, index) => {
            console.log(index);
            if (index === snapshot.docs.length - 1)
              setLast(doc.data().createAt);
            return {
              id: doc.id,
              ...doc.data(),
            };
          });
          setNweets(nweets.concat(nweetArray));
          console.log(last);
        });
      setFirst(false);
    } else if (!first) {
      await dbService
        .collection("nweets")
        .orderBy("createAt", "desc")
        .limit(n)
        .startAfter(last)
        .onSnapshot((snapshot) => {
          const nweetArray = snapshot.docs.map((doc, index) => {
            console.log(index);
            if (index === snapshot.docs.length - 1)
              setLast(doc.data().createAt);
            return {
              id: doc.id,
              ...doc.data(),
            };
          });
          setNweets(nweets.concat(nweetArray));
        });
    }
  };

  useEffect(() => {
    _getData();
  }, []);

  return (
    <LinearGradient colors={["#fdfbfb", "#ebedee"]}>
      <Popup visible={props.user.visible} />
      <FlatList
        data={nweets}
        renderItem={_renderItem}
        ListHeaderComponent={_headerComponent}
        keyExtractor={(item, index) => item.id}
        onEndReached={_getData}
        onEndReachedThreshold={1}
        contentContainerStyle={{ paddingBottom: 100 }}
      />
    </LinearGradient>
  );
};

export default Home;

 

이게 FlatList를 구현한 내 메인 화면의 코드다.

LinearGradient와 Popup은 상관이 없는 부분이니 무시해도 된다.

 

FlatList는 많은 속성을 가지고 있다.

우선 하나하나 살펴보도록 하자.

 

  • data : FlatList가 다루는 데이터다. 렌더링을 추가적으로 진행할 때 이 data가 전달된다.
  • renderItem : 추가적으로 렌더링을 진행할 시에 렌더링되는 아이템이다. dom 구조로 이루어져 있다.
  • ListHeaderComponent : 추가적으로 렌더링되는 dom 구조가 아니라 헤더에 고정적으로 존재하는 구성요소다.
  • onEndReached : 스크롤이 화면 끝에 도달했을 때 실행되는 함수다. 추가적으로 데이터를 다운받는 용도로 사용된다.
  • onEndReachedThreshold : 스크롤이 화면 끝 어느 정도에 도달할 때 onEndReached를 실행할지를 설정한다.

알아야 할건 이 부분이다.

renderItem과 ListHeaderComponent를 통해 고정적으로 존재하는 부분과 유동적으로 추가되는 부분을 구분하는 게 중요하다.

 

그리고 data는 항상 배열로 존재한다.

data의 배열로 전달된 내용들을 하나하나 이용해서 renderItem을 구성하는 것이다.

절대 개별 item을 차례로 data에 전달해선 안된다.

 

그리고 추가적으로 로딩되는 data들은 반드시 concat으로 배열에 끼워넣는다.

교체하는 형식이 아니다❌

이전의 데이터도 포함한 새 데이터 배열을 renderItem에 전달한다고 해도 이 패키지는 알아서 새로 추가한 부분만 새롭게 렌더링해줄 것이다.

 

헷갈렸던 부분은 firebase의 데이터베이스에서 어떻게 10개씩 데이터를 빼내오느냐였다.

일단 데이터베이스의 document를 구성하는 필드 중에서 유일한 식별자는 createAt 뿐이다.

그러니까 createAt을 통해서 데이터를 10개씩 로딩해야했다.

 

그 방법은 의외로 간단했다.

파이어베이스의 쿼리 함수 중에서 limit을 사용하면 얻어오는 document의 수를 제한할 수 있다.

그리고 startAfter을 이용하면 orderBy로 정렬된 경우 해당 index 이후의 데이터를 얻어온다.

 

그러니까 내가 할 일은 limit을 통해 얻어올 document의 수를 제한하고,

document를 얻어올 때마다 마지막 index의 값을 startAfter로 업데이터 해서 다음 시작점을 정해주는 거였다.

 

Stack Navigation

내가 처음 Navigation 연습 용도로 참고했던 블로그가 Switch Navigation을 사용했길래 나도 쭉 Switch Navigation을 사용했다.

하지만 솔직히 엄청 불편했다.

 

불편함을 이기지 못해 결국 Stack Navigation으로 갈아탔고 그 편리함에 나는 감동했다.

Switch Navigation은 한번에 하나의 페이지만 보여주는 용도로 쓰이고,

Stack Navigation은 페이지 위에 페이지를 겹칠 수 있도록 하는 용도로 쓰인다.

 

이게 무슨 말이냐하면... Switch Navigation을 사용하면 페이지 이동시 무조건 렌더링이 일어난다.

하지만 Stack Navigation은 Stack 형태처럼 페이지 위에 페이지를 쌓는 것이기 때문에,

페이지 위에 페이지가 새로 렌더링 된다 해도 기존의 페이지는 사라지지 않는다.

여전히 뒤에 존재하는 것이다.

그렇기 때문에 뒤로 가기를 클릭해도 새로 렌더링은 일어나지 않는 것이다.

 

뿐만 아니라 Stack Navigation은 멋있는 네비게이션 바까지 제공해준다.다만 이 네비게이션 바와 Stack이라는 특성 때문에 호불호가 갈릴 수는 있겠다고 생각했다.

 

import React, { useEffect, useState } from "react";
import { createStackNavigator } from "@react-navigation/stack";
import { NavigationContainer } from "@react-navigation/native";
import Login from "../screens/Login";
import Signup from "../screens/Signup";
import Enter from "../screens/Enter";
import ViewPager from "../screens/ViewPager";
import { useSelector, useDispatch } from "react-redux";
import { authService } from "../Firebase";
import { getUser } from "../actions/user";
import { Text } from "react-native";

const Stack = createStackNavigator();

const StackNavigator = () => {
  const [init, setInit] = useState(false);
  const [login, setLogin] = useState(false);
  const user = useSelector((state) => state.user);
  const dispatch = useDispatch();

  useEffect(() => {
    authService.onAuthStateChanged((User) => {
      if (User) {
        dispatch(getUser(User.uid));
        if (user != null) {
          console.log("로그인 됨!");
          setLogin(true);
        }
      } else {
        setLogin(false);
      }
      setInit(true);
    });
  }, []);

  return (
    <NavigationContainer>
      {!init ? (
        <Text>Loading...</Text>
      ) : login ? (
        <Stack.Navigator initialRouteName="ViewPager">
          <Stack.Screen
            name="Enter"
            component={Enter}
            options={{ title: "업로드" }}
          />
          <Stack.Screen
            name="ViewPager"
            component={ViewPager}
            options={{ title: "메인" }}
          />
        </Stack.Navigator>
      ) : (
        <Stack.Navigator initialRouteName="Login">
          <Stack.Screen
            name="Login"
            component={Login}
            options={{ title: "로그인" }}
          />
          <Stack.Screen
            name="Signup"
            component={Signup}
            options={{ title: "회원가입" }}
          />
        </Stack.Navigator>
      )}
    </NavigationContainer>
  );
};

export default StackNavigator;

 

이건 실제 내 Stack Navigation을 구현한 함수다.

사용방법은 설명할 건덕지가 별로 없다.

상기해야할 사항이라면 반드시 NavigationContainer 안에 포함되어야 있어야 한다는 점?

그리고 생각보다 의존성을 따지는 패키지가 많다. 그게 좀 불편했다.

 

Switch Navigation에서는 제일 먼저 Login 화면에 가서 Login 화면에서 로그인 여부를 체크한 다음에 Home 화면으로 보냈다.

한번에 하나의 컴포넌트만 보여주는 Switch 네비게이션의 특성 때문에 가능했다.

 

하지만 Stack 에서는 그런 식으로 구동을 하면 Login 페이지 위에 Home 페이지가 겹치기 때문에 Login 페이지가 그대로 유지가 되거나 뒤로 가기를 통해 Login 페이지로 돌아갈 수 있다는 단점이 있었다.

 

그런 이유 때문에 아예 로그인용과 메인용의 네비게이터를 따로 나눠서,

처음부터 Stack Navigation 컴포넌트에서 로그인 체크를 실행한 뒤에 네비게이터를 스위치 하기로 했다.

방법은 보시다시피 위와 같다.

 

그리고 options를 통해 title을 설정할 수 있다.

이 title을 안넣어주면 컴포넌트 파일명이 네이게이션 바에 그대로 드러나기 때문에 웬만하면 설정해주는 편이 좋다.

 

마지막으로 Stack이기 때문에 페이지의 이동은 push와 pop을 통해 이루어진다.

push(value)를 통해 원하는 페이지를 위에 쌓고,

pop()을 통해 현재 페이지를 Stack에서 뺀다.

 

자세한 사용법은 공식 문서를 참조하도록 하자.

 

결론

앱을 만드는 게 생각보다 재밌어서 놀라웠다.

완전 새로운 영역을 배우는 건 아닐까 걱정했지만 의외로 REACT와 차이점이 거의 없다시피 해서 놀랐다.

 

리액트의 기술로 모바일 앱의 개발을 고려할 수 있다는 게 REACT-NATIVE의 엄청난 장점같다.하지만 이 기술을 사용하면 과연 리액트로 구성한 반응형 웹과의 차이가 있을까? 싶은 의문도 생긴다.

 

어제 보고 싶은 책이 있어 부천 시립 도서관 홈페이지를 들어가봤다.거기의 공지사항에서 반응형 웹의 개편으로 인해 기존의 도서관 앱은 폐기한다고 했다.

 

리액트 네이티브는 확실히 편리하지만 웹을 앱처럼 꾸민 것 외의 장점이 있을까 싶은 의문이 생겨났다.모든 모바일에는 기본 브라우저가 있는 만큼 굳이 브라우저를 통해 들어갈 수 있는 웹을 두고 번거롭게 앱을 다운받을 필요가 있을까?

 

아무튼 다음 프로젝트는 좀 짧고 가볍게 채팅 앱을 구현해볼 생각이다.친구관리의 기능에 대해서도 좋은 연습이 될거라고 생각한다.

 

그럼 오늘은 이만🖐🖐🖐🖐🖐😁😁😁😁😁😁😁😁

 

 

 

반응형

댓글