인앱 콘텐츠 로드

인터넷을 통해 가져오는 대신 앱에 정적으로 컴파일하는 콘텐츠를 앱에 사용할 수 있도록 HTML, JavaScript, CSS와 같은 웹 기반 콘텐츠를 제공할 수 있습니다.

인앱 콘텐츠는 인터넷 액세스가 필요하지 않으며 사용자의 대역폭을 소비하지 않습니다. 콘텐츠가 WebView 전용으로 설계된 경우(즉, 네이티브 앱과의 통신에 종속됨) 사용자가 실수로 웹브라우저에 로드할 수 없습니다.

하지만 인앱 콘텐츠에는 몇 가지 단점이 있습니다. 웹 기반 콘텐츠를 업데이트하려면 새 앱 업데이트를 제공해야 하며, 사용자가 오래된 앱 버전을 사용하는 경우 웹사이트에 있는 내용과 기기의 앱에 있는 내용 간에 콘텐츠가 일치하지 않을 수 있습니다.

WebViewAssetLoader

WebViewAssetLoaderWebView 객체에 인앱 콘텐츠를 로드하는 유연하고 성능이 뛰어난 방법입니다. 이 클래스는 다음을 지원합니다.

  • 동일 출처 정책과의 호환성을 위해 HTTP(S) URL로 콘텐츠를 로드합니다.
  • 자바스크립트, CSS, 이미지, iframe과 같은 하위 리소스 로드

기본 활동 파일에 WebViewAssetLoader를 포함합니다. 다음은 애셋 폴더에서 간단한 웹 콘텐츠를 로드하는 예입니다.

Kotlin

private class LocalContentWebViewClient(private val assetLoader: WebViewAssetLoader) : WebViewClientCompat() {
    @RequiresApi(21)
    override fun shouldInterceptRequest(
        view: WebView,
        request: WebResourceRequest
    ): WebResourceResponse? {
        return assetLoader.shouldInterceptRequest(request.url)
    }

    // To support API < 21.
    override fun shouldInterceptRequest(
        view: WebView,
        url: String
    ): WebResourceResponse? {
        return assetLoader.shouldInterceptRequest(Uri.parse(url))
    }
}

Java

private static class LocalContentWebViewClient extends WebViewClientCompat {

    private final WebViewAssetLoader mAssetLoader;

    LocalContentWebViewClient(WebViewAssetLoader assetLoader) {
        mAssetLoader = assetLoader;
    }

    @Override
    @RequiresApi(21)
    public WebResourceResponse shouldInterceptRequest(WebView view,
                                     WebResourceRequest request) {
        return mAssetLoader.shouldInterceptRequest(request.getUrl());
    }

    @Override
    @SuppressWarnings("deprecation") // To support API < 21.
    public WebResourceResponse shouldInterceptRequest(WebView view,
                                     String url) {
        return mAssetLoader.shouldInterceptRequest(Uri.parse(url));
    }
}

앱은 필요에 맞게 WebViewAssetLoader 인스턴스를 구성해야 합니다. 다음 섹션에 예시가 있습니다.

인앱 애셋 및 리소스 만들기

WebViewAssetLoaderPathHandler 인스턴스를 사용하여 지정된 리소스 경로에 해당하는 리소스를 로드합니다. 이 인터페이스를 구현하여 앱의 필요에 따라 리소스를 가져올 수 있지만 Webkit 라이브러리는 각각 Android 애셋과 리소스를 로드하기 위한 AssetsPathHandlerResourcesPathHandler을 번들로 제공합니다.

시작하려면 앱의 애셋과 리소스를 만듭니다. 일반적으로 다음 사항이 적용됩니다.

  • HTML, 자바스크립트, CSS 등의 텍스트 파일은 애셋에 포함됩니다.
  • 이미지와 기타 바이너리 파일은 리소스에 속합니다.

프로젝트에 텍스트 기반 웹 파일을 추가하려면 다음 단계를 따르세요.

  1. Android 스튜디오에서 app > src > main 폴더를 마우스 오른쪽 버튼으로 클릭한 다음 New > Directory를 선택합니다.
    Android 스튜디오의 디렉터리 만들기 메뉴를 보여주는 이미지
    그림 1. 프로젝트의 애셋 폴더를 만드세요.
  2. 폴더 이름을 'assets'로 지정합니다.
    애셋 폴더를 보여주는 이미지
    그림 2. 애셋 폴더의 이름을 지정합니다.
  3. assets 폴더를 마우스 오른쪽 버튼으로 클릭한 다음 새로 만들기 > 파일을 클릭합니다. index.html를 입력하고 Return 또는 Enter 키를 누릅니다.
  4. 이전 단계를 반복하여 stylesheet.css의 빈 파일을 만듭니다.
  5. 다음 두 코드 샘플의 콘텐츠로 만든 빈 파일을 입력합니다.
```html
<!-- index.html content -->

<html>
  <head>
    <!-- Tip: Use relative URLs when referring to other in-app content to give
              your app code the flexibility to change the scheme or domain as
              necessary. -->
    <link rel="stylesheet" href="/assets/stylesheet.css">
  </head>
  <body>
    <p>This file is loaded from in-app content.</p>
    <p><img src="/res/drawable/android_robot.png" alt="Android robot" width="100"></p>
  </body>
</html>
```

```css
<!-- stylesheet.css content -->

body {
  background-color: lightblue;
}
```

프로젝트에 이미지 기반 웹 파일을 추가하려면 다음 단계를 따르세요.

  1. Android_symbol_green_RGB.png 파일을 로컬 머신에 다운로드합니다.

  2. 파일 이름을 android_robot.png로 바꿉니다.

  3. 수동으로 파일을 하드 드라이브에서 프로젝트의 main/res/drawable 디렉터리로 이동합니다.

그림 4에서는 추가한 이미지와 앱에서 렌더링된 위 코드 샘플의 텍스트를 보여줍니다.

앱의 렌더링 출력을 보여주는 이미지
그림 4. 앱에서 렌더링된 인앱 HTML 파일 및 이미지 파일

앱을 완료하려면 다음 단계를 따르세요.

  1. 핸들러를 등록하고 onCreate() 메서드에 다음 코드를 추가하여 AssetLoader를 구성합니다.

    Kotlin

    val assetLoader = WebViewAssetLoader.Builder()
                           .addPathHandler("/assets/", AssetsPathHandler(this))
                           .addPathHandler("/res/", ResourcesPathHandler(this))
                           .build()
    webView.webViewClient = LocalContentWebViewClient(assetLoader)
    

    Java

    final WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
             .addPathHandler("/assets/", new WebViewAssetLoader.AssetsPathHandler(this))
             .addPathHandler("/res/", new WebViewAssetLoader.ResourcesPathHandler(this))
             .build();
    mWebView.setWebViewClient(new LocalContentWebViewClient(assetLoader));
    
  2. 다음 코드를 onCreate() 메서드에 추가하여 콘텐츠를 로드합니다.

    Kotlin

    webView.loadUrl("https://appassets.androidplatform.net/assets/index.html")
    

    Java

    mWebView.loadUrl("https://appassets.androidplatform.net/assets/index.html");
    

인앱 콘텐츠와 웹사이트의 리소스 혼합

앱은 인앱 콘텐츠와 인터넷의 콘텐츠(예: 웹사이트의 CSS로 스타일이 지정된 인앱 HTML 페이지)를 혼합하여 로드해야 할 수 있습니다. WebViewAssetLoader은 이 사용 사례를 지원합니다. 등록된 PathHandler 인스턴스 중 지정된 경로의 리소스를 찾을 수 없으면 WebView는 인터넷에서 콘텐츠를 로드하는 방식으로 대체됩니다. 인앱 콘텐츠와 웹사이트의 리소스를 혼합하는 경우 인앱 리소스의 디렉터리 경로(예: /assets/ 또는 /resources/)를 예약합니다. 이러한 위치에 웹사이트의 리소스를 저장하지 마세요.

Kotlin

val assetLoader = WebViewAssetLoader.Builder()
                        .setDomain("example.com") // Replace this with your website's domain.
                        .addPathHandler("/assets/", AssetsPathHandler(this))
                        .build()

webView.webViewClient = LocalContentWebViewClient(assetLoader)
val inAppHtmlUrl = "https://example.com/assets/index.html"
webView.loadUrl(inAppHtmlUrl)
val websiteUrl = "https://example.com/website/data.json"

// JavaScript code to fetch() content from the same origin.
val jsCode = "fetch('$websiteUrl')" +
        ".then(resp => resp.json())" +
        ".then(data => console.log(data));"

webView.evaluateJavascript(jsCode, null)

Java

final WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
           .setDomain("example.com") // Replace this with your website's domain.
           .addPathHandler("/assets/", new AssetsPathHandler(this))
           .build();

mWebView.setWebViewClient(new LocalContentWebViewClient(assetLoader));
String inAppHtmlUrl = "https://example.com/assets/index.html";
mWebView.loadUrl(inAppHtmlUrl);
String websiteUrl = "https://example.com/website/data.json";

// JavaScript code to fetch() content from the same origin.
String jsCode = "fetch('" + websiteUrl + "')" +
      ".then(resp => resp.json())" +
      ".then(data => console.log(data));";

mWebView.evaluateJavascript(jsCode, null);

웹 호스팅 JSON 데이터를 가져오는 인앱 HTML 페이지의 예시는 GitHub의 WebView 데모를 참고하세요.

loadDataWithBaseURL

앱이 HTML 페이지만 로드해야 하고 하위 리소스를 가로채지 않아도 되는 경우 앱 애셋이 필요하지 않은 loadDataWithBaseURL()를 사용하는 것이 좋습니다. 다음 코드 샘플에서와 같이 이를 사용할 수 있습니다.

Kotlin

val html = "<html><body><p>Hello world</p></body></html>"
val baseUrl = "https://example.com/"

webView.loadDataWithBaseURL(baseUrl, html, "text/html", null, baseUrl)

Java

String html = "<html><body><p>Hello world</p></body></html>";
String baseUrl = "https://example.com/";

mWebView.loadDataWithBaseURL(baseUrl, html, "text/html", null, baseUrl);

인수 값은 신중하게 선택하세요. 다음을 고려하세요.

  • baseUrl: HTML 콘텐츠가 로드되는 URL입니다. HTTP(S) URL이어야 합니다.
  • data: 문자열로 표시하려는 HTML 콘텐츠입니다.
  • mimeType: 일반적으로 text/html로 설정해야 합니다.
  • encoding: baseUrl가 HTTP(S) URL인 경우 사용되지 않으므로 null로 설정할 수 있습니다.
  • historyUrl: baseUrl와 동일한 값으로 설정됩니다.

앱이 동일 출처 정책을 준수하도록 하려면 HTTP(S) URL을 baseUrl로 사용하는 것이 좋습니다.

콘텐츠에 적합한 baseUrl를 찾을 수 없고 loadData()를 사용하려는 경우 백분율 인코딩 또는 Base64 인코딩으로 콘텐츠를 인코딩해야 합니다. 다음 코드 샘플과 같이 Base64 인코딩을 선택하고 Android API를 사용하여 프로그래매틱 방식으로 인코딩하는 것이 좋습니다.

Kotlin

val encodedHtml: String = Base64.encodeToString(html.toByteArray(), Base64.NO_PADDING)

webView.loadData(encodedHtml, mimeType, "base64")

Java

String encodedHtml = Base64.encodeToString(html.getBytes(), Base64.NO_PADDING);

mWebView.loadData(encodedHtml, mimeType, "base64");

피해야 할 사항

인앱 콘텐츠를 로드하는 데는 여러 가지 다른 방법이 있지만 다음과 같은 방법을 사용하지 않는 것이 좋습니다.

  • file:// URL과 data: URL은 불투명 출처로 간주되므로 fetch() 또는 XMLHttpRequest와 같은 강력한 웹 API를 활용할 수 없습니다. loadData()는 내부적으로 data: URL을 사용하므로 대신 WebViewAssetLoader 또는 loadDataWithBaseURL()를 사용하는 것이 좋습니다.
  • WebSettings.setAllowFileAccessFromFileURLs()WebSettings.setAllowUniversalAccessFromFileURLs()file:// URL 문제를 해결할 수 있지만 true로 설정하면 앱이 파일 기반 악용에 취약해질 수 있으므로 설정하지 않는 것이 좋습니다. 보안을 강화하기 위해 모든 API 수준에서 이를 false로 명시적으로 설정하는 것이 좋습니다.
  • 같은 이유로 file://android_assets/file://android_res/ URL을 사용하지 않는 것이 좋습니다. AssetsHandlerResourcesHandler 클래스는 드롭인 교체를 위한 것입니다.
  • MIXED_CONTENT_ALWAYS_ALLOW를 사용하지 않습니다. 이 설정은 일반적으로 필요하지 않으며 앱의 보안을 약화시킵니다. 웹사이트의 리소스와 동일한 스키마(HTTP 또는 HTTPS)를 통해 인앱 콘텐츠를 로드하고 MIXED_CONTENT_COMPATIBILITY_MODE 또는 MIXED_CONTENT_NEVER_ALLOW를 적절하게 사용하는 것이 좋습니다.