최근 개인 프로젝트인 'Pin-Plate(맛집 기록 서비스)'를 개발하며 웹으로 만든 기능을 모바일 앱 내 웹뷰로 이식하는 작업을 진행했습니다.
분명 웹 브라우저에서는 장소 검색 기능이 아주 잘 작동했는데, 유독 모바일 앱(안드로이드 웹뷰) 환경에서만 검색 결과가 빈 화면으로 나오는 현상을 발견했습니다. 원인은 보안 정책으로 인한 위치 정보(Geolocation) 요청 차단이었고, 이를 Native Bridge로 해결한 과정을 공유합니다.
장소 검색 로직은 현재 사용자의 위경도 좌표를 받아와 Query String에 담아 API를 호출하는 방식입니다.
http://localhost:3000 접속 시 위치 정보가 정상적으로 수집됨.http://192.168.x.x:3000)로 접속 시 위치 정보를 가져오지 못해 검색 결과가 0건으로 표시됨.W3C 명세와 브라우저 보안 정책에 따르면, Geolocation API는 개인정보 보호를 위해 Secure Context(보안 컨텍스트)에서만 작동합니다.
웹 표준에서는 HTTPS 환경을 안전하다고 판단합니다. 다만, 개발 편의를 위해 http://localhost나 http://127.0.0.1은 예외적으로 안전한 환경(Secure Context)으로 간주하여 위치 정보 요청을 허용합니다.
모바일 기기에서 PC 서버 IP로 접속하는 것은 브라우저 입장에서 '안전하지 않은 원격 접속'입니다. localhost 예외 조항에 해당하지 않으므로, HTTPS가 적용되지 않은 상태에서는 위치 정보 API 호출이 원천 차단됩니다.
참고: iOS 웹뷰는 시뮬레이터나 특정 조건에서 localhost 접속 시 관대할 수 있지만, 안드로이드 웹뷰는 HTTP 환경에서의 민감한 권한 요청에 훨씬 엄격합니다.
가장 먼저 Next.js의 --experimental-https 옵션을 사용하여 로컬 서버를 HTTPS로 구동해 보았습니다. 하지만 두 가지 문제로 인해 기각했습니다.
SSL error: The certificate authority is not trusted)했습니다.onReceivedSslError를 통해 에러를 무시할 수 있지만, 이는 보안상 매우 취약하며 실제 배포 환경과는 거리가 먼 설정이라 판단했습니다.웹뷰 자체의 navigator.geolocation 기능을 포기하고, 앱(Native)의 기능을 빌려오는 Bridge 패턴을 선택했습니다.
expo-location 등을 이용해 기기 자체 GPS 좌표를 취득합니다.postMessage를 통해 웹으로 다시 전달합니다.브릿지 통신은 크게 웹에서 요청하기와 앱에서 응답하기 두 단계로 나뉩니다.
웹에서는 현재 환경이 웹뷰인지 확인한 후, 네이티브에 메시지를 보냅니다.
// types/webview.d.ts
interface Window {
ReactNativeWebView?: {
postMessage: (message: string) => void;
};
}
// components/LocationSearch.tsx
import { useEffect } from 'react';
const requestLocationFromNative = () => {
if (window.ReactNativeWebView) {
// 네이티브 앱에 위치 정보 요청 메시지 전송
window.ReactNativeWebView.postMessage(
JSON.stringify({ type: 'GET_LOCATION' })
);
} else {
console.warn("웹뷰 환경이 아닙니다.");
}
};
앱에서는 웹뷰의 onMessage를 통해 요청을 받고, expo-location으로 좌표를 구해 다시 전달합니다.
import React, { useRef } from 'react';
import { WebView, WebViewMessageEvent } from 'react-native-webview';
import * as Location from 'expo-location';
export default function App() {
const webViewRef = useRef<WebView>(null);
const onMessage = async (event: WebViewMessageEvent) => {
const message = JSON.parse(event.nativeEvent.data);
if (message.type === 'GET_LOCATION') {
// 1. 위치 권한 요청
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
console.log('위치 권한이 거부되었습니다.');
return;
}
// 2. 현재 위치 가져오기
const location = await Location.getCurrentPositionAsync({});
const { latitude, longitude } = location.coords;
// 3. 웹뷰로 좌표 전송
const response = {
type: 'SET_LOCATION',
payload: { latitude, longitude },
};
webViewRef.current?.postMessage(JSON.stringify(response));
}
};
return (
<WebView
ref={webViewRef}
source={{ uri: 'http://your-local-ip:3000' }}
onMessage={onMessage}
/>
);
}
Native 기능을 사용하기 위해 OS별 권한 설정이 필요합니다. Expo 환경에서의 설정법입니다.
expo-location 라이브러리가 필요한 설정을 자동으로 생성하도록 추가합니다.
"plugins": [
[
"expo-location",
{
"locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location."
}
]
]
ios.infoPlist)애플의 심사 기준을 준수하기 위해 구체적인 목적을 명시해야 합니다.
"ios": {
"infoPlist": {
"NSLocationWhenInUseUsageDescription": "사용자 주변 맛집 검색 및 위치 기록을 위해 현재 위치 권한을 사용합니다."
}
}
android.permissions)GPS와 기지국 기반 위치 정보를 모두 받기 위해 아래 권한들을 추가합니다.
"android": {
"permissions": [
"android.permission.ACCESS_COARSE_LOCATION",
"android.permission.ACCESS_FINE_LOCATION"
]
}
웹뷰 개발은 웹 브라우저와 네이티브 OS 사이의 보안 정책 차이를 이해하는 과정인 것 같습니다. 단순히 웹을 앱 안에 띄우는 것에 그치지 않고, Native Bridge를 적극 활용할 때 훨씬 견고하고 안정적인 사용자 경험을 만들 수 있다는 점을 배울 수 있었습니다.