문제 해결
자주 발생하는 문제와 해결 방법입니다.
NotInitializedError
증상
SDK 메서드를 호출했을 때 NotInitializedError가 발생합니다.
NotInitializedError: Appify SDK is not initialized.
Call appify.initialize() before using any SDK methods.
원인
initialize()를 호출하기 전에 SDK 기능을 사용했습니다.
해결 방법
앱 진입점에서 initialize()를 await한 뒤 다른 메서드를 호출합니다.
// 잘못된 사용
const token = await appify.push.getToken(); // initialize() 전에 호출
// 올바른 사용
await appify.initialize();
const token = await appify.push.getToken();
React를 사용하는 경우 최상위 컴포넌트의 useEffect에서 초기화합니다.
useEffect(() => {
appify.initialize().then(() => {
setReady(true);
});
}, []);
TimeoutError
증상
SDK 메서드 호출 후 30초가 지나면 TimeoutError가 발생합니다.
TimeoutError: Request timed out after 30000ms (key: CAMERA_SCAN)
원인
다음 중 하나입니다.
- 앱이 백그라운드 상태로 전환되어 WebView 처리가 중단됨
- 해당 메시지 키에 대한 핸들러가 네이티브에 등록되어 있지 않음
- 핸들러가 응답을 반환하지 않고 종료됨
해결 방법
네이티브 앱 개발자에게 해당 메시지 키의 핸들러 등록 여부를 확인합니다. 앱이 백그라운드에서 복귀한 뒤 재시도하도록 처리합니다.
try {
const result = await appify.camera.scan();
} catch (error) {
if (error.name === 'TimeoutError') {
// 사용자에게 재시도 안내
showRetryMessage();
}
}
BridgeNotAvailableError
증상
일반 브라우저(Chrome, Safari 등)에서 SDK 메서드를 호출하면 BridgeNotAvailableError가 발생합니다.
BridgeNotAvailableError: ReactNativeWebView bridge is not available.
This feature requires running inside the native app WebView.
원인
window.ReactNativeWebView가 존재하지 않는 환경에서 네이티브 기능을 호출했습니다. 일반 브라우저에는 WebView 브릿지가 없습니다.
해결 방법
appify.isWebview를 확인한 뒤 네이티브 기능을 조건부로 호출합니다.
if (appify.isWebview) {
const token = await appify.push.getToken();
} else {
// 일반 브라우저 대응 로직 또는 기능 비활성화
console.log('이 기능은 앱에서만 사용할 수 있습니다.');
}
소셜 로그인 실패
증상
appify.auth.loginWithKakao() 또는 appify.auth.loginWithGoogle() 등을 호출하면 로그인 창이 열리지 않거나, 열린 뒤 에러가 반환됩니다.
원인
다음 중 하나입니다.
- 네이티브 앱에 소셜 플랫폼 API 키(앱 키, 클라이언트 ID)가 설정되어 있지 않음
- URL Scheme이 앱에 등록되어 있지 않아 OAuth 콜백을 수신할 수 없음
- 소셜 플랫폼 개발자 콘솔에 앱 번들 ID 또는 패키지명이 등록되어 있지 않음
해결 방법
네이티브 앱 개발자에게 다음 항목을 확인합니다.
- 소셜 플랫폼 개발자 콘솔에서 API 키를 발급하고 앱에 설정했는지 확인합니다.
Info.plist(iOS) 또는AndroidManifest.xml(Android)에 URL Scheme이 등록되어 있는지 확인합니다.- 소셜 플랫폼 콘솔에 앱 번들 ID와 패키지명이 등록되어 있는지 확인합니다.
try {
const result = await appify.auth.loginWithKakao();
} catch (error) {
console.error('소셜 로그인 실패:', error.message);
// error.code로 구체적인 원인 확인 가능
}
푸시 토큰을 받을 수 없음
증상
appify.push.getToken()이 null을 반환하거나 에러가 발생합니다.
원인
다음 중 하나입니다.
- 사용자가 푸시 알림 권한을 허용하지 않음
- Firebase 프로젝트 설정 파일(
google-services.json,GoogleService-Info.plist)이 앱에 포함되어 있지 않음 - Firebase 초기화가 완료되지 않은 상태에서 토큰을 요청함
해결 방법
토큰 요청 전에 푸시 권한을 먼저 요청합니다.
// 권한 요청 후 토큰 조회
const permission = await appify.push.requestPermission();
if (permission === 'granted') {
const token = await appify.push.getToken();
console.log('FCM 토큰:', token);
} else {
console.log('푸시 권한이 거부되었습니다.');
}
Firebase 설정 파일 누락 여부는 네이티브 앱 개발자에게 확인합니다.
SDK가 초기화되지 않음 (SSR 환경)
증상
Next.js, Nuxt 등 SSR 환경에서 initialize()를 호출하면 에러가 발생합니다.
ReferenceError: window is not defined
원인
서버 사이드에서 SDK를 호출했습니다. SDK는 window 객체가 필요하며, 서버 환경에는 window가 존재하지 않습니다.
해결 방법
typeof window를 확인하여 클라이언트 환경에서만 SDK를 사용합니다.
// 잘못된 사용 (서버에서 실행될 수 있음)
await appify.initialize();
// 올바른 사용
if (typeof window !== 'undefined') {
await appify.initialize();
}
Next.js App Router를 사용하는 경우 'use client' 지시어를 파일 최상단에 추가하고 useEffect 내에서 초기화합니다.
'use client';
import { useEffect } from 'react';
import { appify } from '@nolraunsoft/appify-sdk';
export function AppProvider({ children }) {
useEffect(() => {
appify.initialize();
}, []);
return children;
}
동일 요청이 중복 실행됨
증상
appify.camera.scan()을 한 번 호출했는데 네이티브에서 핸들러가 두 번 이상 실행됩니다.
원인
일반적으로 이는 SDK의 요청 큐잉 동작과 관련이 없습니다. 주로 React 컴포넌트가 리렌더링되거나, 이벤트 핸들러가 여러 번 등록되어 호출이 중복 발생한 경우입니다.
SDK 내부에서 동일한 키에 대한 동시 요청은 큐에 넣어 순차 처리합니다. 즉, 두 번째 요청은 첫 번째 응답이 도착한 뒤에 전송됩니다. 네이티브 핸들러가 두 번 실행된다면 SDK에서 실제로 두 번 요청을 보낸 것입니다.
해결 방법
호출 지점에 중복 방지 로직을 추가합니다.
let isScanning = false;
async function handleScan() {
if (isScanning) return;
isScanning = true;
try {
const result = await appify.camera.scan();
processResult(result);
} finally {
isScanning = false;
}
}
React를 사용하는 경우 useRef로 진행 중 상태를 추적합니다.
const isScanningRef = useRef(false);
const handleScan = async () => {
if (isScanningRef.current) return;
isScanningRef.current = true;
try {
const result = await appify.camera.scan();
processResult(result);
} finally {
isScanningRef.current = false;
}
};