웹뷰 개발을 하면서 다시 만날 문제들

요즘은 웹뷰를 안쓰는 앱이 없을 정도로 빼놓을 수 없는 기술이 되었다고 생각하는데요.

실무에서 웹뷰를 다루면서 겪은 경험과 해결책을 정리하고 같은 이슈를 만났을 때 현명하게 대처하고자 합니다.

개인적으로 웹뷰를 쓰는 이유

  • 장점

    • 하나의 코드로 여러 플랫폼을 지원하여 생산성이 높다

    • 스토어 배포 없이 업데이트 가능합니다.

    • 런타임에서 웹 코드 격리되어 앱이 죽지 않습니다.

  • 단점

    • 네이티브 대비 상대적인 속도는 느릴 수 있습니다.

    • 오프라인에서 사용 불가능하다.

    • 웹 기반 보안 취약점을 고려해야 합니다.

    • 스토어 심사에서 추가 고려사항이 될 수 있습니다.

하지만 장점이 단점보다 매우 크기 때문에 웹 하나로 여러 플랫폼을 지원하고 빠른 실험과 피드백을 위해서라면 한 번쯤 도입을 고민해보아도 좋을 것 같습니다.

FE개발자를 위한 웹뷰

플랫폼 구분

  • navigator.userAgent을 이용한 분기처리

라우팅

  • 웹: URL이 브라우저 히스토리에 스택으로 관리

  • 앱: 스택으로 화면 관리

⇒ 네이티브 이벤트를 활용한 웹뷰 라우팅 동기화(ex. Android onBackPressed)

앱과 웹 컴포넌트 구분?

  • 앱인지 웹뷰인지 platform 조건문으로 컴포넌트 렌더링

    • userAgent를 이용

웹 vs 앱 라우팅

  • 웹에서는 URL이 브라우저 히스토리에 스택으로 푸시

  • 앱은 네비게이터(스택)

  • 네이티브 이벤트를 이용해서 웹뷰에 보낸다

인증 관리

  • 웹앱이면 모든 처리를 웹에서 처리하면 되지만 네이티브 앱이 로그인의 기준점이 되어야 합니다. 웹앱과 달리 네이티브와 웹뷰가 공존하는 환경에서는 일관된 인증 상태 관리가 중요합니다.

  • 인증 플로우(토큰 기반)

    1. 사용자가 네이티브 앱에서 로그인

    2. 토큰을 네이티브 보안 저장소에 저장 (Android SharedPreferences에 KeyStore로 암호화, iOS Keychain)

    3. 네이티브 -> 웹뷰로 인증 정보 전달

      1. 웹뷰 로드 시 네이티브에서 토큰을 쿠키로 설정하여 전달

      2. JavaScript Interface를 통해서 토큰 전달(ex. postmessage, evaluateJavaScript)

      3. 웹뷰 로드 시 URL 쿼리파라미터로 전달

    4. 토큰 기반 인증

앱 개발자를 위한 웹뷰

브라우저(Chromium, Webkit)와 Webview는 다르다

JavaScript Override

  • 웹페이지 로그를 네이티브 개발환경으로 출력

    const originalConsoleLog = console.log;
    console.log = function(...args) {
      originalConsoleLog.apply(console, args);
      if (window.Bridge) {
        window.Bridge.sendLog('log', args);
      }
    };

웹뷰 성능 최적화

CSR 웹앱의 성능 문제

번들 로딩 문제

  • 큰 번들 사이즈: 초기 로딩 지연 및 흰 화면 현상

  • CDN 이슈: 다른 리전(ex. 미국)을 경유하여 늦어지는 현상

  • 해결책

    • 번들 분할 및 코드 스플리팅

    • S3 + CloudFront 활용하여 배포

API 호출 지연

  • 번들 로딩 완료 후 API 요청으로 순차 로딩으로 인한 지연

  • 일반적으로 Cross-domain 요청을 하게 될텐데 Preflight 요청으로 인해 100-200ms 추가 지연

  • 해결책

    • SSR 도입 검토

SSR(Server-Side Rendering) 도입

  • 장점

    • 서버에서 API 호출하여 사용자 네트워크 환경 의존성 감소

    • 초기 페이지 로딩 속도 개선

  • 단점

    • 모든 API 응답 완료까지 대기 필요

    • 서버 모니터링 및 확장성 고려 필요

    • 개별 API 실패 시 전체 페이지 지연

  • 해결책

    • Streaming SSR 도입 검토

Streaming SSR 활용

  • Suspense기준으로 준비되는만큼 부분적(청크)으로 유저에게 피드백할 수 있습니다.

// React 18의 Streaming SSR 예시
import { renderToPipeableStream } from 'react-dom/server';

const { pipe } = renderToPipeableStream(<App />, {
  onShellReady() {
    // Suspense 경계 밖의 부분(셸)이 준비되면 스트리밍 시작
    pipe(response);
  }
});

다시 만날 이슈들

iOS 키보드 focus 문제

  • 문제: focus되는 요소가 화면 아래에 있을 때 키보드 높이만큼 웹뷰가 안 보이거나 밀리는 현상

  • 해결책

    • 위로 밀리는 현상이 있으면 iOS는 높이만큼 padding, Android는 뷰의 height를 줄여줍니다.

    • 안 보이는 현상이 있으면 웹뷰가 visualViewport의 onresize 핸들러를 추가하여 scroll 위치 조정합니다.

iOS CSS (flex gap) 호환

  • 문제: iOS Safari 14.4 이하에서 flex gap 미지원

  • 해결책: 대체 CSS 사용(ex. margin)

브라우저 확대 방지

<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no" />
<!-- 또는 -->
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0" />

App Scheme 동작 실패

문제: 2개를 호출했는데 1개만 동작하는 경우

원인

  • 앱 스킴의 앱 동작 확인

  • 웹페이지의 호출 코드 확인

  • location.href 사용 시 두 번째만 수행되는 문제

해결책

  • 호출 사이에 지연시간을 준다

  • Javascript Interface방식으로 전환

문자열 Escape 동작 차이

문제: iOS, Android, JavaScript 간 문자열 처리 방식 차이

해결책

  • 앱 → 웹 호출 시 문자열(", ', \\) 적절한 인코딩/이스케이핑 필요

  • 플랫폼별 화이트스페이스 처리 규칙 통일 (공백, + 등)

쿠키 용량 제한

문제 RFC 6265에 따른 쿠키 4KB 제한으로 웹뷰가 안 열리는 경우

해결책

  • 쿠키 크기 모니터링

  • 대용량 데이터는 다른 저장소 활용

휴리스틱 캐시 문제

문제: RFC 7234에 따른 웹엔진 임의 캐시 (lastModified의 1/10)

해결책

<!-- 캐시 방지 헤더 설정 -->
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate, max-age=0">

웹 페이지가 동작하지 않는 경우(흰색 화면만)

  • iOS에서는 webViewWebContentProcessDidTerminate(_:)

  • Android에서는 onRenderProcessGone()

  • 그 밖에는 에러 처리(reload)

앱과 웹이 통신하는 방법

  • App Scheme([scheme]://[host]/[path]?[query])

  • Javascript Interface(브릿지)

    • 사전에 정의한 JS를 호출하는 방법

    • 호출 길이 제한(브라우저마다 다릅니다), URL 로딩이 없습니다.

웹 → 앱

App Scheme 방식

// 기본 호출
location.href = "[scheme]://[host]/[path]?title=webview";

// 콜백 함수와 함께 (XSS 주의)
location.href = "[scheme]://[host]/[path]?callback=<function_name>";

한계

  • Parameter 인코딩 시 URL 길이 제한

  • 동일한 App Scheme 중복 호출 시 응답 특정 불가능

  • 여러번 호출 시 누락 문제 발생 가능

JavaScript Interface 방식

Android

// 사전 정의된 네이티브 함수 호출(사전 정의된 이름 jsToNative)
window.Bridge.jsToNative(JSON.stringify({
  data: {...},
  callbackId: "callbackId"
}));

iOS

// WebKit Message Handler 사용(사전 정의된 이름 jsToNative)
window.webkit.messageHandlers.jsToNative.postMessage(JSON.stringify({
  data: {...},
  callbackId: "callbackId"
}));

통합 구현

// 사전 정의된 이름 jsToNative
function jsToNative(data, callback) {
  const callbackId = {unique_id};
  
  // 콜백 등록
  window.addEventListener(callbackId, callback, { once: true });
  
  if (isIos()) {
    window.webkit?.messageHandlers.jsToNative.postMessage(
      JSON.stringify({ data: {...data}, callbackId })
    );
  } else if (isAndroid()) {
    window.Bridge?.jsToNative(
      JSON.stringify({ data: {...data}, callbackId })
    );
  }
}

앱 → 웹

직접 함수 호출

// 전역 함수 정의
function handleNativeEvent(data) {
console.log('Native event received:', data);
}

네이티브(Android, iOS)

webView.evaluateJavaScript("handleNativeEvent('test')")

JavaScript Custom Event

window.addEventListener('nativeEvent', (event) => {
  console.log('Event detail:', event.detail);
});

네이티브(Android, iOS)

let script = """
  window.dispatchEvent(new CustomEvent('nativeEvent', {
    detail: data
  }));
"""
webView.evaluateJavaScript(script)

장점

  • 앱: 예외처리 불필요

  • 웹: eventListener 사용으로 별도 관리 리소스 불필요

디버깅 도구

  • iOS

    • iOS 16.4 이전: 개발 빌드에서만 인스펙터 사용 가능 (기기 등록 수 제한)

    • iOS 16.4 이후: 안드로이드와 같이 인스펙터 지원

  • Android

  • Library

    • Pulse: 앱/Mac에서 실시간 로그 확인

    • Eruda: 모바일 웹뷰 내 개발자 도구

참고

Last updated