expo-camera는 디바이스의 카메라에 대해 미리보기를 랜더링하는 기능을 제공합니다.
또한 미리보기에서 줌, AutoFocus, 화이트 밸런스, 플래시 모드 등 다양한 카메라 기능을 조작할 수 있게 도와줍니다.
위 캡쳐본에 보이는 카메라 기능과 찍은 사진을 저장하는 방법을 알아보겠습니다.
(CSS 및 디자인은 부끄러우니 참고하지 말아주세요...😂)
(키보드 실제로는 깨끗합니다..!! ✨)
npx expo install expo-camera
설치// CameraScreen.jsx
import { AutoFocus, Camera, CameraType } from 'expo-camera';
//...
<Camera
ref={cameraRef}
type={cameraType}
zoom={zoomLevel}
autoFocus={AutoFocus.on}
whiteBalance={toggleWhiteBalance}
/>
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의 공식문서를 참조해주세요.
// 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('카메라 접근 허용은 필수입니다.');
}
};
이제 해당 버튼을 클릭하면 디바이스의 카메라 접근 권한을 묻고, 접근 권한을 얻으면 카메라 컴포넌트로 이동합니다.
<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
로 변경해주었습니다.
CapturedImage
와PreviewVisible
State는 방금 찍은 사진을 보여주고 추가로 편집을 할건지, 아니면 실제 기기에 바로 저장할 것인지 묻기 위해 만들어 주었습니다.
takePictureAsync에는 다양한 옵션이 존재해요.
옵션은base64
외에도additionalExif
,exif
,quality
,scale
등 다양한 옵션 프로퍼티들이 존재합니다.
자세한 내용은 공식문서를 참조해주세요.
앱 저장과 디바이스 저장은 다릅니다!
takePictureAsync
에서 사진을 저장한다는 것은 디바이스에(ex: 갤러리,파일)에 저장된다는 것이 아니라 앱의 캐시 디렉토리에 저장된다는 것을 의미합니다. 실제 디바이스에 저장하기 위해서는 Expo-Media-Library를 사용해야 합니다.
저는 프리뷰를 이렇게 구현하였어요
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번에서 저장한 previewVisible
과 capturedImage
가 truthy하다면, 방금 찍은 사진을 보여주고, 그게 아니라면 Camera
컴포넌트로 사진을 찍을 수 있게끔 만들었습니다.
그리고 CameraPreview
컴포넌트가 랜더링 되었다면 찍은 사진에 대해 다시 찍을 것인지, 저장할 것인지, 편집할 것인지 선택할 수 있게 만들어 두었어요.
(CSS와 디자인은 무시해주세요.. 😢)
CameraPreview에 props로 전달하는 3가지 아래와 같습니다
retakePictureHandler
: 다시찍기 기능. 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.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()
}
여기까지 진행하셨다면 어느정도 카메라에 대해 감이 잡히셨을거라 생각합니다!
설명이 부족했거나 추가로 알고 싶은 부분이 있다면 언제든 말씀주세요
좋은 내용 감사합니다~ 덕분에 많은 시간을 아꼈습니다 !