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입니다.

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

탐색 시작 및 수명 주기 추적

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

  1. WebView 설정 중에 WebViewCompat.addNavigationListener를 사용하여 NavigationListener 구현을 등록하여 구조화된 수명 주기 콜백을 수신합니다. 메모리 누수 및 중복 콜백 실행을 방지하려면 모든 탐색 호출에서가 아니라 한 번만 리스너를 등록하세요.
  2. NavigationParameters 인스턴스를 구성하여 NavigationParameters.Builder기록 대체 또는 커스텀 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);
    }
}

장애 모드 및 오류 처리

WebViewCompat.navigate API는 구성 오류 및 런타임 탐색 실패를 처리하기 위한 고유한 메커니즘을 제공합니다.

잘못된 인수 예외

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

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

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

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

  • 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를 사용하여 과도한 상태 데이터가 저장되지 않도록 합니다.

추가 리소스

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