Android WebView 커스텀 스킴 파라미터 검증 및 라우팅 구현 방법
이래저래 하이브리드 앱을 개발하고 운영하다 보면, 웹 화면에서 안드로이드 네이티브 기능을 꼭 호출해야 하는 상황이 생깁니다. 공유하기를 띄우거나, 외부 브라우저를 열거나, 기기 지도를 호출하는 기능들이 대표적이죠.
저의 경우 결과값을 웹으로 다시 리턴해 줄 필요가 없는 단순 단방향 호출에는 커스텀 스킴(myapp://action?key=value)을 자주 사용합니다. 구조가 단순해서 웹 쪽에서 호출하기 편하거든요.
하지만 이게 웹 개발자랑 앱 개발자 사이에 규칙을 제대로 안 잡아두면 나중에 코드가 지저분해지고 보안 구멍이 생기기 딱 좋습니다. 웹에서 주는 값을 검증 없이 그대로 Intent에 쑤셔 넣었다가 앱이 픽 터지거나 민감한 정보가 노출되는 경우가 많기 때문입니다. 실무에서 안전하게 쓸 수 있는 라우팅 구조와 파라미터 검증 코드를 정리해 둡니다.
1. 연동 방식 선택 기준 (내가 커스텀 스킴을 쓰는 이유)
웹과 앱을 연결하는 방법은 몇 가지가 있지만, 각자 장단점이 명확합니다. 상황에 맞게 골라 써야 나중에 고생을 안 합니다.
| 방식 | 설명 | 장점 | 주의할 점 / 추천 상황 |
| 커스텀 스킴 | 특정 URL 형식을 앱이 가로채서 분기 | 구조가 단순함 | 결과 리턴이 필요 없는 단순 단방향 호출 |
| JavaScript Interface | JS에서 Android 메서드를 직접 호출 | 양방향 연동 가능 | 노출 메서드가 많아지면 보안 검토 필수 |
| postMessage | 메시지 기반으로 웹/앱 통신 | 구조화하기 좋음 | OS 버전별 동작 확인 필요 |
| 서버 API | 웹/앱이 서버를 통해 상태를 공유 | 결합도가 낮음 | 실시간 네이티브 제어에는 부적합 |
저의 경우, "공유창 열기", "외부 브라우저 토스"처럼 호출만 하고 끝나는 기능에는 무조건 커스텀 스킴으로 구조를 단순하게 가져갑니다.
2. 핵심 구현 코드 (Java)
자꾸 까먹어서 나중에 바로 복사해 쓰려고 남겨두는 베이스 코드입니다. 실무 프로젝트에 적용할 때는 서비스 규칙에 맞게 스킴명(myapp)이나 호스트(native), 액션 목록만 수정해서 사용하시면 됩니다. 최신 안드로이드 컴포넌트 기준에 맞춰 OnBackPressedDispatcher 구조를 함께 적용해 뒀습니다.
public class MainActivity extends AppCompatActivity {
private static final String APP_SCHEME = "myapp";
private static final String NATIVE_HOST = "native";
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
webView = findViewById(R.id.webView);
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setDomStorageEnabled(true);
// 웹뷰 클라이언트에서 스킴 가로채기
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
Uri uri = request.getUrl();
if (isNativeScheme(uri)) {
handleNativeAction(uri);
return true; // 앱에서 직접 처리했으므로 웹뷰 로딩 중단
}
return false; // 일반 웹 페이지는 그대로 진행
}
});
}
// 우리 앱 전용 커스텀 스킴 규칙인지 확인
private boolean isNativeScheme(Uri uri) {
return APP_SCHEME.equals(uri.getScheme())
&& NATIVE_HOST.equals(uri.getHost());
}
// 요청받은 액션 분기 처리
private void handleNativeAction(Uri uri) {
String action = uri.getPath() == null ? "" : uri.getPath().replace("/", "");
switch (action) {
case "share":
share(uri.getQueryParameter("title"), uri.getQueryParameter("url"));
break;
case "open-browser":
openBrowser(uri.getQueryParameter("url"));
break;
case "bookmark":
saveBookmark(uri.getQueryParameter("contentId"));
break;
default:
Toast.makeText(this, "지원하지 않는 기능입니다.", Toast.LENGTH_SHORT).show();
break;
}
}
// 1. 공유하기 기능 처리
private void share(String title, String url) {
if (isBlank(title) || !isHttpsUrl(url)) return; // 파라미터 검증 필수
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, title + "\n" + url);
startActivity(Intent.createChooser(intent, "공유하기"));
}
// 2. 외부 브라우저 호출
private void openBrowser(String url) {
if (!isHttpsUrl(url)) return; // 허용되지 않은 스킴이나 주소 차단
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
// 3. 로컬 즐겨찾기 저장 처리
private void saveBookmark(String contentId) {
// 정규식을 이용해 비정상적인 파라미터(SQL 인젝션 등) 방어
if (isBlank(contentId) || !contentId.matches("^[0-9A-Za-z_-]{1,40}$")) return;
// 여기에 로컬 DB 저장 로직이나 서버 API 연동 진행
Toast.makeText(this, "즐겨찾기에 추가했습니다.", Toast.LENGTH_SHORT).show();
}
// HTTPS URL 패턴 기본 검증
private boolean isHttpsUrl(String value) {
if (isBlank(value)) return false;
Uri uri = Uri.parse(value);
return "https".equals(uri.getScheme());
}
private boolean isBlank(String value) {
return value == null || value.trim().isEmpty();
}
}
3. 기능별 파라미터 검증 기준
웹에서 넘겨주는 파라미터 값은 절대로 그대로 믿으면 안 됩니다. 변조되어 들어올 수 있다는 가정하에 앱 내부에서 필터링을 꼼꼼하게 거쳐야 합니다.
| 기능명 | 웹에서 전달할 파라미터 | 앱에서 필수로 검증할 사항 | 주의사항 |
| share | title, url | 빈 값 여부 체크, 공유 URL이 HTTPS인지 확인 | 토큰이나 개인정보가 노출된 URL 차단 |
| bookmark | contentId | 문자열 길이 제한, 허용 문자 패턴(정규식) 검증 | 내부 사용자 식별자를 가급적 직접 받지 말 것 |
| open-browser | url | HTTPS 스킴 확인, 서비스 내부 허용 도메인 체크 | 세션 파라미터나 변조된 URL 토스 방지 |
| map | lat, lng | 위경도 값이 올바른 숫자 범위 형태인지 확인 | 기기 내 지도 앱 설치 여부 사전 체크 |
| login | 없음 (action만 호출) | 파라미터 없이 호출 후 앱 네이티브 로그인창 띄움 | 절대로 Access Token을 URL에 실어 보내지 말 것 |
4. 운영하면서 직접 겪기 쉬운 실수 (주의할 점)
shouldOverrideUrlLoading리턴 값을 헷갈리지 마세요.앱 내부에서 커스텀 스킴을 가로채서 처리했다면 반드시
return true;를 줘서 웹뷰가 다음 동작을 진행하지 못하게 막아야 합니다. 반대로 일반 도메인 이동처럼 웹뷰가 계속 페이지를 로딩해야 하는 상황에는return false;를 던져줘야 흐름이 끊기지 않습니다.
검증 없는
Intent.ACTION_VIEW실행은 금물입니다.웹에서 넘겨준 URL 주소를 그대로 Intent로 실행해 버리면 피싱 사이트로 유도되거나 알 수 없는 외부 스킴이 동작하여 보안 사고로 이어집니다. 최소한 안전한 서비스 도메인이거나
https스킴을 만족하는지 필터링하는 로직을 넣어두셔야 합니다.
인증 토큰(Token)이나 중요 개인정보는 스킴 파라미터에 담지 마세요.
간혹 로그인 완료 후 토큰값을 URL 뒤에 쿼리 스트링으로 실어서 네이티브로 넘기려는 경우가 있습니다. URL은 안드로이드
Logcat이나 크래시 리포트, 기기 히스토리에 텍스트 그대로 남을 위험이 큽니다. 인증 정보나 중요 정보는 웹뷰 암호화 스토리지나 보안 브리지 흐름을 타고 넘어가야 안전합니다.
5. 실무 운영 체크리스트
[ ] 스킴 이름(
myapp)과 호스트(native)를 화이트리스트로 고정하고 예외는 전부 무시하는가?[ ] 처리 불가능하거나 정의되지 않은 액션이 들어왔을 때 앱이 크래시되지 않고 예외 처리(Toast 등)를 해주는가?
[ ] 외부로 토스하는 URL 파라미터에 민감한 세션값이나 개인정보가 포함되어 있지는 않은가?
[ ] 정규식을 활용해 ID나 숫자 파라미터의 포맷을 명확히 필터링하고 있는가?
[ ] 디버그 로그(
Log.d)에 전체 스킴 URL을 가공 없이 그대로 노출시키지 않는가?
커스텀 스킴은 웹에서 앱 기능을 맘대로 주무르는 통로가 아니라, '미리 약속된 안전한 요청만 앱이 검증해서 실행해 주는 제한된 창구'라고 생각하고 접근해야 뒤탈이 없습니다. 기능이 하나씩 늘어날 때마다 웹 연동 문서와 안드로이드 스위치 문을 함께 업데이트하면서 동기화를 맞춰 운영하시기 바랍니다.
댓글
댓글 쓰기