RN + EXPO) 카메라기능 구현

김명성·2023년 1월 11일
4

Expo-Camera

expo-camera는 디바이스의 카메라에 대해 미리보기를 랜더링하는 기능을 제공합니다.
또한 미리보기에서 줌, AutoFocus, 화이트 밸런스, 플래시 모드 등 다양한 카메라 기능을 조작할 수 있게 도와줍니다.

위 캡쳐본에 보이는 카메라 기능과 찍은 사진을 저장하는 방법을 알아보겠습니다.
(CSS 및 디자인은 부끄러우니 참고하지 말아주세요...😂)
(키보드 실제로는 깨끗합니다..!! ✨)


카메라로 사진 찍기

  1. npx expo install expo-camera 설치

  1. JSX문 내에 카메라 컴포넌트 작성
// CameraScreen.jsx
import { AutoFocus, Camera, CameraType } from 'expo-camera';
	//...
      <Camera
        ref={cameraRef}
        type={cameraType}
        zoom={zoomLevel}
        autoFocus={AutoFocus.on}
        whiteBalance={toggleWhiteBalance}
      />
  • Camera 컴포넌트의의 Props
    • ref: useRef를 통해 Camera를 제어합니다.
    const cameraRef = useRef(null);
    • type: useState를 통해 카메라의 전/후면을 제어합니다. 기본값은 back으로 후면부 카메라를 사용했습니다.
    const [cameraType, setCameraType] = useState(CameraType.back);
    • zoom: useState를 통해 카메라의 확대/축소를 제어합니다. 줌레벨은 0 부터 1 사이의 값으로 제어할 수 있으며 저는 0.1 단위로 움직이게 만들었습니다.
    const [zoomLevel,setZoomLevel] = useState(0);
    • autoFocus: on이면 자동 초점이 활성화되고 off는 초점이 고정됩니다. default 값이 on이지만 사용예제를 위해 작성하였습니다.
    • whiteBalance: 카메라의 화이트 밸런스 기능입니다. 무드 기능이라고 보시면 될 것 같습니다. 숫자로 제어하며, 디바이스마다 다르겠지만 Galaxy S22 기준으로는 0부터 5까지 6가지 필터를 지원합니다.

추가 설정은 Expo의 공식문서를 참조해주세요.


  1. 카메라 권한 획득과 권한 획득 후 카메라 컴포넌트로 이동시킬 버튼을 생성해주세요.
// GettingCameraPermissionComponent.jsx
<Pressable onPress={openCameraHandler}>
  <Text>카메라 켜기</Text>
</Pressable>

openCameraHandler 함수는 아래와 같습니다.


 const openCameraHandler = async () => { 
    // 카메라에 대한 접근 권한을 얻을 수 있는지 묻는 함수입니다.
      const { status } = await Camera.requestCameraPermissionsAsync();
   
   // 권한을 획득하면 status가 granted 상태가 됩니다.
      if (status === 'granted') {
   // Camera 컴포넌트가 있는 CameraScreen으로 이동합니다.
        navigation.navigate('CameraScreen',{
          title: route.params.title
        });
      } else {
        Alert.alert('카메라 접근 허용은 필수입니다.');
      }
  };

이제 해당 버튼을 클릭하면 디바이스의 카메라 접근 권한을 묻고, 접근 권한을 얻으면 카메라 컴포넌트로 이동합니다.


  1. 사진을 찍을 수 있는 버튼 생성
        <TouchableOpacity onPress={takePictureHandler}>
          <Text>Click Me!</Text>  
        </TouchableOpacity>

takePictureHandler 함수는 아래와 같습니다.

  const takePictureHandler = async () => { 
    // cameraRef가 없으면 해당 함수가 실행되지 않게 가드
    if (!cameraRef.current) return;
    
    // takePictureAsync를 통해 사진을 찍습니다.
    // 찍은 사진은 base64 형식으로 저장합니다.
    await cameraRef.current
      .takePictureAsync({
        base64: true,
      })
      .then((data) => {
        setPreviewVisible(true);
        setCapturedImage(data);
      });
  };

takePictureAsync의 리턴값으로 로컬 파일 디렉토리에 저장된 주소를 가리키는 uri, 사진의 width,height값을 제공받습니다.
또한 takePictureAsync에는 객체 형식으로 옵션을 입력하면 추가적인 데이터를 제공 받을 수 있습니다. 저는 base64로 인코딩된 스트링도 받겠다고 옵션을 설정해 주었습니다.
그렇게 takePictureAsync가 완료되면 반환값을 CapturedImage State에 저장해 주고,PreviewVisible State를 true로 변경해주었습니다.

CapturedImagePreviewVisible State는 방금 찍은 사진을 보여주고 추가로 편집을 할건지, 아니면 실제 기기에 바로 저장할 것인지 묻기 위해 만들어 주었습니다.

takePictureAsync에는 다양한 옵션이 존재해요.
옵션은base64 외에도 additionalExif,exif,quality,scale 등 다양한 옵션 프로퍼티들이 존재합니다.
자세한 내용은 공식문서를 참조해주세요.

앱 저장과 디바이스 저장은 다릅니다!
takePictureAsync에서 사진을 저장한다는 것은 디바이스에(ex: 갤러리,파일)에 저장된다는 것이 아니라 앱의 캐시 디렉토리에 저장된다는 것을 의미합니다. 실제 디바이스에 저장하기 위해서는 Expo-Media-Library를 사용해야 합니다.


  1. 방금 찍은 사진의 프리뷰 및 옵션 제공

저는 프리뷰를 이렇게 구현하였어요

return previewVisible && capturedImage ? (
    <CameraPreview
      photo={capturedImage}
      retakePictureHandler={retakePictureHandler}
      savePictureHandler={savePictureHandler}
      editPictureHandler={editPictureHandler}
    />
  ) : (<Camera
        ref={cameraRef}
        type={cameraType}
        zoom={zoomLevel}
        autoFocus={AutoFocus.on}
        whiteBalance={toggleWhiteBalance}
      />)
// ...

위 4번에서 저장한 previewVisiblecapturedImage가 truthy하다면, 방금 찍은 사진을 보여주고, 그게 아니라면 Camera 컴포넌트로 사진을 찍을 수 있게끔 만들었습니다.
그리고 CameraPreview 컴포넌트가 랜더링 되었다면 찍은 사진에 대해 다시 찍을 것인지, 저장할 것인지, 편집할 것인지 선택할 수 있게 만들어 두었어요.
(CSS와 디자인은 무시해주세요.. 😢)

CameraPreview에 props로 전달하는 3가지 아래와 같습니다

  • retakePictureHandler : 다시찍기 기능.
    CameraPreview의 조건을 falsy하게 바꿔주어요. 그러면 카메라 컴포넌트가 다시 보이겠죠?
  const retakePictureHandler = () => {
	setPreviewVisible(false);
    setCapturedImage(null);
  };
  • savePictureHandler: 실제 디바이스에 찍은 사진을 저장하는 기능.
    사진을 저장하기 위해서는 expo-camera뿐만 아니라 expo-media-library도 설치해주어야 합니다.
    터미널에서 npx expo install expo-media-library
    import * as MediaLibrary from 'expo-media-library'; 해주세요.
    mediaLibrary 또한 카메라와 마찬가지로 접근 권한을 얻어야 합니다.
    MediaLibrary.requestPermissionAsync()status를 얻은 후 status === grant 라면 찍은 사진을 디바이스에 저장하고, 코드로도 저장하는 로직을 만들어봅니다.
  const savePictureHandler = async () => {
    
      const { status } = await MediaLibrary.requestPermissionsAsync();
      if (status === 'granted') {
        // saveToLibraryAsync()를 통해 캐싱된 데이터를 실제 디바이스로 저장합니다.
        await MediaLibrary.saveToLibraryAsync(capturedImage.uri).then(() => {
          // 사진을 찍을 때 사용한 takePictureAsync 함수 이후 저장한한 capturedImage를 picture라는 state에 옮겨줍니다.
          // picture는 사용할 컴포넌트가 많기에 Recoil을 통해 전역 변수로 담았습니다.
          setPicture(capturedImage);
        }).then(() => {
          // 그 뒤 firebase의 firestore에 저장합니다.
          uploadImageAsync(capturedImage.uri,'originalPhoto/')
        });
      }
  };
  • editPictureHandler: 찍은 사진을 편집한 뒤 사진을 저장하는 기능.
    launchImageLibraryAsync()는 디바이스에서 이미지 또는 비디오를 선택하기 위한 UI를 표시합니다.
    순서대로 보면, 접근 권한을 얻고, 찍은 사진을 디바이스에 저장한 다음에, 갤러리를 열어 저장한 사진을 선택한 뒤 편집하고, 편집한 상태가 canceled가 아니라면 다시 전역 상태인 picture에 편집본을 저장합니다.
  const editPictureHandler = async () => {
    const { status } = await MediaLibrary.requestPermissionsAsync();
    if(status !== 'granted') {
      return Alert.alert("갤러리 접근권한 허용은 필수입니다.")
    }
    // 실제 디바이스에 사진을 저장.
      await MediaLibrary.saveToLibraryAsync(capturedImage.uri).then(() => {
        setPicture(capturedImage);
      })
      .then( async() => {
        // 갤러리를 불러와 편집
        let result = await ImagePicker.launchImageLibraryAsync({
          mediaTypes: ImagePicker.MediaTypeOptions.Images,
          allowsEditing: true,
          base64:true,
          aspect: [4,3],
          quality:1,
        })
        if(!result.canceled){
          // 편집 후 다시 전역상태 Picture에 저장.
          setPicture(result.assets[0]);
          return result.assets[0]
        }
      }).then((response) => {
        // 편집된 사진은 editPhoto url을 갖는 firebase Storage에 저장
        uploadImageAsync(response.uri,'editPhoto/');
      });
  }

저는 카메라에 바로 저장한 사진과 편집한 사진을 분리해서 Firebase의 storage에 저장하기 위해 uploadImageAsync라는 함수를 만들어 사용했습니다.
blob으로 이미지를 변환 한 다음 storage에 저장하고, 이후 downloadURL까지 변경하였습니다.(이 부분은 fireStore에 image의 URL을 저장하기 위해 만든것입니다.)

uploadImageAsync()

  async function uploadImageAsync(uri,photoURL) { 
    const blob = await new Promise((resolve, reject) => { 
      const xhr = new XMLHttpRequest(); 
      xhr.onload = function () { 
        resolve(xhr.response); 
      }; 
      xhr.onerror = function (e) { 
        console.log(e); 
      reject(new TypeError("네트워크 요청 실패")); 
      }; 
      xhr.responseType = "blob"; 
      xhr.open("GET", uri, true); 
      xhr.send(null);
    }); 
    const storageRef = ref(storage,`${photoURL}${uuid.v4()}`);
      uploadBytes(storageRef,blob)
      .then(async (snapshot) => {
        // 추후 downloadURL을 fireStore에 유저 정보- 유저 사진집을 만들 수 있게끔 변환
        const downloadURL = await getDownloadURL(storageRef);
        console.log(downloadURL);
      }).catch((error) => console.log(error));
    blob.close()
 }

여기까지 진행하셨다면 어느정도 카메라에 대해 감이 잡히셨을거라 생각합니다!

설명이 부족했거나 추가로 알고 싶은 부분이 있다면 언제든 말씀주세요

1개의 댓글

comment-user-thumbnail
2023년 12월 18일

좋은 내용 감사합니다~ 덕분에 많은 시간을 아꼈습니다 !

답글 달기