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

EXPO INSTAGRAM CLONE 만들기 (2)

by Lihano 2021. 8. 4.
반응형

서론

인스타그램 클론 만들기 두번째 시간이다.

오늘은 사진을 업로드하는 방법에 대해서 알아봤다.

 

이건 저번 프로젝트에서도 다뤘던 내용이다.

하지만 지금 보고 있는 유튜버가 사용한 방법이 훨씬 세련되고 간편하다.

 

특히 충격이었던 부분은 BLOB 생성 부분이다.

나는 엄청 고생하며 생성한 BLOB 타입 데이터를 여기에서는 엄청 간단하게 만든다...

부조리함 마저 느껴질 정도였다.

 

아무튼 오늘은 새롭게 배우는 내용이라면 저번에는 다루지 않았던 카메라 접근 정도가 되겠다.

그럼 시작해보자.

 

접근 권한 요청

저번에도 똑같았지만 EXPO에서 사진을 찍거나 선택하는 작업은 Image Picker로 진행한다.

npm install expo-camera
npm install expo-image-picker

 

우선 작업에 필요한 패키지들을 다운받아준다.

Image Picker야 알테고 Expo Camera는 장치의 전면 또는 후면 카메라에 대한 미리보기를 렌더링하는 React 구성요소를 제공한다.

줌, 자동 초점, 흰색 균형 및 플래시모드와 같은 카메라의 매개 변수를 조정할 수 있다.

 

그리고 사용할 컴포넌트에서 이 둘을 import 해주면 된다.

import {Camera} from "expo-camera";
import * as ImagePicker from "expo-image-picker";

Image Picker도 Camea도 먼저 사용자에게 카메라와 갤러리에 접근할 수 있는 권한을 요청해야 한다.

이런 권한이 없다면 작업이 불가능하니까.

 

권한을 요청하는 함수는 useEffect를 통해 마운트 초기부터 실행해주도록 하는 게 좋다.

초기화 과정에서 미리 권한을 요청받도록하자.

권한을 요청하는 함수는 대게 비슷비슷하다.

 

const cameraStatus = await Camera.requestPermissionsAsync();
setHasCameraPermission(cameraStatus.status === "granted");

const galleryStatus = await ImagePicker.requestCameraRollPermissionAsync();
setHasGalleryPermission(galleryStatus.status === "granted")

이 두 함수 모두 Camera의 액세스 허가를 요청하는 기능이다.

 

특히 Camera Roll에 대한 권한을 요청하는 부분이 눈에 띄는데,

Camera Roll이란 iOS 환경에서의 카메라 앨범을 카메라 롤이라고 한다고 한다.

이게 갤럭시에서도 통용되는 권한 요청 함수인가는 모르겠다. 검색해도 안나오는 함수라서.

아마도 될 것 같지만.

 

결과의 response의 status가 사용자의 허락 여부를 나타내는 데, granted 라고 되어 있어야 허가가 되었다는 의미다.

그리고 이 컴포넌트에서는 camera의 허가가 없다면 아무것도 return 하지 않도록 한다.

 

if (hasCameraPermission === null || hasGalleryPermission === false) return <View />
if (hasCameraPermission === false || hasGalleryPermission === false) return <View />

카메라 켜기

허가를 요청하면 Camera 객체는 바로 사용자의 카메라에 액세스해서 카메라를 킨다.

즉, 카메라를 통해 찍은 정보가 지속적으로 Camera 객체로 흘러간다는 이야기다.

 

Camera 객체는 사용자에게 카메라를 통한 미리보기를 지원한다.

이 카메라도 여러 속성과 메소드를 지원하지만 우리가 사용할 기능은 제한적이다.

 

const [camera, setCamera] = useState(null);
const [type, setType] = useState(Camera.Constants.Type.back);

return 
  <View>
    <Camera
      ref={(ref)=> setCamera(ref)}
      style={styles.fixedRatio}
      type={type}
      ratio={"1:1"}
    />
  </View>

 

Camera 객체를 렌더링 하면 알아서 미리보기 화면을 보여줄 것이다.

하지만 이 카메라 기능을 카메라 기능답게 사용하기 위해선 몇가지 설정이 필요하다.

 

우선 ref는 말그대로 카메라의 참조다.

이 ref를 통해서 Camera의 여러가지 작업을 해줄 수가 있다.

 

그리고 Type은 카메라의 카메라 방향을 의미한다.

이게 지금 후면 카메라인지, 전면 카메라인지를 결정하는 요소다.

보다시피 default는 back(후면)으로 되어있는 걸 볼 수 있다.

 

그리고 이 Type를 전환하는 버튼을 만드는 것 역시 간단하다.

<Button title="Filp Image" opPress={()=>{
	setType(
    	type === Camera.Constants.Type.back
        ? Camera.Constants.Type.front
        : Camera.Constants.Type.back
    )
}} />

이렇게 back과 front를 전환시켜주기만 하면 된다.

 

사진 찍기

카메라를 켰으니 사진을 찍는 기능과 그걸 저장하는 기능은 필수다.

그리고 카메라를 사용하지 않더라도 갤러리에서 사진을 불러올 수 있도록 하자.

 

<Button titile="Take picture" onPress={()=> takePicture()} />

우선 사진 찍기 기능을 실현하는 버튼을 하나 만들어준다.

그리고 사진 찍기 기능을 구현하는 함수를 만들어준다.

const [Image, setImage] = useState(null);

const takePicture = async() => {
	if (camera) {
    	const data = await camera.takePictureAsync(null);
        setImage(data.url);
    }
}

사진 찍기 기능은 정말 간단하다.

camera는 Camera의 참조(ref)라고 위에서 언급했다.

 

camera를 통해 사진을 찍는 기능인 takePicktureAsync를 실행할 수 있다.

이 함수는 사진을 찍고 앱의 캐시 디렉토리에 저장한다. 사진은 미리보기와 일치하도록 배율이 자동 조절된다.

 

이 함수에도 여러 option을 넣을 수 있지만 여기서는 아무 것도 넣지 않았다.

option이 궁금하다면 공식 사이트를 참조하도록 하자.

 

그리고 함수를 통해 반환된 객체는 uri, width, height 등의 정보를 포함한다.

물론 옵션을 통해서 base64, exif를 요청할 수도 있다.

 

갤러리에서 사진 고르기

카메라를 통한 작업은 Expo Camera를 통해서 하지만 갤러리를 통한 작업은 Image Picker를 통한다.

useEffect를 통해 미리 허가도 받았겠다, 갤러리를 조회하는 버튼을 만들어준다.

<Button title="Picture Iamge From Gallery" onPress={()=> pickImage()} />

그리고 실제 기능을 구현하는 함수를 만들어준다.

const pickImage = async() => {
	let result = await ImagePicker.launchImageLibraryAsync({
    		mediaTypes : ImagePicker.MediaTypeOptions.Images,
        	allowEditing : true,
        	aspect : [1,1],
        	quality : 1,
    	})
    
    	if (!result.cancelled) {
    		setImage(result.uri)
    	}
}

launchImageLibraryAsync() 함수는 앱에서 이미지 또는 비디오를 선택하기 위한 UI를 표시한다.

보다시피 위에는 option을 몇가지 넣어줬는데, 그 내용은 다음과 같다.

 

mediaTypes :  어떤 종류의 미디어를 선택할지를 결정한다. 위에선 이미지만을 받는다.

allowEditing : 이미지를 선택한 후 편집 화면을 띄울지를 결정한다. 위에선 편집 화면을 띄우도록 한다.

aspect : 이미지의 비율을 미리 고정한다. 편집화면에선 저 비율로만 사진을 자를 수 있다.

quality : 사진의 압축 퀄리티를 결정한다. 1이 최대 품질을 위한 압축이며 0은 작은 크기에 대한 압축을 의미한다.

 

result cancelled도 전에 언급한대로 취소 여부를 나타내므로 이게 false일 경우에만 image를 업데이트 하기로 한다.

 

이미지 저장하기

사진을 찍고, 갤러리에서 불러오는 건 다 했다.

그럼 남은 건 그 사진 정보를 저장하는 일이다.

 

저장하는 작업은 다음과 같다.

먼저 firebase의 Storage에 저장한 후,

거기의 download URL을 받아 firebase database에 저장한다.

 

그러기 위해 우선 이미지 파일의 uri와 Storage에 저장할 경로를 설정한다.

const uplaodImage = async() => {
  const uri = props.route.params.image
  const childPath = `post/${firebase.auth().currentUser.uid}/${Math.random().toString(36)}`
}

보다시피 이 함수는 경로를 uuid가 아니라 Random 함수로 생성한 무작위의 36개의 숫자로 설정한다.

대신 이건 정말 희박한 확률로 경로 이름이 겹칠 가능성도 고려해야한다.

 

그리고 다음은 blob을 생성해줘야한다.

저번 포스팅에선 나는 blob을 만들기 위해 온갖 개고생을 했었다.

XMlHttpRequest를 사용해 blob을 요청했었는데 그 코드의 길이가 족히 30줄은 넘었을 것이다.

 

하지만 fetch 함수를 통해서는 단 두줄만에 blob을 얻을 수 있다.

미친 거 아닌가?

 

나는 몰랐는데 fetch 함수는 XMLHttpsRequest 객체보다 최근에 나온, Http 요청 전송 기능을 제공하는 Web API라고 한다.

이 함수는 HTTP response 객체를 래핑한 Promise 객체를 반환한다. 즉, then catch 함수도 사용가능하다.

 

fetch 함수를 통해 HTTP를 요청할 때 기본 설정값은 GET이다.

그외의 POST 등의 요청을 하기 위해서는 option이 따로 필요하다.

 

 fetch에 대해서는 다음에 기회가 되면 더 자세하게 다루도록 하고...

우선 중요한 건 이 fetch를 통해 아주 간단하게 HTTP 요청을 보낼 수 있고 blob 데이터를 받을 수 있다는 것이다.

 

const response = await fetch(uri);
const blob = awiat response.blob();

홀리.

fetch를 더 애용하도록하자.

 

이 blob 데이터를 storage에 저장한다.

blob의 경우에는 putString이 아니라 put을 사용한다고 저번에 언급했었다.

const task = firebase.storage().ref().child(childPath).put(blob);

그리고 task는 함수의 결과로 전달받은 snapshot이다.

이 snapshot은 이벤트 발생 시점의 작업 상태를 의미하며 변경이 불가능하다.

위의 snapshot은 다음과 같은 속성들을 포함한다.

 

bytesTransferred : 스냅샷을 생성한 시점까지의 전송된 총 바이트 수

totalBytes : 업로드가 예정된 총 바이트 수

state : 업로드의 현재 상태

metadata : 업로드가 완료되기 전에는 서버에 전송된 메타데이터. 업도드가 완료된 후에는 서버에서 다시 전송한 메타데이터.

task : 스냅샷 대상작업으로서 작업을 일시중지, 재개, 취소하는데 사용할 수 있다.

ref : 이 작업이 유래된 참조.

 

이러한 상태변화와 스냅샷의 속성을 결합하면 업로드 이벤트를 간편하고 효과적인 방법으로 모니터링할 수 있다.

task.on("state_changed", taskProgress, taskError, taskCompleted)

const taskProgress = (snapshot) => {
  console.log(`transferred : ${snapshot.bytesTransferred}`)
}

const taskComplete = () {
  task.snapshot.ref.getDownloadURL().then(snapshot => {
    savePostData(snapshot);
  })
}

const taskError = (snapshot) => {
  console.log(snapshot)
}

이런 식으로 말이다.

 

위의 on 함수는 세가지 함수를 설정할 수 있는데,

첫번째 함수는 Progress, pause, resume 이다.

쉽게 말해서 진행 중, 오류, 성공의 경우라고 직관적으로 이해해두자.

 

이 함수는 업로드가 완료된 시점에서 storage로부터 download URL을 받아오고 그걸 다른 함수를 통해 데이터베이스에 업로드하도록 되어 있다.

 

const savePostData = (downloadURL) => {
  firebase
    .firestore()
    .colleciton("posts")
    .doc(firebase.auth().currentUser.uid)
    .collection("userPosts)
    .add({
      downloadURL,
      caption,
      creation : firebase.firestore.FieldValue.serverTimestamp()
    })
    .then(function() {
      props.navigation.popToTop()
    })
}

이게 downloadURL을 받아와서 데이터 베이스에 업로드하는 함수다.

collection의 이름은 posts이며 document의 이름은 사용자의 uid다.

그리고 document에는 downloadURL, caption, creation 이 세가지 요소가 있다.

creation은 간편하게 말하면 시간 정보다.

 

이렇게 오늘은 이미지 업로드하는 방법을 자세하게 알아봤다.

쉴 틈 없이 다음 거 공부하러 가야지!

그럼 이만🖐🖐🖐🖐🖐

반응형

댓글