기본 사용법
usePetnowCamera hook과 CameraView로 카메라 UI를 통합하는 단계별 가이드.
시작하기 전에
이 가이드를 시작하기 전에 시작하기를 완료하세요. 패키지 설치·카메라 권한 설정·Petnow API 키가 필요합니다.
이 가이드에서는 usePetnowCamera hook과 <CameraView>를 사용하여 반려동물 코무늬/얼굴 촬영 기능을 앱에 통합하는 방법을 단계별로 안내합니다.
카메라 준비
usePetnowCamera로 컨트롤러를 만들고 ready 상태를 기다립니다.
카메라 표시
ready가 되면 <CameraView>를 마운트해 프리뷰와 오버레이를 띄웁니다.
이벤트 받기
탐지 상태·진행률·완료 결과를 콜백으로 처리합니다.
결과 처리
촬영한 이미지를 앱 서버로 업로드합니다.
추가 기능
재촬영, 카메라 전환, 일시정지/재개를 사용합니다.
Step 1: 카메라 준비
usePetnowCamera는 라이선스를 보유하고 검출 커맨드를 노출하는 컨트롤러 핸들을 반환하는 hook입니다. apiKey를 네이티브 SDK에 저장하며(apiKey가 바뀌면 다시 저장), camera.state로 SDK 준비 상태를 노출합니다. status가 'ready'가 될 때까지 기다린 뒤 <CameraView>를 마운트하며, 준비되기 전에는 로딩 화면을 표시하는 것이 좋습니다.
import { usePetnowCamera, CameraView } from '@petify/react-native-camera-ui';
function Scan({ apiKey }) {
const camera = usePetnowCamera({ apiKey });
// ready일 때만 CameraView를 마운트한다.
if (camera.state.status !== 'ready') {
return <Loading state={camera.state} />; // 직접 만드는 로딩 컴포넌트
}
// Step 2에서 <CameraView>를 마운트합니다.
return null;
}camera.state
SDK 준비 상태입니다. status === 'ready'일 때만 <CameraView>를 마운트하세요.
| status | 의미 |
|---|---|
idle | apiKey가 아직 없음 |
initializing | 네이티브 SDK에 라이선스 저장 중 |
ready | 저장 완료 — 카메라를 마운트할 수 있음 |
error | 네이티브 initialize 호출 실패(state.error에 메시지) |
state는 라이선스 저장을 추적하며 검증이 아닙니다. 라이선스 검증은 iOS에서 뷰 마운트 시점(initializeCamera)에 수행되고, 검증 실패는 onDetectionStatus의 failed로 표면화됩니다. Android는 라이선스 서버 검증이 없습니다(키는 모니터링 용도로 보관). ready보다 먼저 <CameraView>를 마운트하는 것은 지원되지 않습니다.
Step 2: 카메라 표시
camera.state가 ready가 되면 <CameraView>를 마운트합니다. 마운트가 카메라를 초기화하고, 언마운트가 정리합니다. 종류(species)·목적(purpose)과 서버에서 발급한 captureSessionId를 prop으로 전달합니다.
<CameraView
camera={camera}
species="DOG"
purpose="PET_PROFILE_REGISTRATION"
captureSessionId={captureSessionId}
style={{ flex: 1 }}
onDetectionStatus={setStatus}
onDetectionProgress={setProgress}
onDetectionFinished={onFinished}
/><CameraView> 위에 가이드 텍스트·버튼 같은 UI를 올리는 방법은 커스터마이징을 참고하세요.
Props
| Prop | 타입 | 설명 |
|---|---|---|
camera | PetnowCamera | usePetnowCamera가 반환한 컨트롤러 |
species | 'DOG' | 'CAT' | 반려동물 종류(탐지 파이프라인 결정) |
purpose | 'PET_PROFILE_REGISTRATION' | 'PET_IDENTIFICATION' | 'PET_VERIFICATION' | 촬영 목적(필요 이미지 수 결정) |
captureSessionId | string | 서버 발급 UUID(Server API) |
difficultyMode? | 'EASY' | 'NORMAL' | 'HARD' | 미지정 시 서버가 선택 |
enableFakeDetection? | boolean | 가짜 이미지 탐지(기본 false) |
bracketingMode? | boolean | 캡처 브래키팅 — 채택 프레임 주변을 짧게 연사해 더 좋은 지문 이미지를 수집(기본 false). 런타임 토글 가능(리마운트 불필요) |
showDetectionMarker? | boolean | SDK 기본 코/얼굴 마커 표시(기본 true). false면 onDetectionResult의 rect로 완전 커스텀 오버레이를 그릴 수 있으며, 런타임 토글해도 촬영·탐지 세션이 재시작되지 않습니다 (SDK 1.4.3+) |
style | ViewStyle | 일반 RN 뷰 스타일 |
세션 설정(species/purpose/difficultyMode/enableFakeDetection)이나 captureSessionId를 바꾸면 네이티브가 같은 뷰에서 재초기화 흐름을 자동으로 탑니다 — species/purpose/difficultyMode/enableFakeDetection 변경 시 컨트롤러를 재생성하고, captureSessionId 변경 시 capture runtime·detector를 재구성합니다. UX상 더 명확한 전환을 원하면 React key로 뷰를 다시 마운트해도 됩니다.
촬영 목적(purpose)에 따라 서버 캡처 세션의 petId 요건이 다릅니다 — 등록·검증은 petId가 필요하고 식별은 불필요합니다. 자세한 내용은 서버 API – 생체 데이터를 참고하세요.
Step 3: 이벤트 받기
탐지 상태·진행률·완료 결과는 <CameraView>의 이벤트 콜백으로 전달됩니다.
onDetectionStatus={(s: DetectionStatus) => { /* {type} 또는 {type:'failed', reason} */ }}
onDetectionProgress={(p: number) => { /* 0 ~ 100 */ }}
onDetectionFinished={(r: CameraResult) => { /* {success, fingerprintImages, appearanceImages} */ }}
onDetectionResult={(r: DetectionResult) => { /* 프레임별 코/얼굴 박스 — 커스텀 마커용 */ }}onDetectionStatus: 현재 탐지 상태(DetectionStatus).type은noObject|processing|detected|finished|failed(이때reason포함).onDetectionProgress: 진행률0 ~ 100정수.onDetectionFinished: 최종 결과(CameraResult) —fingerprintImages/appearanceImages는 로컬file://URI 배열입니다.onDetectionResult: 프레임별 코/얼굴 검출 박스(DetectionResult). 마커를 직접 그릴 때 사용하며, 예시는 커스터마이징을 참고하세요.
각 타입의 정의는 아래 타입을 참고하세요.
onDetectionProgress·onDetectionResult는 검출 업데이트마다(프레임 단위에 근접) 메인 스레드에서 호출됩니다. 핸들러는 가볍게 유지하고, 무거운 작업이나 과도한 setState는 피하세요.
Step 4: 결과 처리 / 업로드
onDetectionFinished의 fingerprintImages / appearanceImages는 로컬 file:// URI 배열입니다. JS에서 앱 서버로 업로드한 뒤, 서버가 Server API로 등록·인증·식별을 수행합니다. 클라이언트는 촬영과 업로드만 담당합니다.
촬영이 실패해도 onDetectionFinished는 success: false와 빈 이미지 배열로 호출됩니다(실패 사유는 onDetectionStatus의 failed로 별도 전달). 결과를 쓰기 전에 r.success를 확인하세요.
x-petnow-api-key는 앱 서버에만 두세요. RN 클라이언트에서 Petnow API를 직접 호출하지 말고, 아래처럼 file:// URI를 앱 서버로 보내고 앱 서버가 /v2/fingerprints:upload·/v2/appearances:upload로 프록시합니다.
RN에서 file:// URI는 FormData로 업로드합니다:
async function upload(r: CameraResult) {
if (!r.success) return;
const form = new FormData();
// RN의 FormData는 { uri, name, type } 형태로 파일을 받습니다.
r.fingerprintImages.forEach((uri, i) =>
form.append('fingerprints', { uri, name: `nose_${i}.jpg`, type: 'image/jpeg' } as any),
);
r.appearanceImages.forEach((uri, i) =>
form.append('appearances', { uri, name: `face_${i}.jpg`, type: 'image/jpeg' } as any),
);
form.append('captureSessionId', captureSessionId);
// 본인 앱 서버 엔드포인트로 전송 → 앱 서버가 x-petnow-api-key로 Petnow에 업로드
await fetch('https://your-app-server.example.com/petnow/upload', { method: 'POST', body: form });
}Step 5: 추가 기능
촬영 중·후에 카메라를 제어하는 커맨드입니다. 커맨드는 바인딩된 <CameraView>가 마운트된 뒤 동작합니다.
| 메서드 | 설명 |
|---|---|
camera.startDetection() | 검출 세션 시작(또는 처음부터 재시작 — 재촬영) |
camera.pauseDetection() | 검출 일시정지(카메라는 유지, 진행률 보존) |
camera.resumeDetection() | 일시정지한 검출 재개 |
camera.switchCamera() | 전/후면 카메라 전환 |
camera.retry() | 같은 apiKey로 네이티브 초기화 재시도(일시적 error 복구용) |
재촬영 / 연속 촬영
onDetectionFinished 이후 같은 뷰에서 camera.startDetection()을 호출하면 처음부터 다시 검출합니다(재촬영). 연속 흐름은 완료 콜백에서 다시 시작하면 됩니다.
const onFinished = useCallback((r: CameraResult) => {
upload(r);
if (continuousMode) {
setTimeout(() => camera.startDetection(), 1200); // 잠깐 쉰 뒤 재시작
}
}, [camera, continuousMode]);타입
usePetnowCamera와 <CameraView>가 사용하는 타입입니다.
// usePetnowCamera 옵션
type LicenseInfo = {
apiKey: string;
};
// usePetnowCamera가 반환하는 컨트롤러 핸들
type PetnowCamera = {
state: PetnowCameraState;
startDetection(): void; // 검출 시작 / 재촬영
pauseDetection(): void; // 일시정지(진행률 보존)
resumeDetection(): void; // 재개
switchCamera(): void; // 전/후면 전환
retry(): void; // 네이티브 초기화 재시도
};
type PetnowCameraState =
| { status: 'idle' }
| { status: 'initializing' }
| { status: 'ready' }
| { status: 'error'; error: string }; // error 메시지는 'error' 상태에만 존재
// onDetectionStatus 콜백 인자
type DetectionStatus =
| { type: 'noObject' } // 대상 미탐지
| { type: 'processing' } // 탐지 진행 중
| { type: 'detected' } // 탐지 성공
| { type: 'finished' } // 촬영 완료
| { type: 'failed'; reason: DetectionFailureReason }; // 실패(사유 포함)
// onDetectionFinished 콜백 인자
type CameraResult = {
success: boolean;
fingerprintImages: string[]; // 로컬 file:// URI
appearanceImages: string[]; // 로컬 file:// URI
};
// onDetectionResult 콜백 인자 (프레임별 검출 박스)
type BoundingBox = { x: number; y: number; width: number; height: number }; // normalized 0–1
type DetectionResult = {
nose: BoundingBox | null;
face: BoundingBox | null;
};에러 / 권한 처리
별도 에러 채널은 없습니다. 실패는 두 곳으로 표면화됩니다.
- 초기화(라이선스 저장) 실패 →
camera.state가error.camera.retry()로 재시도. - 카메라/검출 단계 실패(잘못된 라이선스(iOS), 권한 거부, 카메라 오픈 실패, 촬영 실패) →
onDetectionStatus의{ type: 'failed', reason }.
function guideMessage(status: DetectionStatus | null): string {
if (!status) return '카메라 초기화 중...';
switch (status.type) {
case 'failed':
return `인식 실패: ${status.reason}`;
case 'noObject':
return '반려동물을 화면 중앙에 맞춰주세요';
case 'finished':
return '촬영 완료!';
default:
return '';
}
}권한이 거부되면 failed가 전달됩니다. 설정에서 권한을 허용하도록 안내한 뒤 <CameraView>를 다시 마운트하세요.
2계층 수명주기
카메라(뷰 수명)가 바깥 계층이고, 검출은 그 안에서 도는 하위 수명주기입니다. 다이어그램과 자세한 설명은 소개를 참고하세요.
세션 수명 자동화
카메라는 단일 하드웨어 자원인데 RN 뷰는 자주 죽고 살아나므로, 세션은 뷰가 아니라 패키지가 싱글톤으로 관리합니다. 마지막 detach 후 1.5초 grace로 전환·리마운트에서 세션을 재사용하고, captureSessionId가 바뀌면 컨트롤러·세션 소유 구조는 유지한 채 capture runtime과 detector를 새 세션 ID로 재구성합니다. 자세한 내용은 소개를 참고하세요.
유도 사운드
반려동물의 주의를 끄는 유도 사운드(PetnowSound)는 카메라와 독립적으로 재생할 수 있습니다. 사운드 목록·미리듣기·전체 API는 사운드 가이드를 참고하세요.
전체 코드
hook·ready 게이트·<CameraView>·이벤트·업로드를 한 컴포넌트로 합친 예시입니다. captureSessionId는 앱 서버가 Server API로 발급한 값을 prop으로 받습니다.
import { useCallback, useState } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import {
usePetnowCamera,
CameraView,
type DetectionStatus,
type CameraResult,
} from '@petify/react-native-camera-ui';
export function Scan({ apiKey, captureSessionId }: { apiKey: string; captureSessionId: string }) {
const camera = usePetnowCamera({ apiKey });
const [, setStatus] = useState<DetectionStatus | null>(null);
// file:// URI를 앱 서버로 업로드 (앱 서버가 x-petnow-api-key로 Petnow에 프록시)
const upload = useCallback(
async (r: CameraResult) => {
if (!r.success) return;
const form = new FormData();
r.fingerprintImages.forEach((uri, i) =>
form.append('fingerprints', { uri, name: `nose_${i}.jpg`, type: 'image/jpeg' } as any),
);
r.appearanceImages.forEach((uri, i) =>
form.append('appearances', { uri, name: `face_${i}.jpg`, type: 'image/jpeg' } as any),
);
form.append('captureSessionId', captureSessionId);
await fetch('https://your-app-server.example.com/petnow/upload', { method: 'POST', body: form });
},
[captureSessionId],
);
// ready 전에는 CameraView를 마운트하지 않는다.
if (camera.state.status !== 'ready') {
return (
<View style={styles.center}>
<Text>카메라 준비 중… ({camera.state.status})</Text>
</View>
);
}
return (
<CameraView
camera={camera}
species="DOG"
purpose="PET_PROFILE_REGISTRATION"
captureSessionId={captureSessionId}
style={styles.fill}
onDetectionStatus={setStatus}
onDetectionFinished={upload}
/>
);
}
const styles = StyleSheet.create({
fill: { flex: 1 },
center: { flex: 1, alignItems: 'center', justifyContent: 'center' },
});