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

REACT-NATIVE(EXPO) SNS 프로젝트 시작 (7)

by Lihano 2021. 7. 30.
반응형

서론

오늘부로 목표로 했던 모든 기능은 구현을 마쳤다.

전의 프로젝트에서 사용했던 파이어베이스와 같은 서비스를 공유하면서 게시물의 업로드와 프로필 업데이트 기능도 끝마쳤다.

 

난관이라면 모바일이라는 환경 때문에 생기는 변수와 아직 생소한 EXPO라는 개발 환경이었다.

사소한 무지에서 오는 여러 버그들을 해결하는 데 엄청난 시간을 잡아먹혔다.

 

오늘부로 최소한의 기능구현은 마무리 지었으니 앞으로는 이 기능을 좀 더 강화하는데 집중할까 한다.

그럴싸한 SNS 구색을 갖춘 뒤 실제로 앱 스토어에 배포해보는 연습도 해 볼 생각이다.

 

일단은 오늘 완성된 결과물에 대해서 설명을 할까 한다.

 

이미지 선택 (Image Picker)

게시물을 제대로 업로드하기 위해서는 이미지를 선택하는 기능이 필수였다.

당연한 말이겠지만 이미지를 input 하는 과정도 웹과는 다르다.

 

EXPO에 file 타입 input은 없지만 다행히도 이미지를 선택하는 API는 있었다.

ImagePicker는 엑스포에서 제공하는 API로 모바일에서 갤러리에 접근해 이미지와 비디오를 선택하거나 카메라에 접근해 사진을 찍을 수 있도록 하는 UI를 제공한다.

 

expo install expo-image-picker를 통해서 설치를 할 수 있다.

이미지 피커 - 엑스포 문서 (expo.dev)

 

ImagePicker - Expo Documentation

Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React.

docs.expo.dev

위의 엑스포 문서 탭에 가면 아주 자세한 정보를 알 수 있다.

우선 특이할만한 사항을 적어두겠다.

 

갤러리에 접근하기 위해서는 먼저 허락을 구하는 권한 요청 작업이 필요하다.

이때 쓰이는 것이

const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync()

이다.

 

이 함수는 사용자에게 권한 요청을 하고 그 결과를 반환한다.

결과에 대한 참조는 status를 참조하면 된다.

이때 status가 "granted"라면 사용자가 권한 요청에 동의했다는 뜻이다.

 

그리고 갤러리에 접근하도록 하는 함수가

let result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.All,
      allowsEditing: true,
      aspect: [4, 3],
      quality: 1,
    });

이다.

보시다시피 이 함수는 여러 옵션을 가진다.

그 종류와 기능은 다음과 같다.

  • mediaTypes : 어떤 타입의 미디어를 선택할지 결정한다.
  • allowsEditing : 이미지를 선택했을 때 편집 화면을 띄울지를 결정한다. 아이폰 유저는 자르기 밖에 못한다.
  • allowsMultipleSelection : 여러 이미지를 선택할지 말지를 결정한다.
  • aspect : 선택한 이미지의 비율을 결정한다.
  • quality : 압축한 이미지의 퀄리티를 결정한다.
  • base64base64 format도 이미지 결과에 포함시킬지를 결정한다.

이 정도가 주요 내용이며 그 외는 알 필요가 없다.

참고로 base64란 uri가 아닌 8비트 이진 데이터를 문자코드에 영향을 받지 않는 공통 ASCII 영역의 문자들로만 이루어진 일련의 문자열로 바꾸는 인코딩 방식을 뜻한다.

 

이 외에는 카메라의 권한 요청과 카메라를 키는 함수가 존재하지만 내가 관심있는게 아니라서 따로 기술하지 않겠다.

필요한 사람은 직접 위 링크에 들어가서 문서를 읽도록.

 

이미지 업로드하기

이미지 피커를 통해 이미지의 uri와 base64를 얻은 다음 내가 할일은 이것을 storage에 저장한 후 downloadURL을 받아오는 일이었다.

하지만 여기에는 엄청난 오류가 존재했다.

 

내가 웹에서 프로젝트를 개발할 때는 File 리더기를 통해 이미지 파일을 읽은 후 거기서 얻은 Data  URL string 문자열을 putString() 함수를 통해 스토리지 서버에 업로드했다.

하지만 리액트 네이티브에선 이 과정에 커다란 문제가 존재했다.

putString을 사용할 수가 없는 것이다.

 

이유는 잘모르겠지만 검색을 해보니 RN 기반 환경에서는 putString이 잘 작동하지 않는 것 같다.

이유는 잘 모르겠지만 url의 문제가 아닐까 싶다.

 

아무튼 putString 대신 파일을 업로드하는 방법을 찾느라 시간을 좀 썼다.

다행히도 expo 문서 탭에 이와 같은 문제를 해결하는 방법이 있었다.

 

해결법은 다음과 같다.

const uploadImageAsync = async (uri) => {
    const blob = await new Promise((resolve, reject) => {
      const xhr = new XMLHttpRequest();
      xhr.onload = function () {
        resolve(xhr.response);
      };
      xhr.onerror = function (e) {
        reject(new TypeError("Network request failed"));
      };
      xhr.responseType = "blob";
      xhr.open("GET", uri, true);
      xhr.send(null);
    });
    const ref = storageService.ref().child(`${user.uid}/${uuidv4()}`);
    const snapshot = await ref.put(blob);
    blob.close();
    return await snapshot.ref.getDownloadURL();
  };

 

위의 함수가 해결법이다.

바로 Blob을 이용하는 것이다.

 

Blob(Binary Large Object)이란 이미지, 사운드, 비디오와 같은 멀티미디어 데이터를 다룰 때 사용한다.

단순 텍스트가 아닌 대용량 바이너리 데이터를 담을 수 있다. 데이터를 간접적으로 접근하기 위한 객체!

주로 데이터의 크기 및 MIME 타입을 알아내거나 데이터의 송수신 작업에 사용한다.

즉, storage에 미디어를 전달하려는 내 목적과도 완전히 일맥상통한다.

 

그럼 이 Blob은 어디서 구하냐?

이미지피커를 통해 얻은 uri를 통해서 얻을 수 있다.

XMLHttpRequest(XHR) 객체는 서버와 상호작용하기 위하여 사용되며 새로고침 없이도 URL로부터 데이터를 받아올 수 있다.

즉, 전체 페이지와는 독립적으로(비동기적) 데이터를 주고 받을 수 있다는 뜻이다.

 

이 XML은 모든 종류의 데이터를 받아올 수 있는데 이를 통해서 이미지피커의 uri를 통해 blob 타입의 데이터를 다운받는다.

  • xhr의 onload 함수는 통신이 완료되어 데이터를 다 받아온 경우, 실행된다.
  • 반대로 onerror 함수는 에러가 발생했을 때 실행된다.
  • xhr의 responseType 함수를 통해서 다운받을 데이터의 형식을 "blob"으로 지정할 수 있다.
  • xhr의 open 함수는 (Http메서드 GET POST, 접속할 URL, true=비동기 false=동기 방식) 이렇게 3가지를 인수로 받아 요청을 초기화한다.
  • send를 통해 요청을 서버로 보낸다.
  • response는 요청에 대한 결과다.

이렇게 어렵지 않은 과정을 통해 blob을 얻을 수 있다.

그리고 Promise 객체에 대해서는 저번 포스팅에서 다룬 적이 있다.

 

그때 나는 비동기 작업의 순서를 정해주는 거라고 말했었다.

비동기 작업이라는 건 요청하는 쪽과 처리하는 쪽이 확실하게 구분되어 있다.

그러므로 비동기 작업을 요청하는 사람은 당연히 "실패할 경우"와 "성공할 경우"를 따로 생각해야한다.

 

Promise 객체는 이러한 작업에 도움을 준다.

resolve가 성공의 경우를 뜻하며 reject가 실패의 경우를 뜻한다.

Promise 객체 내부에서 비동기 작업을 처리한 후 실패의 경우에는 reject를 처리하고 성공시에는 resolve를 처리하면된다.

 

얻은 Blob은 Storage에 put 명령어를 통해서 업로드된다.

참고로 putString을 사용하지 않는 이유는 putString이 Blob, File 또는 Uint8Array를 사용할 수 없는 경우에 대신 원시 문자열, baase64, base64url 또는 data_url 등으로 인코딩된 문자열을 업로드하기 위한 함수이기 때문이다.

put은 File 및 Blob과 Uint8Array를 업로드하는데 쓰인다.

둘이 서로 반대다.

 

UUID의 사용

uuid란 랜덤으로 이름 지어주는 서비스라고 생각하면 편하다.

이걸 이용해서 파일의 이름을 생성했었다.

하지만 EXPO에서 사용하려니까 자꾸 이상한 오류가 발생했다.

 

해결법은 import "react-native-get-random-values" 을 설치해서 import 해주는 것이다.

이걸 먼저 import 한 후에 UUID 서비스를 이용해야 오류가 안난다.

이유는 모르겠다.

여러 자잘한 부분의 오류가 너무 많아서 열받는다.

 

Alert 사용

사용자에게 예 아니요 여부를 묻는 선택창을 띄우고 싶을 때 웹에서는 window.alert() 를 사용했었다.

하지만 지금 이건 window 환경이 아니기 때문에 사용할 수 없다.

 

이 대체용으로 나온게 Alert 컴포넌트다.

엑스포에서 명시된 사용법은 다음과 같다.

 

const confirm = () => {
    Alert.alert(
      "당신의 이야기를 지우려고 합니다.",
      "정말로 지우시겠습니까?",
      [
        { text: "예!", onPress: () => onDelete() },
        {
          text: "아니오...",
          onPress: () => console.log("Cancel Pressed"),
          style: "cancel",
        },
      ],
      { cancelable: true }
    );
  };

이 함수는 다른 버튼의 onPress 이벤트리스너에 의해 작동된다.

사용법은 직관적으로 알 수 있다.

 

위의 두 텍스트는 경고창의 제목과 내용을 의미한다.

그리고 배열안에는 두 개의 버튼과 그 버튼을 눌렀을 때의 작용을 각각 설정할 수 있다.

나 같은 경우에는 예 를 눌렀을 때 삭제 작업을 진행시키도록 했고 아니요를 누르면 아무것도 실행하지 않도록 했다.

사람에 따라서는 좀 더 다양한 활용도 가능할것이다.

 

버튼은 3개 까지 늘릴 수 있다.

그리고 마지막 아래의 cancelable 속성은 경고창 외부를 눌렀을 때 경고창을 지울 수 있다.

 

KeyboarAvoidingView에 대하여

모바일 앱에는 키보드라는 게 존재한다.이 키보드는 텍스트 입력창을 클릭하면 위로 튀어나와 모든 컨텐츠들을 밀어내서 화면을 엉망으로 만들기도한다.

어떤 경우 입력 컴포넌트가 키보드에 가려지는 일도 있다.

 

이런 경우를 위해서 나온게 KeyboardAvoidingView다.

가상 키보드의 방해에서 벗어나야 하는 뷰의 일반적인 문제를 해결하는 구성요소다.

키보드 높이에 따라 높이, 위치 또는 아래쪽 패딩을 자동으로 조정할 수 있다.

 

import React from 'react';
import {
  View,
  KeyboardAvoidingView,
  TextInput,
  StyleSheet,
  Text,
  Platform,
  TouchableWithoutFeedback,
  Button,
  Keyboard,
} from 'react-native';

export default function KeyboardAvoidingComponent() {
  return (
    <KeyboardAvoidingView
      behavior={Platform.OS == 'ios' ? 'padding' : 'height'}
      style={styles.container}>
      <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
        <View style={styles.inner}>
          <Text style={styles.header}>Header</Text>
          <TextInput placeholder="Username" style={styles.textInput} />
          <View style={styles.btnContainer}>
            <Button title="Submit" onPress={() => null} />
          </View>
        </View>
      </TouchableWithoutFeedback>
    </KeyboardAvoidingView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  inner: {
    padding: 24,
    flex: 1,
    justifyContent: 'space-around',
  },
  header: {
    fontSize: 36,
    marginBottom: 48,
  },
  textInput: {
    height: 40,
    borderColor: '#000000',
    borderBottomWidth: 1,
    marginBottom: 36,
  },
  btnContainer: {
    backgroundColor: 'white',
    marginTop: 12,
  },
});

이건 내 코드가 아니라 EXPO 문서탭에서 제공하는 예제 코드다.

 

behavior 속성은 padding, height, position 중에 어느 요소를 반응할지를 정한다.

contentContainerStyle 속성은 behavior 속성이 position일 때 컨테이너 속성의 스타일을 정한다.

enable 속성은 KeyboardAvoidingView의 적용여부를 결정한다. default는 true다.

keyboardVerticalOffset 속성은 화면의 상단과 REACT NATIVE VIEW 사이의 거리다.

 

즉, behavior가 height라면 input창이 키보드로부터 keyboardVerticalOffset 의 거리만큼 위치.

padding이라면 keyboardVerticalOffset 의 거리만큼 위치하게 된다.

 

TouchableWithoutFeedback 속성은 클릭 가능하지만 아무런 동작도 하지 않는 영역을 말한다.

대게 이런 건 키보드 외의 영역을 터치했을 때 키보드를 내리고 싶을 때 사용한다.

<TouchableWithoutFeedback onPress={Keyboard.dismiss}>

keyboard.dismiss는 말 그대로 keyboard를 내리는 역할을 한다.

 

그 외

그 외에도 추가적으로 알게 된 지식들이 있다.

우선 import Constants from "expo-constants" 패키지는 앱이 작동중인 모바일의 시스템 정보들을 반환한다.

반환하는 정보에 대해서는 문서의 링크를 걸어놓을 테니 이곳을 참조하자.

Constants - Expo Documentation

 

Constants - Expo Documentation

Expo is an open-source platform for making universal native apps for Android, iOS, and the web with JavaScript and React.

docs.expo.dev

 

이 Constants를 사용해서 상단바의 높이를 구하고 뷰가 상단바에 가려지지 않게 marginTop을 줄 수도 있다.

 

그리고 Dimensions를 이용하여 응용프로그램의 창의 너비와 높이를 얻을 수 있다.

const windowWidth = Dimensions.get('window').width;
const windowHeight = Dimensions.get('window').height;

화면 크기와 앱의 크기를 분할할 수 있는 모바일이라면 "window" 대신 "screen"으로 따로 작업을 만들어 줄 수도 있다.

Dimensions은 화면의 크기가 변할 경우의 이벤트리스너를 추가할 수도 있다.

다음 예제를 보면 이해가 갈 것이다.

 

import React, { useState, useEffect } from 'react';
import { View, StyleSheet, Text, Dimensions } from 'react-native';

const window = Dimensions.get('window');
const screen = Dimensions.get('screen');

export default function App() {
  const [dimensions, setDimensions] = useState({ window, screen });

  const onChange = ({ window, screen }) => {
    setDimensions({ window, screen });
  };

  useEffect(() => {
    Dimensions.addEventListener('change', onChange);
    return () => {
      Dimensions.removeEventListener('change', onChange);
    };
  });

  return (
    <View style={styles.container}>
      <Text>{`Window Dimensions: height - ${dimensions.window.height}, width - ${dimensions.window.width}`}</Text>
      <Text>{`Screen Dimensions: height - ${dimensions.screen.height}, width - ${dimensions.screen.width}`}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

이런 식으로 반응형 앱을 구사할 수도 있다.

 

그럼 오늘은 여기까지!

그럼 이만.🖐🖐🖐🖐

반응형

댓글