Bleeding edge

[Toy Project]React Native로 카메라 앱을 만들자 - Camera 본문

Javascript/React-Native

[Toy Project]React Native로 카메라 앱을 만들자 - Camera

codevil 2023. 9. 12. 15:01

0) RN-Camera에 들어가기 전 고민...

React Native로 카메라 앱을 만들기 위해서 크게 두 가지 View를 토이프로젝트에 넣기로 했다.

1. 카메라 앱

2. Image picker

기존에 사진을 가지고 있지 않은 경우 사진을 직접 촬용하기 위한 카메라뷰

사진을 가지고 있다면 이를 선택하기 위한 Image picker 뷰

React native에서는 카메라의 선택지가 react-native-camera(지금은 deprecate가 되어서 react-native-vision-camera)와 expork 가 있지만, react-native-camera의 오픈소스를 deprecate를 한 것을 보면 expo가 react-native보다 더 코드를 관리를 잘한다고 생각이 들어서 expo에 있는 카메라가 더 안정적이라고 생각했다(native의 기능을 위해서는 나중에 탈출해야할 수 있지만 우선은 toy project이니까 처음 공부는 간단하게!)

 

1) React-native 초기 설정

1. create-expo-app

npx create-expo-app --template

 

2. typescript

yarn add --dev @tsconfig/react-native @types/jest @types/react @types/react-test-renderer typescript

 

3. create tsconfig

touch tsconfig.json && open tsconfig.json

tsconfig 수정

{
  "extends": "@tsconfig/react-native/tsconfig.json"
}

 

4. create index.js

프로젝트의 엔트리가 될 수 있는 index.js 파일을 생성한다.

import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('RN-camera', () => App);

 

5. App.js 파일을 App.tsx로 수정한다.

 

 

이제 react-native와 typescript를 설정하는 것은 끝났다. yarn start로 체크!

주의 : gitignore에 .expo를 넣는 것을 잊지 말자!


2) 카메라 화면

1. 관련 라이브러리 설치

npx expo install expo-media-library && npx expo install expo-camera

2. 카메라의 권한 받기

MediaLibrary를 이용하여 모바일의 카메라 권한을 받도록 App.tsx에 설정한다.

import { Text, SafeAreaView } from "react-native";
import { Camera } from "expo-camera";
import * as MediaLibrary from "expo-media-library";
import { useState, useEffect } from "react";
export default function App() {
  const [isPermittedCamera, setIsPermittedCamera] = useState(false);
  const getCameraPermission = async () => {
    MediaLibrary.requestPermissionsAsync();
    const cameraPermission = await Camera.requestMicrophonePermissionsAsync();
    setIsPermittedCamera(cameraPermission.status === "granted");
  };

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

  return (
    <SafeAreaView>
      {!isPermittedCamera ? (
        <Text>카메라 권한이 없습니다.</Text>
      ) : (
        <Text>카메라 권한이 있습니다.</Text>
      )}
    </SafeAreaView>
  );
}

 

3. 사진을 찍지 않은 경우 카메라 설정

3-1 카메라의 유무에 대한 state와 사진을 잘못 찍었을 때 카메라를 지우기 위한 함수를 작성한다.

const [cameraImage, setCameraImage] = useState(null);
const clearPicture = () => setCameraImage(null);

카메라를 사용하기 위해 권한이 있고 cameraImage가 없는 경우에는 Camera component가 View에 보이도록 한다.

3-2 위에서 추가한 Camera component를 넣고 Camera 컴포넌트를 사진으로 저장하기 위하여 useRef를 사용한다

(중간 부분만 보면 헷갈릴 수 있으니 추가한 부분 위에 주석을 추가하였다.

export default function App() {
  const [isPermittedCamera, setIsPermittedCamera] = useState(false);
  const [cameraImage, setCameraImage] = useState<null | string>(null);
  const cameraRef = useRef<Camera>(null);

  const getCameraPermission = async () => {
    MediaLibrary.requestPermissionsAsync();
    const cameraPermission = await Camera.requestMicrophonePermissionsAsync();
    setIsPermittedCamera(cameraPermission.status === "granted");
  };
  const clearPicture = () => setCameraImage(null);
  const takePicture = async () => {
    if (cameraRef.current === null) return;
    try {
    //Camera component를 사진으로 찍는다
      const { uri } = await cameraRef.current.takePictureAsync();
      setCameraImage(uri);
    } catch (e) {
      console.log(e);
    }
  };

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

  if (!isPermittedCamera)
    return (
      <SafeAreaView>
        <Text>카메라 권한이 없습니다.</Text>
      </SafeAreaView>
    );
  return (
    <View style={styles.container}>
    //Camera component에 Ref를 넣는다
      <Camera style={styles.camera} ref={cameraRef} />
      
      <View style={styles.buttonWrapper}>
        <IconButton color="#fff" onPress={takePicture} icon="camera">
          Take a Picture
        </IconButton>
      </View>
    </View>
  );
}

 

4. MediaLibrary의 createAssetAync를 이용하여 카메라를 넣는다

  const savePicture = async () => {
    if (!cameraImage) return;
    try {
      MediaLibrary.createAssetAsync(cameraImage);
      Alert.alert("사진이 저장되었습니다!");
    } catch (e) {
      console.log(e);
    }
  };

 

최종

export default function App() {
  const [isPermittedCamera, setIsPermittedCamera] = useState(false);
  const [cameraImage, setCameraImage] = useState<null | string>(null);
  const cameraRef = useRef<Camera>(null);

  const getCameraPermission = async () => {
    MediaLibrary.requestPermissionsAsync();
    const cameraPermission = await Camera.requestMicrophonePermissionsAsync();
    setIsPermittedCamera(cameraPermission.status === "granted");
  };
  const clearPicture = () => setCameraImage(null);
  const takePicture = async () => {
    if (cameraRef.current === null) return;
    try {
      const { uri } = await cameraRef.current.takePictureAsync();
      setCameraImage(uri);
    } catch (e) {
      console.log(e);
    }
  };
  const savePicture = async () => {
    if (!cameraImage) return;
    try {
      MediaLibrary.createAssetAsync(cameraImage);
      Alert.alert("사진이 저장되었습니다!");
    } catch (e) {
      console.log(e);
    }
  };
  useEffect(() => {
    getCameraPermission();
  }, []);

  if (!isPermittedCamera)
    return (
      <SafeAreaView>
        <Text>카메라 권한이 없습니다.</Text>
      </SafeAreaView>
    );
  return (
    <View style={styles.container}>
      {!cameraImage ? (
        <>
          <Camera style={styles.camera} ref={cameraRef} />
          <View>
            <IconButton onPress={takePicture} icon="camera">
              사진 찍기
            </IconButton>
          </View>
        </>
      ) : (
        <>
          <Image source={{ uri: cameraImage }} style={styles.camera} />
          <View style={styles.buttonWrapper}>
            <IconButton onPress={clearPicture} icon="retweet">
              뒤로
            </IconButton>
            <IconButton onPress={savePicture} icon="check">
              저장하기
            </IconButton>
          </View>
        </>
      )}
    </View>
  );
}

카메라 토이 프로젝트를 만들면서.. 뭔가 카메라에 직접적으로 연결하는 코드는 작성하지 않다보니 뭔가.. 노코드로 코딩을 하는 느낌이 들었다.. 뭔가 찝찝한 느낌.. react native가 익숙해지면 결국에는 native를 공부해야하나 생각이 들기도 한다..

한번에 글을 쓰려고 했는데 생각보다 코드가 길어서 글을 두개로 나누기로 하였다. 그럼 다음글에서...!