WebViewCompat.navigate를 사용한 페이지 탐색 개선

WebViewCompat.navigateWebView에서 웹페이지 로드, 기록 관리, 탐색 수명 주기 추적을 세부적으로 제어할 수 있는 WebView.loadUrl의 향상된 대안입니다.

이전에는 loadUrl를 사용하여 페이지 탐색을 시작하는 데 다음과 같은 제한사항이 있었습니다.

  • 기록 항목 대체 불가: 현재 기록 항목을 대체할 수 없어 백 스택에 항목을 추가하지 않고 새 페이지로 이동할 수 없습니다.
  • 분리된 콜백: 특정 loadUrl 호출을 WebViewClient의 후속 콜백 이벤트와 연결하는 직접적인 메커니즘이 없었습니다.
  • 추가 헤더가 저장되지 않음: loadUrl에 전달된 맞춤 헤더가 WebView 상태의 일부로 저장되지 않아 상태를 복원할 때 손실되었습니다.

WebViewCompat.navigate API는 다음 기능을 도입하여 이러한 문제를 해결합니다.

  • 탐색 기록 항목 대체: WebView 기록 스택에서 현재 페이지를 대체할 수 있습니다.
  • 상관관계가 지정된 콜백 추적: 탐색 수명 주기의 모든 단계에서 고유 식별자 역할을 하는 Navigation 객체를 반환합니다.
  • 저장된 상태 헤더 지원: 상태 복원 시 재사용할 수 있도록 추가 헤더가 WebView 상태 번들에 안정적으로 저장됩니다.

주요 기능 및 제한사항

WebViewCompat.navigate를 채택하기 전에 다음 운영 규칙과 제약 조건을 고려하세요.

  • 스레드 안전: UI(기본) 스레드에서 WebViewCompat.navigate를 호출해야 합니다.

  • 취소 및 우선순위: 진행 중인 탐색은 명시적으로 취소할 수 없습니다. 하지만 동일한 WebView에서 새 navigate 통화를 시작하면 활성 탐색이 대체됩니다.

  • URI 스키마 지원: 표준 (예: https:http:) 및 맞춤 URI 스키마가 지원됩니다. javascript: 스킴은 지원되지 않습니다.

  • URL 크기 제한: 지원되는 최대 URL 문자열 길이는 2MB입니다.

  • 기능 확인: 다양한 WebView APK 버전 간의 호환성을 유지하려면 API를 호출하기 전에 항상 WebViewFeature.isFeatureSupported을 사용하여 기능 사용 가능 여부를 확인하세요.

내비게이션 시작 및 수명 주기 추적

탐색을 구성하고 수명 주기를 추적하려면 다음 단계를 따르세요.

  1. WebView 설정 중에 WebViewCompat.addNavigationListener를 사용하여 NavigationListener 구현을 등록하여 구조화된 수명 주기 콜백을 수신합니다. 메모리 누수와 중복 콜백 실행을 방지하려면 모든 탐색 호출에서가 아닌 한 번만 리스너를 등록하세요.
  2. NavigationParameters.Builder를 사용하여 NavigationParameters 인스턴스를 구성하여 기록 대체 또는 맞춤 HTTP 헤더와 같은 선택적 동작을 지정합니다.
  3. WebViewCompat.navigate을 호출하여 WebView 인스턴스, 도착 URL, 매개변수를 전달합니다.

WebViewCompat.navigate은 요청을 고유하게 식별하는 Navigation 객체를 반환합니다. NavigationListener 콜백에서 이 객체를 수신되는 Navigation 매개변수와 비교하여 특정 탐색을 추적합니다.

구현 예시

다음 예에서는 탐색 매개변수를 구성하고, WebViewCompat.navigate를 호출하고, 탐색 수명 주기 이벤트를 수신 대기하는 방법을 보여줍니다.

Kotlin

class WebNavigationManager(private val webView: WebView) {
    // Track the navigation instance returned by the API
    private var currentNavigation: Navigation? = null

    init {
        // 1. Define listener to observe navigation lifecycle events
        val listener = object : NavigationListener {
            override fun onNavigationStarted(navigation: Navigation) {
                if (navigation == currentNavigation) {
                    // Navigation started
                }
            }

            override fun onNavigationRedirected(navigation: Navigation) {
                if (navigation == currentNavigation) {
                    // Navigation encountered a redirect
                }
            }

            override fun onNavigationCompleted(navigation: Navigation) {
                if (navigation == currentNavigation) {
                    if (navigation.didCommit()) {
                        // Navigation committed successfully
                    } else if (navigation.didCommitErrorPage()) {
                        // Navigation committed an error page
                        val statusCode = navigation.statusCode
                        val error = navigation.webResourceError
                    }
                }
            }

            override fun onFirstContentfulPaintMillis(page: Page, durationMillis: Long) {
                // Match page with current navigation
                if (page == currentNavigation?.page) {
                    // Page rendering started (First Contentful Paint achieved)
                }
            }
        }

        // 2. Register listener on the main thread
        WebViewCompat.addNavigationListener(webView, listener)
    }

    @UiThread
    fun navigateToPage(url: String) {
        // Check feature availability
        if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEBVIEW_NAVIGATE_EXPERIMENTAL_V1)) {
            // Fall back to standard loadUrl if navigate API is unavailable
            webView.loadUrl(url)
            return
        }

        // 3. Configure navigation parameters
        val params = NavigationParameters.Builder()
            .setShouldReplaceCurrentEntry(true)
            .addAdditionalHeaders(
                mapOf("X-Test-Navigate-Header" to "TestValue")
            )
            .build()

        // 4. Initiate navigation on the UI thread
        currentNavigation = WebViewCompat.navigate(webView, url, params)
    }
}

자바

public class WebNavigationManager {

    private Navigation mCurrentNavigation;
    private final WebView mWebView;

    public WebNavigationManager(@NonNull WebView webView) {
        mWebView = webView;
        setupListener();
    }

    private void setupListener() {
        // 1. Define listener to observe navigation lifecycle events
        NavigationListener listener = new NavigationListener() {
            @Override
            public void onNavigationStarted(@NonNull Navigation navigation) {
                if (navigation.equals(mCurrentNavigation)) {
                    // Navigation started
                }
            }

            @Override
            public void onNavigationRedirected(@NonNull Navigation navigation) {
                if (navigation.equals(mCurrentNavigation)) {
                    // Navigation encountered a redirect
                }
            }

            @Override
            public void onNavigationCompleted(@NonNull Navigation navigation) {
                if (navigation.equals(mCurrentNavigation)) {
                    if (navigation.didCommit()) {
                        // Navigation committed successfully
                    } else if (navigation.didCommitErrorPage()) {
                        // Navigation committed an error page
                        int statusCode = navigation.getStatusCode();
                        WebResourceErrorCompat error = navigation.getWebResourceError();
                    }
                }
            }

            @Override
            public void onFirstContentfulPaintMillis(@NonNull Page page, long durationMillis) {
                if (mCurrentNavigation != null && page.equals(mCurrentNavigation.getPage())) {
                    // Page rendering started (First Contentful Paint achieved)
                }
            }
        };

        // 2. Register listener on the main thread
        WebViewCompat.addNavigationListener(mWebView, listener);
    }

    @UiThread
    public void navigateToPage(@NonNull String url) {
        // Check feature availability
        if (!WebViewFeature.isFeatureSupported(WebViewFeature.WEBVIEW_NAVIGATE_EXPERIMENTAL_V1)) {
            // Fall back to standard loadUrl if navigate API is unavailable
            mWebView.loadUrl(url);
            return;
        }

        // 3. Configure navigation parameters
        NavigationParameters params = new NavigationParameters.Builder()
            .setShouldReplaceCurrentEntry(true)
            .addAdditionalHeaders(Collections.singletonMap(
                "X-Test-Navigate-Header", "TestValue"
            ))
            .build();

        // 4. Initiate navigation on the UI thread
        mCurrentNavigation = WebViewCompat.navigate(mWebView, url, params);
    }
}

HTTP 헤더를 사용하여 앱 상태 전파

웹 앱은 백엔드 로직을 조정하거나 웹 콘텐츠를 맞춤설정하기 위해 호스트 Android 앱의 컨텍스트가 필요한 경우가 많습니다. 이 정보를 전달하기 위해 URL에 쿼리 매개변수를 추가하면 URL이 복잡해지고, 캐싱이 방해되며, 내부 앱 상태가 노출될 수 있습니다.

대신 맞춤 HTTP 헤더를 사용하여 앱 컨텍스트를 전달하는 것이 좋습니다. WebViewCompat.navigateNavigationParameters를 사용하면 이 데이터를 서버로 안전하게 전송할 수 있습니다. 또한 WebView는 상태 복원 중에 이러한 헤더를 유지하므로 구성 변경 전반에서 웹 콘텐츠가 일관되게 유지됩니다. 이 지속성은 WebViewCompat.navigate를 사용하는 경우에만 적용됩니다. WebView.loadUrl를 사용하는 경우 맞춤 헤더가 WebView 상태 번들에 저장되지 않으며 복원 시 손실됩니다.

일반적인 사용 사례

호스트 앱 컨텍스트 전달의 일반적인 사용 사례는 다음과 같습니다.

  • 앱 버전 (X-App-Version): 호스트 앱의 출시 버전(예: BuildConfig.VERSION_NAME)을 전달하면 백엔드 서버에서 네이티브 JavaScript 브리지 호환성을 확인하거나, 기능을 제한하거나, 사용자에게 이전 앱을 업데이트하라는 메시지를 표시할 수 있습니다.
  • 클라이언트 플랫폼 (X-Client-Platform): 호스트 환경을 Android로 명시적으로 식별하면 서버가 User-Agent 문자열 파싱에 의존하지 않고 플랫폼에 맞게 조정된 UI를 제공하거나 스토어 링크를 라우팅할 수 있습니다.

구현 예시

다음 예에서는 애플리케이션 버전과 클라이언트 플랫폼을 웹 서버에 전달하는 방법을 보여줍니다.

Kotlin

// Attach host app metadata so the server can verify compatibility and tailor content
val params = NavigationParameters.Builder()
    .addAdditionalHeaders(
        mapOf(
            "X-App-Version" to BuildConfig.VERSION_NAME,
            "X-Client-Platform" to "Android"
        )
    )
    .build()

// Use navigate instead of loadUrl to retain custom headers across state restoration
WebViewCompat.navigate(webView, "https://www.example.com", params)

자바

// Attach host app metadata so the server can verify compatibility and tailor content
Map<String, String> headers = new HashMap<>();
headers.put("X-App-Version", BuildConfig.VERSION_NAME);
headers.put("X-Client-Platform", "Android");

NavigationParameters params = new NavigationParameters.Builder()
    .addAdditionalHeaders(headers)
    .build();

// Use navigate instead of loadUrl to retain custom headers across state restoration
WebViewCompat.navigate(webView, "https://www.example.com", params);

실패 모드 및 오류 처리

WebViewCompat.navigate API는 구성 오류와 런타임 탐색 실패를 처리하는 별도의 메커니즘을 제공합니다.

잘못된 인수 예외

잘못된 인수를 전달하면 동기 IllegalArgumentException가 트리거됩니다. 일반적인 원인은 다음과 같습니다.

  • 필수 null이 아닌 매개변수 (webView, url 또는 params)에 null을 전달합니다.
  • 지원되지 않는 URL 스키마(예: javascript:)를 제공합니다.
  • RFC 2616 사양을 준수하지 않는 잘못된 HTTP 헤더 키 또는 값을 전달합니다.

네트워크 요청 또는 페이지 로드 중에 오류가 발생하면 (예: HTTP 404 상태 코드, DNS 변환 실패, SSL 오류) WebViewCompat.navigate는 여전히 유효한 Navigation 객체를 반환합니다.

탐색이 완료되면 onNavigationCompleted 콜백 내의 Navigation 인스턴스에서 다음 메서드를 검사하여 실패를 진단합니다.

  • getStatusCode: HTTP 응답 상태 코드 (예: 404 또는 500)를 반환합니다.
  • getWebResourceError: 연결 시간 초과 또는 호스트 조회 실패와 같은 네트워크 오류를 자세히 설명하는 WebResourceErrorCompat 객체를 반환합니다.
  • didCommitErrorPage: WebView이 커밋되었는지, 사용자에게 오류 페이지가 표시되었는지를 나타냅니다.
  • didCommit: 탐색이 중단되지 않고 타겟 페이지에 커밋되었는지 여부를 나타냅니다.

상태 번들 관리 저장

NavigationParameters로 추가 헤더를 전달하면 WebView는 상태 복원 시 재사용할 수 있도록 저장된 상태 번들에 이러한 헤더를 저장합니다. 하지만 헤더 컬렉션이 크면 저장된 상태 Bundle의 크기가 크게 증가할 수 있습니다.

Android 상태 저장 중에 TransactionTooLargeException를 방지하기 위해 번들 크기를 제한해야 하는 경우 WebViewCompat.saveState를 사용하세요. 이 메서드를 사용하면 최대 번들 크기 한도를 바이트 단위로 설정하고 선택적으로 이전 기록 항목을 제외할 수 있습니다.

Kotlin

// Save state with a maximum bundle size limit (for example, 64 KB)
val maxSizeBytes = 64 * 1024
val includeForwardState = false
val outState = Bundle()

WebViewCompat.saveState(webView, outState, maxSizeBytes, includeForwardState)

자바

// Save state with a maximum bundle size limit (for example, 64 KB)
int maxSizeBytes = 64 * 1024;
boolean includeForwardState = false;
Bundle outState = new Bundle();

WebViewCompat.saveState(webView, outState, maxSizeBytes, includeForwardState);

결과 번들은 표준 WebView.restoreState 메서드와 호환됩니다.

이전 및 구현 권장사항

WebView에서 탐색할 때 최적의 성능과 안정성을 보장하려면 다음 권장사항을 따르세요.

  • loadUrl에서 navigate로 마이그레이션: 모든 기존 WebView.loadUrl 호출을 WebViewCompat.navigate로 마이그레이션합니다. 이렇게 하면 기록 관리가 균일해지고 헤더가 항상 저장된 상태의 일부로 저장됩니다.

  • 항상 기능 지원 확인: API를 호출하기 전에 WebViewFeature.isFeatureSupported로 런타임 지원을 확인하여 이전 WebView 버전으로부터 보호하세요.

  • 탐색 인스턴스 상관관계 지정: 반환된 Navigation 객체를 사용하여 동시 탐색을 구분하거나 여러 WebView 인스턴스를 관리할 때 콜백을 필터링합니다.

  • 초기화 중에 리스너를 한 번 등록: WebViewCompat.addNavigationListener는 기존 리스너를 대체하는 대신 리스너를 추가하므로 WebView 설정 중에 NavigationListener를 한 번 등록하여 후속 탐색에서 메모리 누수와 중복 콜백 실행을 방지하세요.

  • 저장 상태 크기 모니터링: 큰 헤더 페이로드를 전달할 때는 명시적 크기 경계가 있는 WebViewCompat.saveState를 사용하여 과도한 상태 데이터가 저장되지 않도록 합니다.

추가 리소스

삽입된 웹 기능 및 성능 최적화에 대해 자세히 알아보려면 다음 가이드를 참고하세요.