기본 사용법
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는 라이선스 저장을 추적하며 검증이 아닙니다. 라이선스 검증은 두 플랫폼 모두 카메라 뷰 마운트 시점(initializeCamera)에 수행되며, 거부된 키는 onDetectionStatus의 failed로 표면화됩니다. 이미 검증된 키는 같은 프로세스 안에서 다시 검증하지 않습니다. 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' | 1.5.0부터 deprecated — 받기만 하고 무시 |
enableFakeDetection? | boolean | 사진·화면·인쇄물에 대한 스푸핑 탐지(기본 true) |
bracketingMode? | boolean | 캡처 브래키팅 — 채택 프레임 주변을 짧게 연사해 더 좋은 지문 이미지를 수집(기본 false). 런타임 토글 가능(리마운트 불필요) |
showDetectionMarker? | boolean | SDK 기본 코/얼굴 마커 표시(기본 true). false면 onDetectionResult의 rect로 완전 커스텀 오버레이를 그릴 수 있으며, 런타임 토글해도 촬영·탐지 세션이 재시작되지 않습니다 (SDK 1.4.3+) |
style | ViewStyle | 일반 RN 뷰 스타일 |
enableFakeDetection 기본값은 true
사진·화면·인쇄물에 대한 스푸핑 탐지가 기본으로 켜져 있습니다. false를 넘기면 해당 세션에서 스푸핑 탐지가 비활성화됩니다. 살아있는 동물이 아니라 반려동물 사진으로 SDK를 평가할 때 이렇게 사용합니다.
세션 설정(species/purpose/enableFakeDetection)이나 captureSessionId를 바꾸면 네이티브가 같은 뷰에서 재초기화 흐름을 자동으로 탑니다 — species/purpose/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를 확인하세요.
앱 서버가 Petnow API 호출에 쓰는 키(x-petnow-api-key)는 앱 서버에만 두세요. RN 클라이언트에서 Petnow API를 직접 호출하지 말고, 아래처럼 file:// URI를 앱 서버로 보내고 앱 서버가 /v2/fingerprints:upload·/v2/appearances:upload로 프록시합니다. 이 키는 usePetnowCamera에 넘기는 키와 별개입니다 → API 키 보안.
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>가 사용하는 타입입니다.
LicenseInfo.environment는 선택입니다. 생략하면 해당 SDK가 빌드된 환경을 그대로 사용하며, 배포되는 모든 빌드는 production입니다. staging 콘솔 키는 staging 백엔드에서만 유효하고 그 반대도 마찬가지여서, 맞지 않으면 라이선스 검증에 실패합니다. __DEV__에 연결하지 마세요 — release 빌드가 실수로 staging을 선택하면 해당 반려동물이 등록되어 있지 않은 백엔드로 실제 촬영 결과를 보내게 됩니다. (v1.3.x의 isDebugMode 기능으로, v1.4에서 제거되었다가 복원되었습니다.) apiKey는 프로세스가 살아 있는 동안 하나로 고정해 넘기고, 다른 키로 바꾸려면 앱을 재시작하세요.
// usePetnowCamera 옵션
type PetnowEnvironment = 'production' | 'staging';
type LicenseInfo = {
apiKey: string;
environment?: PetnowEnvironment;
};
// 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; rawReason?: string };
// failed: `reason`은 정규화된 값입니다. JS 빌드가 모르는 사유를 최신 네이티브
// SDK가 보내면 `reason`은 'Unexpected'가 되고 원본 문자열이 `rawReason`에
// 남습니다.
// 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()로 재시도. - 카메라/검출 단계 실패(거부된 라이선스 키, 권한 거부, 카메라 오픈 실패, 촬영 실패) →
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' },
});