Corca Medium 아카이브
iOS WebView에서 postMessage 소통 이슈. iOS와 웹 통신 트러블 슈팅
오늘은 웹과의 통신을 제공하는 iOS 플러그인 제작 중 겪은 문제에 대해 소개해보려고 합니다.
현재 코르카에선 ADCIO 라는 Retail Media Platform 서비스를 제공하고 있습니다.
그중에서도 저희 App팀은 ADCIO 고객인 서비스 개발사가 자사 서비스에 ADCIO 서비스를 붙일 수 있도록 Android, iOS, Flutter 등 플랫폼에 대응하는 플러그인을 제작하고 있습니다.
ADCIO Agent(에이전트)? 🧐 ADCIO 서비스 중 Agent(에이전트)라는 기능이 있는데요, 에이전트는 고객을 응대하는 LLM 기반 AI 챗봇으 로 대화를 통해 고객이 원하는 상품을 추천하는 서비스에요.
웹 기반으로 구현이 되어 ADCIO 서비스 유저라면 아래와 같이 제공하고 있어요.
앱 플러그인에선 위와 같은 Agent 웹 화면을 WebView로 제공하여 클라이언트 앱 개발자들이 쉽게 적용할 수 있도록 합니다.
“왜 WebView를 이용하지? 직접 App 화면으로 개발 정돈 해놔야 되는 거 아닌가?”
“App 플러그인(라이브러리)으로 제공할 필요가 있나? WebView는 다들 붙일 수 있지 않나?”
라는 생각이 드실 수 있지만, 유지보수성 관리 측면에서 iOS, Android, Flutter, Web의 Sync(동기화)를 맞춰 가며 업데이트를 하는 게 리소스 비용이 많이 들어서 우선 한 곳으로 관리하는 게 좋겠다 싶었어요.
그리고 클라이언트 앱 개발자가 Webview 구현 그리고 클릭했을 때 라우트 효과를 열어놓는 등 부가적인 기 능을 직접 구현하는 것 대신, 플러그인 내에서 쉽게 제공하자는 목적으로 인해 Agent 다양한 플랫폼의 App 플러그인 탄생하게 되었습니다.
WebView 플러그인 내 postMessage() 사용 📥 Agent 플러그인 기능 중, 에이전트가 추천한 상품을 클릭하면 클라이언트 앱 서비스 내 등록된 제품의 고유 값인 productId 문자열을 반환하는 기능이 있는데요.
Web 내에서 상품을 클릭할 때 JavaScript postMessage() 함수를 사용하여 앱에 productId(message 문자열) 을 발송하고, 그 발송 된 메시지를 감지하여 유저가 쉽게 사용할 수 있게 onClickProduct(productId) 함수로 제공 합니다.
사용자 즉, 클라이언트 개발자는 onClickProduct(productId) 함수를 받아 유저가 상품을 클릭 했을 때 후발 액션을 자유롭게 커스텀할 수 있는거죠. (라우트나 토스트 출력 등)
정리해보자면, 아래와 같을 것 같아요.
🖥 (Web): 제품 클릭 시 productId 문자열 보낼게 📱 (App): WebView로 구현하고 웹뷰 내에서 메시지 보낸거 팔로업 할게 📱 (App): 팔로업 중 메시지 받으면 즉시 유저 사용함수로 매개변수 보낼게 🧑🏻💻 (Client App): 플러그인 내 유저 사용함수에 상품 클릭 후 처리 액션 함수로 구현할게 Android 코드를 예시로 들어 설명을 드리면 아래와 같습니다.
🖥 제품 클릭 시 문자열 보낼게 (Web): productId window.BridgeRouter.postMessage("productId") 📱 로 구현하고 웹뷰 내에서 메시지 보낸거 팔로업 할게 (App): WebView 📱 팔로업 중 메시지 받으면 즉시 유저 사용함수로 매개변수 보낼게 (App):
→ Android 수신 구현 코드 (in. 플러그인) @SuppressLint("SetJavaScriptEnabled") agentUrl: String ): AgentPageManager { init { webView.apply { settings.apply { ...
} loadUrl(agentUrl) } webView.addJavascriptInterface(ProductRouterJavascriptInterface(), "ProductRouter") } } @JavascriptInterface AdcioAgent().setProductId(productId) } } 🧑🏻💻 플러그인 내 유저 사용함수에 상품 클릭 후 처리 액션 함수로 구현할게 (Client App):
→ Android 클라이언트 사용자 사용 예시 코드 // productId를 활용한 Toast 예시 // productId를 활용한 화면 이동 예시 intent.putExtra("productId", productId) startActivity(intent) } } 🚨 문제 상황 위에서 보셨다시피, App 플러그인 코드 내에선 팔로업을 하고 있고, 메시지를 받는걸 감지해야합니다.
📱 웹뷰 내에서 메시지 보낸거 팔로업 할게 (App): 📱 팔로업 중 메시지 받으면 즉시 유저 사용함수로 매개변수 보낼게 (App): Swift 수신 구현 코드:
print(message.body) } } } 하지만 에서는 브릿지 요청을 전혀 감지하지 못하였습니다 🥲 ? iOS .
iOS가 웹의 브릿지 요청을 감지 못하는 이슈의 트러블 슈팅 과정을 들려드리도록 하겠습니다..
step 1. 기존 코드 분석 print(message.body) } } } 기존에 작성되어있던 소스 코드는 WKScriptMessageHandler 프로토콜의 userContentController 함수에서 브 릿지 요청을 감지한 후, message name을 확인하고 body 값을 가져오는 코드입니다.
해당 WKScriptMessageHandler 프로토콜은 iOS에서 브릿지 요청을 감지할 때 통상적으로 많이 사용됩니다.
하지만? 슬프게도 아무리 클릭을 해도 위 함수가 호출되지 않고 반응이 없었습니다..
iOS에서 postMessage 함수를 감지하지 못하여 발생하는 문제인지 확실히 하기 위해 뷰가 로드 되고 나서 iOS 위치에서 JavaScript 코드를 직접 실행해 보았습니다.
iOS 내에서 evaluateJavaScript 함수를 사용하여 JavaScript postMessage 함수를 직접 실행하는 방식으로 JavaScript postMessage 함수가 무조건 실행이 되니 감지가 되지 않을까? 싶었는데..
js 발송 코드 (swift):
let javascriptCode = """ BridgeRouter.postMessage('productId'); """ return webView } message 받는 코드 (swift): print(message.body) } } } 하지만 여전히 iOS는 postMessage 를 감지하지 못했습니다.
이번 단계에서는 iOS에서 Web의 postMessage 요청을 감지하지 못해 발생하는 문제라는 것을 확신하게 되었 습니다.
참고 코드: https://github.com/Lision/WKWebViewJavascriptBridge 해당 라이브러리는 감지를 원하는 Bridge Name을 파라미터로 전달하면, 해당 요청을 감지한 후 결과 값을 반환하는 iOS 함수를 제공합니다.
이번 Step에서는 실제로 사용되는 라이브러리와 Agent SDK 코드를 비교하여 차이점을 확인하고 코드를 수 정하려고 하였습니다.
요청 감지 코드
Bridge
// 라이브러리의 Bridge 요청 감지 // base.injectJavascriptFile() } } // Agent SDK의 Bridge 요청 감지
print(message.body) } } WKScriptMessageHandler 프로토콜의 userContentController 함수를 사용하여 Bridge 요청을 감지하는 부분은 동일했지만 Handler를 등록하는 부분에서 작은 차이를 발견하였습니다.
등록 코드
Handler
// 라이브러리의 Handler 등록: Bridge 관리하는 Avoider를 등록 configuration.userContentController // Agent SDK의 Handler 등록: WKScriptMessageHandler를 구현하는 class 등록 Controller에 등록하는 Bridge Handler 클래스의 차이를 확인하고 라이브러리에 맞추어 변경해주었습니다.
변경 코드
AdcioCoordinator(self)
} 라이브러리와 동일하게 makeCoordinator 함수에서 저의 Handler를 관리하는 class를 반환하고 Controller 에 등록해주었으나, 이 변경은 iOS에서 웹의 요청을 감지하는데 도움을 주지 못했습니다. Bridge 로직을 처 리하는 핵심 코드 부분은 큰 차이가 없었기 때문입니다.
결국 라이브러리와의 비교를 통해서는 Agent SDK 문제 해결에 도움이 되는 변경 사항을 찾지 못했습니다.
이때 부터, iOS 코드에서 벗어나 다른 플랫폼에서 발생한 유사한 문제나 웹 자체의 이슈 등을 고려하여 다양 한 각도에서 문제를 다시 검토했습니다. 여기서는 iOS 외적인 요소들이 문제의 근본이 될 수 있다는 가설을 세우고 해당 가설을 검증하기 위해 다른 예시 및 플랫폼의 코드를 살펴보기로 했습니다.
step 4. 다른 플랫폼의 Agent SDK 구현 코드 참고 이미 구현 된 Android, Flutter와 같이 다른 플랫폼에서의 Agent SDK 구현 코드 참고 해보기로 하였습니다.
Android: addJavascriptInterface
Flutter: addJavaScriptChannel
를 통해 브릿지 요청을 감지하고 productId를 정상적으로 받아왔습니다.
Android와 Flutter에서 정상적으로 감지되는 부분이 iOS에서만 작동하지 않아, iOS가 Android 와 Flutter와 다른 특성을 가지고 있을 수 있다는 생각이 들어 iOS의 기본 브라우저인 Safari와의 호환성 문제, 특정 환경에 서의 동작 차이 등을 의심해보았습니다.
iOS는 특히나 다양한 보안 및 제약 사항이 있고 Safari는 특히 웹 페이지의 보안 및 안전성을 강화하기 위한 다양한 정책을 가지고 있어서 JavaScript 브릿지 패턴이 iOS에서는 다르게 동작할 수 있다 생각이 들었습니 다.
위 내용을 정리해 ChatGPT에게 질문해보았습니다.
window.BridgeRouter.postMessage(message) 해당 코드는 웹에서 앱으로 브라우저를 통해 요청을 보낼 때 사용하는 방식으로, 모든 플랫폼에서 통용됩니 다.
그러나 Safari에서 지원하지 않는 기능을 사용하는 경우 postMessage() 요청을 감지하지 못하는 상황이 발생 할 수 있다고 합니다. Safari에서 제공하는 API 함수를 사용하여 요청을 보내는 것이 안전하고 정확한 방법이 라고 답변을 얻었습니다.
Solution!
Web에서 iOS로 보내는 요청을 Safari에서 제공하는 API를 사용하여 변경하였습니다.
💡
웹 postMessage 함수
Android & Flutter : window.BridgeRouter.postMessage iOS : window.webkit.messageHandlers.BridgeRouter.postMessage(message) iOS에서는 안정성 및 호환성을 위해 window.webkit.messageHandlers API를 사용하고 권장합니다.
결국 여러 시도 끝에 iOS 코드 변화 없이 Web 요청 방식으로 해결이 되어 허탈하긴 한데 팔로업 코드만 수정 하며 대처하는게 아니라 요청 방식 또한 브라우저 별로 잘 고려해봐야 겠다는 것을 깨달았습니다.
여러분들도 혹시나 같은 이슈로 js postMessage가 캐치업이 안되었다면, 해당 글이 도움이 되어 쉽게 해결 되 길 바라며 이상 글을 마치도록 하겠습니다.
감사합니다.
기술 발전의 혜택을 모두가 누리게 하여 인류 문명의 발전에 기여하는 코르카