상황
이커머스 모바일앱은 네이티브 셸 위에 여러 웹뷰가 얹힌 하이브리드 구조다. 웹 페이지는 커스텀 스킴(emart-today://...)을 iframe에 실어 발화하고, 네이티브가 이를 가로채 페이지 이동, 앱 기능 호출 등을 수행한다.
룰렛 이벤트류 프로모션 랜딩에서 이 브릿지를 손봐야 하는 작업이 생겼다. 스킴 하나(open_main_webpage)의 동작을 정확히 이해하고, 호출부를 찾아 수정해야 했다. 단순해 보이는 작업이었지만, 문서화된 시맨틱이 없었고 — 결정적으로 — 호출부가 코드 검색에 잡히지 않았다.
문제
세 가지가 동시에 불명확했다.
1. 스킴의 시맨틱. open_main_webpage가 현재 페이지를 이동시키는 것인지, 새 페이지를 여는 것인지 문서가 없었다. 기존 코드에 스킴 발화 직후 reload()를 호출하는 구간이 있었는데, "페이지가 이동하는데 왜 reload를 하지?"라는 의문이 풀리지 않으면 그 코드를 지워도 되는지 판단할 수 없었다.
2. iOS에서 스킴이 어디로 들어가는가. 커스텀 스킴이라면 AppDelegate의 URL 핸들러로 들어갈 것이라는 통념이 있지만, 실제 수신 지점을 확인하지 않으면 네이티브 측 디버깅 포인트를 잘못 잡는다.
3. 호출부가 grep에 안 잡힌다. 랜딩 페이지의 버튼과 스킴 호출 핸들러를 수정해야 하는데, 저장소 전체를 검색해도 해당 HTML이 나오지 않았다. 렌더링된 페이지에는 분명히 존재하는 마크업이 코드베이스에는 없었다.
접근과 판단
스킴 시맨틱: push이지 navigate가 아니다
동작을 직접 재현해 확인한 결과, open_main_webpage는 현재 웹뷰의 URL을 바꾸는 것이 아니라 메인 웹뷰 스택에 새 페이지를 push한다. 원래 페이지는 스택 뒤에 살아남아 있고, 사용자가 뒤로가기로 복귀하면 그대로 다시 노출된다.
이 사실 하나로 수수께끼였던 코드가 해석됐다. 스킴 발화 직후의 reload()는 무의미한 코드가 아니라, 복귀 시점에 갱신된 상태(예: 이벤트 참여 완료)를 보여주기 위한 코드였다. push 시맨틱을 모르면 "이동하는데 reload가 왜 필요하지"라며 지워버릴 수 있는 지뢰였다.
iOS 라우팅 경로: AppDelegate가 아니라 navigation delegate
iframe에 스킴 URL을 실어 발화하는 방식은, iOS에서 외부 앱 열기(AppDelegate openURL)가 아니라 웹뷰의 navigation 시도로 취급된다. 따라서 수신 지점은 WKWebView의 navigation delegate(decidePolicyFor)를 감싼 커스텀 델리게이트였다.
네이티브가 스킴을 실제로 수신했는지는 디바이스 로그의 마커로 검증했다.
openWebView url : emart-today://open_main_webpage
이 로그가 찍히면 웹 → 네이티브 전달은 성공한 것이고, 이후 동작 이상은 네이티브 처리 로직이나 웹뷰 스택 쪽 문제로 범위를 좁힐 수 있다. 웹과 네이티브 사이의 책임 경계를 로그 한 줄로 자를 수 있게 된 것이다.
호출부 추적: 코드베이스 밖에 있었다
grep이 침묵한 이유는 검색 실력이 아니라 탐색 범위의 전제가 틀렸기 때문이었다. 룰렛류 랜딩의 버튼/핸들러 HTML은 저장소에 없고, DynamoDB 테이블({env}-eapp-promote-landingContent-ddb)의 contents.roulette.strGiftHtml 필드에 문자열로 저장되어 있었다. 서버가 이 값을 읽어 JSP 렌더링 시점에 페이지에 주입한다.
- DynamoDB: contents.roulette.strGiftHtml
- 서버: JSP 렌더링 시 HTML 주입
- 웹: 주입된 버튼/핸들러가 iframe 스킴 발화
- iOS: navigation delegate(decidePolicyFor) 수신
- 네이티브: 메인 웹뷰 스택에 페이지 push
이 구조가 확인되자 유지보수 판단이 명확해졌다. 수정 지점이 둘로 갈린다 — 페이지의 정적 골격은 JSP에서, 버튼/핸들러 마크업은 DynamoDB 레코드에서 고쳐야 한다. 어느 한쪽만 수정하면 렌더링 결과가 의도와 어긋난다.
결과
open_main_webpage의 push 시맨틱, iOS 라우팅 경로, DDB 저장 랜딩 HTML 구조를 팀 브릿지 문서에 추가해 다음 작업자가 같은 미로를 다시 헤매지 않게 했다.- 스킴 신뢰성 이슈의 계보를 분리해 정리했다. 연속 호출 시 앞선 스킴이 abort되는 문제(iframe 싱글톤 재사용이 원인, 호출별 iframe 생성으로 해결된 건)와, 지연 발화된 스킴이 navigation commit과 경쟁하는 레이스(scheme-timer-vs-navigation-commit-race)는 증상이 비슷해도 원인과 대응이 다른 별개 이슈다. 이 구분이 문서에 없으면 한쪽 수정으로 다른 쪽까지 고쳐졌다고 착각하기 쉽다.
배운 것
1. iframe 경유 커스텀 스킴은 iOS에서 navigation delegate로 들어간다. 하이브리드 앱에서 스킴이 "앱으로 전달되지 않는다"는 이슈를 만나면, AppDelegate가 아니라 WKWebView의 decidePolicyFor 계열부터 확인해야 한다. 발화 방식(iframe navigation vs location.href vs 외부 앱 스킴)에 따라 수신 지점이 달라진다.
2. 서버 저장 HTML이 런타임 주입되는 구조에서는 정적 코드 검색이 호출부를 놓친다. grep이 침묵하는데 렌더링 결과에는 마크업이 존재한다면, 코드베이스가 아니라 데이터 저장소(DB, CMS, 원격 설정)를 의심해야 한다. "코드는 저장소에 있다"는 전제 자체가 하이브리드/프로모션성 페이지에서는 성립하지 않을 수 있다.
3. 시맨틱을 모르는 코드는 지우지 말고 먼저 규명한다. 스킴 직후의 reload처럼 "불필요해 보이는" 코드는, 그 밑에 깔린 플랫폼 시맨틱(웹뷰 스택 push)을 확인하기 전까지는 판단을 유보해야 한다. 이상해 보이는 코드의 절반은 문서화되지 않은 시맨틱의 흔적이다.