안드로이드 웹뷰 ERR_CLEARTEXT_NOT_PERMITTED 완전 해결 가이드: HTTP 차단 원인부터 안전한 우회까지

Prompt Architect 편집팀 · 2026-06-18 · 7분

TL;DR — 안드로이드 9 이상에서 웹뷰가 HTTP 페이지를 막아 발생하는 ERR_CLEARTEXT_NOT_PERMITTED 오류를, 전역 허용과 도메인 한정 설정 두 방식으로 단계별로 해결하는 실무 가이드입니다.

들어가며: 갑자기 빈 화면만 뜨는 웹뷰

앱 안에 WebView를 붙여 외부 페이지를 불러오는 작업은 흔합니다. 그런데 어느 날 잘 동작하던 화면이 갑자기 하얗게 비거나, 로그캣(Logcat)에 다음과 같은 메시지가 찍히는 경우가 있습니다.

net::ERR_CLEARTEXT_NOT_PERMITTED

특히 사내 관리자 페이지, 테스트 서버, 오래된 외부 API처럼 아직 https://로 옮기지 못한 주소를 띄우려 할 때 자주 만나게 됩니다. 코드는 분명 그대로인데 왜 이런 오류가 날까요? 결론부터 말하면 앱의 잘못이 아니라, 안드로이드 플랫폼의 보안 정책이 바뀐 것이 원인입니다. 이 글에서는 오류의 배경을 짚고, 상황에 맞는 두 가지 해결책을 코드와 함께 단계별로 설명합니다.


1. 왜 이 오류가 발생하는가 — 평문 트래픽 차단 정책

cleartext는 암호화되지 않은 평문 통신, 즉 http://(HTTPS가 아닌 일반 HTTP) 연결을 의미합니다.

구글은 사용자 데이터 보호를 강화하기 위해 안드로이드 9(API 레벨 28)부터 평문 HTTP 통신을 기본으로 차단하도록 정책을 변경했습니다. 즉 targetSdkVersion이 28 이상인 앱은 별도 설정 없이는 http:// 요청을 보낼 수 없습니다. 웹뷰뿐 아니라 HttpURLConnection, OkHttp, Retrofit 등 모든 네트워크 계층에 동일하게 적용됩니다.

정리하면 이 오류는 다음 세 조건이 겹칠 때 나타납니다.

  • 앱의 targetSdkVersion이 28 이상이다.
  • 불러오려는 주소가 https://가 아닌 http://다.
  • 평문 트래픽을 허용하는 예외 설정을 하지 않았다.

따라서 해결의 핵심은 "어떤 범위까지 평문 통신을 예외로 허용할 것인가"를 정하는 일입니다.


2. 가장 빠른 임시 해결 — 전역 평문 허용

급하게 동작만 확인하고 싶다면 AndroidManifest.xml<application> 태그에 속성 하나를 추가하면 됩니다.

<!-- AndroidManifest.xml -->
<application
    android:name=".MyApp"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:usesCleartextTraffic="true">  <!-- 앱 전체에서 HTTP 통신 허용 -->

    <!-- activity 등 나머지 선언 -->

</application>

android:usesCleartextTraffic="true" 한 줄이면 앱 전역에서 평문 트래픽 차단이 풀립니다. 빌드 후 다시 실행하면 웹뷰가 정상적으로 HTTP 페이지를 그려줍니다.

다만 이 방식은 앱이 통신하는 모든 도메인에 대해 HTTP를 열어버립니다. 검증용으로는 빠르지만, 보안 관점에서는 권장되지 않습니다. 실서비스 빌드에 이 설정을 그대로 두면, 의도치 않은 평문 통신까지 모두 허용되어 중간자 공격(MITM)에 노출될 수 있습니다.


3. 권장 방법 — 도메인 단위로만 예외 허용

실무에서 권장하는 방식은 꼭 필요한 도메인에만 평문 통신을 허용하는 것입니다. 이를 위해 네트워크 보안 구성 파일(network security config)을 별도로 만듭니다.

3-1. 보안 구성 파일 생성

res/xml/ 디렉터리에 network_security_config.xml 파일을 새로 만듭니다. (폴더가 없다면 res 아래에 xml 폴더를 먼저 생성하세요.)

<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>

    <!-- 이 블록에 명시한 도메인에 대해서만 HTTP 허용 -->
    <domain-config cleartextTrafficPermitted="true">
        <!-- includeSubdomains: api.legacy-server.kr 같은 하위 도메인도 함께 허용 -->
        <domain includeSubdomains="true">legacy-server.kr</domain>
        <domain includeSubdomains="false">staging.internal.local</domain>
    </domain-config>

</network-security-config>

위 예시는 legacy-server.kr와 그 하위 도메인, 그리고 staging.internal.local 두 곳만 HTTP를 허용합니다. 나머지 도메인은 여전히 기본 정책대로 HTTPS만 통하므로, 앱 전체 보안 수준은 유지됩니다.

3-2. 매니페스트에서 구성 파일 연결

만든 파일을 <application> 태그에서 참조합니다.

<!-- AndroidManifest.xml -->
<application
    android:name=".MyApp"
    android:label="@string/app_name"
    android:networkSecurityConfig="@xml/network_security_config">  <!-- 보안 구성 연결 -->

    <!-- 나머지 선언 -->

</application>

이제 usesCleartextTraffic 속성 없이도, 구성 파일에 적은 도메인에 한해서만 평문 통신이 열립니다. "전역 허용"과 "도메인 한정 허용" 두 방식은 함께 쓰지 말고 둘 중 하나만 선택하는 것이 깔끔합니다. 둘이 충돌하면 networkSecurityConfig의 세부 규칙이 우선 적용됩니다.


4. 그래도 안 될 때 — 흔한 실수와 엣지 케이스

설정을 했는데도 같은 오류가 계속된다면 아래를 점검해 보세요.

  • 빌드 캐시 문제: 매니페스트를 고쳐도 반영이 안 될 때가 있습니다. Build > Clean Project 후 다시 빌드하거나, 앱을 완전히 삭제 후 재설치해 보세요.
  • 도메인 철자/포트 불일치: 구성 파일의 <domain> 값에는 http://나 포트 번호, 경로를 넣지 않습니다. 순수 호스트명(legacy-server.kr)만 적어야 합니다.
  • IP 주소 직접 접속: 192.168.0.10 같은 사설 IP로 붙는 경우, 해당 IP를 <domain>에 그대로 적어야 합니다. includeSubdomains는 IP에는 의미가 없습니다.
  • 리다이렉트 함정: HTTPS 페이지가 내부적으로 HTTP 리소스(이미지, 스크립트)를 불러오는 혼합 콘텐츠(mixed content)도 차단될 수 있습니다. 이 경우 웹뷰의 setMixedContentMode 설정을 함께 검토해야 합니다.
  • 다른 라이브러리 통신: 웹뷰가 아니라 Retrofit/OkHttp에서 났다면 원인은 동일하지만, 호출하는 베이스 URL의 도메인을 구성 파일에 추가해야 합니다.

혼합 콘텐츠 관련 웹뷰 옵션 예시는 다음과 같습니다.

// WebView 초기화 시 (코틀린)
webView.settings.apply {
    javaScriptEnabled = true
    // HTTPS 페이지 안에서 HTTP 리소스도 로드 허용 (보안 약화이므로 신중히)
    mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
}

5. 근본 해법은 HTTPS 전환

지금까지의 설정은 모두 **"평문 통신을 예외적으로 허용"**하는 우회책입니다. 빠르게 문제를 풀어주지만, 통신 내용이 암호화되지 않는다는 본질적 위험은 그대로 남습니다.

따라서 다음 우선순위로 접근하는 것을 권합니다.

  1. 가능하면 서버를 HTTPS로 전환하여 예외 설정 자체를 없앤다.
  2. 전환이 당장 어렵다면 도메인 한정 허용으로 범위를 최소화한다.
  3. 전역 허용(usesCleartextTraffic="true")은 개발/테스트 빌드에서만 쓰고, 릴리스 빌드에는 남기지 않는다.

build.gradle의 빌드 타입별로 다른 매니페스트나 보안 구성을 적용하면, 디버그에서만 평문을 열고 릴리스에서는 닫는 분리 운영도 가능합니다.


6. 요약

  • ERR_CLEARTEXT_NOT_PERMITTED는 안드로이드 9(API 28) 이상에서 평문 HTTP를 기본 차단하기 때문에 발생한다.
  • 가장 빠른 해결은 매니페스트에 android:usesCleartextTraffic="true"를 넣는 전역 허용이지만 보안상 권장되지 않는다.
  • 실무에서는 res/xml/network_security_config.xml필요한 도메인에만 평문 통신을 허용하는 방식이 안전하다.
  • 설정 후에도 안 되면 빌드 캐시, 도메인 표기, IP/혼합 콘텐츠 여부를 점검한다.
  • 궁극적으로는 서버를 HTTPS로 옮겨 예외 자체를 제거하는 것이 정답이다.

AI에게 물어볼 때 (프롬프트 팁)

이런 플랫폼 보안 정책 이슈는 맥락을 정확히 주면 AI가 훨씬 정밀한 답을 줍니다. ChatGPT나 Claude에게 질문할 때 다음처럼 환경 정보를 구조화해 보세요.

역할: 너는 안드로이드 네이티브 개발 시니어 엔지니어다.
환경: targetSdkVersion 34, minSdk 24, Kotlin, WebView로 http://legacy-server.kr 로드.
증상: 실행 시 net::ERR_CLEARTEXT_NOT_PERMITTED 발생.
요청: (1) 원인을 2줄로 설명하고
      (2) "전역 허용"과 "도메인 한정 허용" 두 방식의 코드를 각각 제시하되
      (3) 릴리스 빌드 보안 관점에서 어느 쪽을 권장하는지 근거와 함께 알려줘.

해결책의 안전성까지 검증받고 싶다면 이렇게 후속 질문을 던지는 것도 좋습니다.

위 network_security_config.xml 설정에서 보안상 취약해질 수 있는 부분과,
프로덕션 배포 전 반드시 점검해야 할 체크리스트를 우선순위 순으로 정리해줘.

여러 도메인이 얽힌 복잡한 구성이라면 "내 도메인 목록은 A, B, C인데 A만 HTTP, 나머지는 HTTPS 강제"처럼 조건을 명시적으로 나열해 주면 AI가 정확한 XML을 만들어 줍니다. 이렇게 역할·환경·증상·요청을 구분해 묻는 습관은 모든 기술 질문의 답변 품질을 끌어올립니다. 더 효과적인 프롬프트 설계가 궁금하다면 Prompt Architect의 분석기로 자신의 질문을 점검해 보세요.