React Native/React Native_study

[리액트 네이티브를 다루는 기술 #19] 8장 Firebase로 사진 공유 앱 만들기1 (p .466 ~ 478)

bocoder
728x90
반응형

@ List

* 8.5 이미지 업로드하기

** 8.5.1 이미지 선택 기능 구현하기

** 8.5.2 사용자 기본 이미지 설정하기

** 8.5.3 Firebase Storage로 이미지 업로드하기

* 8.6 정리

 

@ Note

1. react-native-image-picker 라이브러리 

$ yarn add react-native-image-picker
$ npx pod-install

 - {launchCamera} : 사용할 이미지를 카메라로 바로 촬영할 때 사용

 - {launchImageLibrary} : 사진첩에서 이미지 선택할 때 사용

   > OS별 callback 로그 값

// iOS
LOG  {"assets": [{"fileName": "F25D0...539.jpg", "fileSize": 242106, "height": 341, "type": "image/jpg", "uri": "file:///Users/...EBE-415CAD01B539.jpg", "width": 512}]}

// Android
LOG  {"assets": [{"base64": "/9j/4AAQSkZJR ... Y3omh//9k=", "fileName": "rn_image_picker_lib_temp_...e1b.jpg", "fileSize": 18179, "height": 500, "type": "image/jpeg", "uri": "file:///data/...7e1b.jpg", "width": 500}]}

 

2. 유용한 사이트

 - 특정 크기를 가진 이미지를 랜덤으로 보여줌 : https://picsum.photos/500

 

3. Firebase Storage 이미지 업로드 사용법

import storage from '@react-native-firebase/storage';

//업로드할 경로 지정
const reference = storage().ref('/directory/filename.png');

// 파일 저장(uri: 선택한 이미지의 로컬 경로, type: 선택한 이미지의 타입)
if (Platform.OS === 'android') {
  await reference.putString(asset.base64, 'base64', {
    contentType: type,
  });
} else {
  await reference.putFile(uri);
}


// 다운로드할 수 있는(또는 Image를 통해 보여줄 수 있는) URL 생성
const url = await reference.getDownloadURL();

- iOS에서는 uri에서 파일을 불러와서 바로 업로드하고, Android 에서는 putString을 통해 base64로 인코딩된 데이터를 업로드해야 함

 

4. Javascript 배열 메서드 이해

 - pop : 배열의 마지막 요소 제거 후, 제거한 요소 반환
 - push : 배열의 마지막에 새로운 요소 추가한 후, 변경된 배열의 길이를 반환
 - shift : 배열의 첫 번째 요소 제거 후, 제거한 요소 반환
 - unshift : 배열의 첫 번째 자리에 새로운 요소 추가한 후, 변경된 배열의 길이 반환

var arr = [1, 1, 2, 3];

console.log(arr.pop());          // 3

console.log(arr.push("add"));    // 4
console.log(arr);                // [ 1, 1, 2, 'add' ]

console.log(arr.shift());        // 1

console.log(arr.unshift("add")); // 4
console.log(arr);                // [ 'add', 1, 2, 'add']

 

5. Firebase Storage 권한 설정

 - 책을 그대로 따라하다 보면 storage에 파일 등록 시 아래처럼 권한 오류가 발생함

 - 해결방법 : https://bocoder.tistory.com/81

WARN  Possible Unhandled Promise Rejection (id: 26):
Error: [storage/unauthorized] User is not authorized to perform the desired action.
NativeFirebaseError: [storage/unauthorized] User is not authorized to perform the desired action.

 

 

6. loading 상태 시 ActivityIndicator 컴포넌트 사용

(...)
import (...) ActivityIndicator, } from 'react-native';

(...)

{loading ? (
  <ActivityIndicator size={32} color="#6200ee" style={styles.spinner} />
) : (

  (...)
  
)}

 

@ Result

01234
iOS / android - 기본 이미지 설정, local 앨범 이미지 등록, 로딩 구현

 

iOS / android - Firebase storage 에 저장된 이미지 불러오기

 

Firebase storage 이미지 저장 확인

 

@ Git

 - https://github.com/eunbok-bocoder/PublicGalleryBocoder/commits/main

 

# Source tree

 

# android > app > src > main > AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.publicgallerybocoder">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
    
    ...
    
    </application>
</manifest>

 

# components > SetupProfile.js

import {useNavigation, useRoute} from '@react-navigation/core';
import React, {useState} from 'react';
import {
  Image,
  Pressable,
  StyleSheet,
  View,
  Platform,
  ActivityIndicator,
} from 'react-native';
import {signOut} from '../lib/auth';
import {createUser} from '../lib/users';
import BorderedInput from './BorderedInput';
import CustomButton from './CustomButton';
import {useUserContext} from '../contexts/UserContext';
import {launchImageLibrary} from 'react-native-image-picker';
import storage from '@react-native-firebase/storage';

function SetupProfile() {
  const [displayName, setDisplayName] = useState('');
  const navigation = useNavigation();
  const {setUser} = useUserContext();
  const {params} = useRoute();
  const {uid} = params || {};

  const [response, setResponse] = useState(null);
  const [loading, setLoading] = useState(false);

  const onSubmit = async () => {
    setLoading(true);

    let photoURL = null;

    if (response) {
      const asset = response.assets[0];
      const extension = asset.fileName.split('.').pop(); // 확장자 추출
      const reference = storage().ref(`/profile/${uid}.${extension}`);

      if (Platform.OS === 'android') {
        await reference.putString(asset.base64, 'base64', {
          contentType: asset.type,
        });
      } else {
        await reference.putFile(asset.uri);
      }
      photoURL = response ? await reference.getDownloadURL() : null;
    }

    const user = {
      id: uid,
      displayName,
      photoURL,
    };

    createUser(user);
    setUser(user);
  };

  const onCancel = () => {
    signOut();
    navigation.goBack();
  };

  const onSelectImage = () => {
    launchImageLibrary(
      {
        mediaType: 'photo',
        maxWidth: 512,
        maxHeight: 512,
        // android의 경우 uri에서 직접 파일을 읽는 과정에서 권한 오류가 발생 할 수 있기 때문에 설정
        includeBase64: Platform.OS === 'android',
      },
      res => {
        if (res.didCancel) {
          //취소했을 경우
          return;
        }
        // console.log(res);
        setResponse(res);
      },
    );
  };

  return (
    <View style={styles.block}>
      <Pressable onPress={onSelectImage}>
        <Image
          style={styles.circle}
          source={
            response
              ? {uri: response?.assets[0].uri}
              : require('../assets/user.png')
          }
        />
      </Pressable>
      <View style={styles.form}>
        <BorderedInput
          placeholder="닉네임"
          value={displayName}
          onChangeText={setDisplayName}
          onSubmitEditing={onSubmit}
          returnKeyType="next"
        />
        {loading ? (
          <ActivityIndicator size={32} color="#6200ee" style={styles.spinner} />
        ) : (
          <View style={styles.buttons}>
            <CustomButton title="다음" onPress={onSubmit} hasMarginbottom />
            <CustomButton title="취소" onPress={onCancel} theme="secondary" />
          </View>
        )}
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  block: {
    alignItems: 'center',
    marginTop: 24,
    paddingHorizontal: 16,
    width: '100%',
  },
  circle: {
    backgroundColor: '#cdcdcd',
    borderRadius: 64,
    width: 128,
    height: 128,
  },
  form: {
    marginTop: 16,
    width: '100%',
  },
  buttons: {
    marginTop: 48,
  },
});

export default SetupProfile;

 

# ios > PublicGalleryBocoder > Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>

	...

	<key>NSPhotoLibraryUsageDescription</key>
	<string>$(PRODUCT_NAME) would like access to your photo gallery</string>
	<key>NSCameraUsageDescription</key>
	<string>$(PRODUCT_NAME) would like to use your camera</string>
	<key>NSPhotoLibraryAddUsageDescription</key>
	<string>$(PRODUCT_NAME) would like to save photos to your photo gallery</string>
</dict>
</plist>

 

# screens > MainTab.js

import React from 'react';
import {Image, StyleSheet, Text, View} from 'react-native';
import {useUserContext} from '../contexts/UserContext';

function MainTab() {
  const {user} = useUserContext();
  return (
    <View style={styles.block}>
      {user.photoURL && (
        <Image
          source={{uri: user.photoURL}}
          style={{width: 128, height: 128, marginBottom: 16}}
          resizeMode="cover"
        />
      )}
      <Text style={styles.text}>Hello, {user.displayName}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  block: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  text: {
    fontSize: 24,
  },
});

export default MainTab;

 

728x90
반응형