
React Native WebView + Kakao Maps JS SDK 로컬 개발 중 겪은 문제와 해결 과정 정리
대상: HealingTrip FE (Expo + react-native-webview + Vite 지도 페이지)
WebView = 멍청한 지도 렌더러, 데이터·상태·UI = 전부 RN
| 영역 | 담당 |
|---|---|
| 장소 fetch, 필터, 바텀시트, 검색 | React Native |
| 지도 렌더링, 마커, 카메라 | WebView (Kakao JS SDK) |
| RN ↔ WebView 통신 | { type, payload } 메시지 프로토콜 |
web/map/ ← Vite로 빌드하는 지도 전용 페이지 (main.js)
components/map/ ← MapWebView.tsx
hooks/ ← useMapBridge.ts (브릿지 + MAP_READY 큐)
app/(tabs)/map/ ← 지도 탭 화면
지도 페이지는 EXPO_PUBLIC_MAP_URL (로컬: http://localhost:5174)로 WebView에 로드.
http://localhost:5174 → 지도 OK초기 가설 (틀린 것들)
load start/end 로그 나옴, HTML 렌더됨<p> 디버그 텍스트는 보임| 로그 | 의미 |
|---|---|
PAGE_BOOT | main.js 실행됨 |
SDK_LOADING | SDK script 요청 시작 |
SDK_SCRIPT_LOADED | sdk.js 로드 완료 |
MAP_INIT_START | initMap 진입 시도 |
MAP_INIT_POLL + hasMapCtor: false | kakao.maps는 있는데 Map 생성자가 영원히 안 생김 |
MAP_INIT_ATTEMPT → MAP_READY | 드디어 성공 |
카카오 SDK 1차 스크립트(dapi.kakao.com)는 로드됐지만, Map 생성자를 등록하는 2차 모듈(daumcdn.net 등) 요청이 iOS WebView ATS에 막혔다.
1단계: sdk.js (dapi.kakao.com) → kakao.maps + load() 함수만 생김 ✅
2단계: 내부 모듈 (daumcdn.net) → kakao.maps.Map 생성자 등록 ❌ (ATS 차단)
그래서 디버그 로그가 이렇게 나왔다:
{
"hasKakao": true,
"hasMaps": true,
"hasMapCtor": false,
"hasLoad": true,
"mapsKeys": ["onloadcallbacks", "readyState", "load", "apikey", ...]
}
껍데기만 있고 Map이 없는 상태. kakao.maps.load() 콜백도 영원히 안 불림.
| Safari | RN WebView (WKWebView) | |
|---|---|---|
| sdk.js | ✅ | ✅ |
| daumcdn.net 모듈 | ✅ | ❌ (ATS) |
| kakao.maps.Map | ✅ | ❌ |
같은 URL, 같은 HTML인데 WKWebView만 실패 → 네트워크 정책(ATS) 차이.
app.json ATS 설정Expo app.json → prebuild → ios/Info.plist에 반영.
"NSAppTransportSecurity": {
"NSAllowsLocalNetworking": true,
"NSExceptionDomains": {
"kakao.com": {
"NSIncludesSubdomains": true,
"NSExceptionAllowsInsecureHTTPLoads": true,
"NSExceptionRequiresForwardSecrecy": false
},
"daum.net": { ... 동일 ... },
"daumcdn.net": { ... 동일 ... }
}
}
| 도메인 | 용도 |
|---|---|
kakao.com | SDK API (dapi.kakao.com) |
daum.net / daumcdn.net | 지도 타일·내부 모듈 CDN |
Android 로컬 dev: "usesCleartextTraffic": true
app.json 변경은 Metro reload로 반영 안 됨.
npx expo prebuild --clean --platform ios # Info.plist 재생성
npx expo run:ios --no-build-cache # 재빌드
--clear는expo start전용.run:ios는--no-build-cache.
재빌드 후:
MAP_INIT_ATTEMPT → MAP_READY {"width":402,"height":690}
| 조치 | 효과 |
|---|---|
app.json ATS (daumcdn.net) | 근본 해결 |
단계별 postMessage 로그 | 원인 좁히기 (hasMapCtor) |
main.js에서 dynamic script load | HTML onload 속성보다 WebView에서 안정적 |
#map flex 100% + map.relayout() | 레이아웃 이슈 예방 |
domStorageEnabled 등 WebView props | 카카오맵 일반 권장 설정 |
| 조치 | 판단 |
|---|---|
| HTTP vs HTTPS 가설 | 로컬 HTTP 다른 앱은 WebView OK → 원인 아님 |
messageQueue / 폴링 / maps.load 재시도 | ATS 해결 전엔 Map 자체가 안 생김 |
autoload=true/false 전환 | ATS 없으면 둘 다 Map 안 생김 |
HTML <script> | WKWebView에서 콜백 미호출 cases |
RN과 WebView는 서로 다른 JS 런타임 → 함수 직접 호출 불가, 메시지 프로토콜 사용.
mapRef.current?.post({ type: 'SET_MARKERS', payload: markers });
↓ injectJavaScript로 WebView 안에서 실행:
window.__handleNative({"type":"SET_MARKERS","payload":[...]}); true;
↓ main.js:
window.__handleNative = (msg) => {
switch (msg.type) {
case 'SET_MARKERS': renderMarkers(msg.payload); break;
// ...
}
};
HTTP 요청이 아니라, RN이 WebView JS 컨텍스트에 코드를 주입(inject)하는 것.
window.ReactNativeWebView.postMessage(JSON.stringify(msg));
↓ RN onMessage → useMapBridge switch
MAP_READY 전에 보낸 post()는 큐에 쌓았다가 ready 후 flush. RN은 타이밍 신경 안 써도 됨.
안 씀. WebView = 마커 + 카메라만. 검색/바텀시트/필터는 RN. vanilla JS + types/map.ts 프로토콜이 적합.
| 파일 | 역할 | 되돌리면 안 되는 것 |
|---|---|---|
app.json | iOS ATS, Android cleartext | daum.net / daumcdn.net 예외 |
web/map/index.html | #map 레이아웃, __KAKAO_JS_KEY | flex 100%, key 주입 |
web/map/main.js | 브릿지 + 카카오 init | __handleNative, bootKakao, send |
web/map/vite.config.js | 키 치환, port 5174 | inject-kakao-key 플러그인 |
hooks/useMapBridge.ts | post/onMessage, MAP_READY 큐 | — |
components/map/MapWebView.tsx | WebView 래퍼 | domStorageEnabled, EXPO_PUBLIC_MAP_URL |
web/map/.env → VITE_KAKAO_JS_KEY.env → EXPO_PUBLIC_MAP_URL=http://localhost:5174http://localhost:5174 도메인 등록cd web/map && npm run devnpx expo run:ios (최초 또는 app.json 변경 후 prebuild)MAP_READY / ERROR 확인?app.json ATS 커밋돼 있고 네이티브 재빌드 했?아님. app.json이 git에 있으면 clean 후에도 Info.plist에 다시 생성됨.
날아가는 건 Xcode에서 Info.plist만 수기 수정한 경우.
WebView 문제 vs 지도 문제 구분
HTML 렌더 + PAGE_BOOT OK → WebView 연결 OK. 지도만 fail → SDK/ATS/레이아웃.
Safari OK + RN WebView fail
같은 URL이면 SDK 도메인보다 iOS ATS / WKWebView 네트워크 정책 먼저 의심.
카카오 SDK는 2단계 로드
sdk.js만 OK ≠ 지도 OK. hasMapCtor: false = 2차 모듈 실패.
Expo에서 네이티브 설정 = app.json
Info.plist 수기 수정 < app.json 커밋 + prebuild. 실무에서도 흔한 패턴.
디버그는 postMessage 단계 로그가 최고
injectJavaScript / postMessage 경계에서 { type } 찍으면 원인 빠르게 좁혀짐.
HTTPS 배포 URL은 로컬 WebView 디버깅 우회책
아키텍처 문서 권장대로 배포 URL 쓰면 ATS 이슈 적음. 로컬 dev는 ATS 설정 필요.
docs/specs/map-webview-architecture.mdWebView + 카카오맵에서 "지도만 안 보인다"는 증상의 80%는 bridge 타이밍이 아니라, SDK 2차 모듈 CDN이 iOS ATS에 막힌 것이었다.
브라우저/Safari와 WebView의 차이를 먼저 의심하고,
postMessage로 SDK 로딩 단계를 계측하면 빠르게 찾을 수 있다.