Ari.dev

웹뷰 안의 로그인 상태가 앱과 따로 놀았다 — iOS/Android 브릿지로 토큰을 양방향 동기화한 이야기

하이브리드 앱의 웹뷰와 네이티브 앱이 로그인 토큰·멤버십 정보를 서로 다르게 들고 있었다. React Native 표준 브릿지를 쓸 수 없는 순수 네이티브 앱 환경에서, iOS·Android 각각 다른 웹뷰 API로 양방향 동기화를 구현한 과정.

아키텍처

관련 프로젝트: B2C 레거시 리팩토링

배경

앱 안에 웹뷰를 얹은 하이브리드 구조였다. 로그인, 토큰 재발급, 로그아웃 같은 인증 상태가 웹 화면과 네이티브 앱 양쪽에 동시에 존재해야 했다 — 웹에서 로그인하면 앱도 그걸 알아야 하고, 앱에서 토큰을 갱신하면 웹도 새 토큰을 써야 한다.

문제 — 표준 브릿지를 쓸 수 없는 환경

React Native 앱이라면 ReactNativeWebView.postMessage라는 표준 브릿지가 있다. 하지만 이건 순수 네이티브(Swift/Kotlin) 앱에 웹뷰를 얹은 구조라 이 표준을 쓸 수 없었다. 대신 iOS와 Android가 각각 자신만의 웹뷰-네이티브 통신 방식을 제공하는데, 이 둘이 서로 완전히 다르다.

  • iOS: WKWebView의 messageHandlers를 통해 웹 → 네이티브로 메시지를 보낸다.
  • Android: @JavascriptInterface로 노출한 커스텀 객체(window.Android.*)를 웹에서 직접 함수처럼 호출한다.

플랫폼마다 API가 다르니, 웹 쪽 코드에서 분기 처리 없이 하나로 뭉뚱그릴 수 없었다.

접근 — 방향별로 다른 메커니즘을 각각 구현

동기화해야 할 데이터는 명확했다: 액세스/리프레시 토큰, 멤버십 정보, 로그인/로그아웃 상태. 방향은 두 갈래였고, 각 방향의 구현 방식도 서로 달랐다.

웹 → 앱 (웹에서 네이티브 함수를 호출):

// iOS
window.webkit.messageHandlers.callbackHandler0.postMessage({
  accessToken, refreshToken
});

// Android
window.Android.setTokenInfo(accessToken, refreshToken);

앱 → 웹 (네이티브가 전역 함수를 직접 호출):

// 네이티브가 웹뷰의 JS 컨텍스트에 주입해둔 전역 함수를 직접 실행
window.setAccessToken = (accessToken, refreshToken) => {
  // 새 토큰을 웹 쪽 상태에 반영
};
window.setUserMembershipInfo = (info) => {
  // 멤버십 정보 갱신
};

주목할 점은 앱 → 웹 방향이 이벤트 리스너를 등록해서 "받는" 구조가 아니라, 네이티브가 웹뷰에 미리 정의된 전역 함수를 찾아 직접 호출하는 구조라는 것이다. 웹 쪽은 그 함수들을 전역에 미리 걸어두기만 하면 되고, 언제 호출될지는 네이티브 쪽 타이밍에 달려 있다.

결과

  • 토큰 재발급, 로그인, 로그아웃 세 시점 모두에서 웹과 앱의 인증 상태가 어긋나지 않게 됐다.
  • 플랫폼별 호출부를 하나의 유틸리티 뒤로 감춰, 실제 화면 코드는 플랫폼을 몰라도 되게 정리했다.

배운 점

  • 표준이 없는 영역일수록 추상화 경계를 스스로 그어야 한다. RN 같은 표준 브릿지가 있었다면 고민할 필요가 없었을 문제인데, 표준이 없으니 iOS 문서와 Android 문서를 각각 읽고 직접 통일된 인터페이스를 설계해야 했다.
  • 양방향 동기화는 "누가 먼저 알려주는가"를 방향마다 따로 설계해야 한다. 웹 → 앱은 웹이 이벤트를 발생시키는 능동적 구조지만, 앱 → 웹은 네이티브가 웹의 준비 상태(전역 함수가 이미 정의돼 있는지)에 의존하는 구조라 타이밍 문제를 별도로 신경 써야 했다.

댓글