웹뷰 개발을 하면서 다시 만날 문제들
요즘은 웹뷰를 안쓰는 앱이 없을 정도로 빼놓을 수 없는 기술이 되었다고 생각하는데요.
실무에서 웹뷰를 다루면서 겪은 경험과 해결책을 정리하고 같은 이슈를 만났을 때 현명하게 대처하고자 합니다.
개인적으로 웹뷰를 쓰는 이유
장점
하나의 코드로 여러 플랫폼을 지원하여 생산성이 높다
스토어 배포 없이 업데이트 가능합니다.
런타임에서 웹 코드 격리되어 앱이 죽지 않습니다.
단점
네이티브 대비 상대적인 속도는 느릴 수 있습니다.
오프라인에서 사용 불가능하다.
웹 기반 보안 취약점을 고려해야 합니다.
스토어 심사에서 추가 고려사항이 될 수 있습니다.
하지만 장점이 단점보다 매우 크기 때문에 웹 하나로 여러 플랫폼을 지원하고 빠른 실험과 피드백을 위해서라면 한 번쯤 도입을 고민해보아도 좋을 것 같습니다.
FE개발자를 위한 웹뷰
플랫폼 구분
navigator.userAgent을 이용한 분기처리
라우팅
웹: URL이 브라우저 히스토리에 스택으로 관리
앱: 스택으로 화면 관리
⇒ 네이티브 이벤트를 활용한 웹뷰 라우팅 동기화(ex. Android onBackPressed)
앱과 웹 컴포넌트 구분?
앱인지 웹뷰인지 platform 조건문으로 컴포넌트 렌더링
userAgent를 이용
웹 vs 앱 라우팅
웹에서는 URL이 브라우저 히스토리에 스택으로 푸시
앱은 네비게이터(스택)
네이티브 이벤트를 이용해서 웹뷰에 보낸다
인증 관리
웹앱이면 모든 처리를 웹에서 처리하면 되지만 네이티브 앱이 로그인의 기준점이 되어야 합니다. 웹앱과 달리 네이티브와 웹뷰가 공존하는 환경에서는 일관된 인증 상태 관리가 중요합니다.
인증 플로우(토큰 기반)
사용자가 네이티브 앱에서 로그인
토큰을 네이티브 보안 저장소에 저장 (Android SharedPreferences에 KeyStore로 암호화, iOS Keychain)
네이티브 -> 웹뷰로 인증 정보 전달
웹뷰 로드 시 네이티브에서 토큰을 쿠키로 설정하여 전달
JavaScript Interface를 통해서 토큰 전달(ex. postmessage, evaluateJavaScript)
웹뷰 로드 시 URL 쿼리파라미터로 전달
토큰 기반 인증
앱 개발자를 위한 웹뷰
브라우저(Chromium, Webkit)와 Webview는 다르다
JavaScript Override
웹페이지 로그를 네이티브 개발환경으로 출력
const originalConsoleLog = console.log; console.log = function(...args) { originalConsoleLog.apply(console, args); if (window.Bridge) { window.Bridge.sendLog('log', args); } };
웹뷰 성능 최적화
CSR 웹앱의 성능 문제
번들 로딩 문제
큰 번들 사이즈: 초기 로딩 지연 및 흰 화면 현상
CDN 이슈: 다른 리전(ex. 미국)을 경유하여 늦어지는 현상
해결책
번들 분할 및 코드 스플리팅
S3 + CloudFront 활용하여 배포
API 호출 지연
번들 로딩 완료 후 API 요청으로 순차 로딩으로 인한 지연
일반적으로 Cross-domain 요청을 하게 될텐데 Preflight 요청으로 인해 100-200ms 추가 지연
해결책
SSR 도입 검토
SSR(Server-Side Rendering) 도입
장점
서버에서 API 호출하여 사용자 네트워크 환경 의존성 감소
초기 페이지 로딩 속도 개선
단점
모든 API 응답 완료까지 대기 필요
서버 모니터링 및 확장성 고려 필요
개별 API 실패 시 전체 페이지 지연
해결책
Streaming SSR 도입 검토
Streaming SSR 활용
Suspense기준으로 준비되는만큼 부분적(청크)으로 유저에게 피드백할 수 있습니다.
// React 18의 Streaming SSR 예시
import { renderToPipeableStream } from 'react-dom/server';
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
// Suspense 경계 밖의 부분(셸)이 준비되면 스트리밍 시작
pipe(response);
}
});
다시 만날 이슈들
iOS 키보드 focus 문제
문제: focus되는 요소가 화면 아래에 있을 때 키보드 높이만큼 웹뷰가 안 보이거나 밀리는 현상
해결책
위로 밀리는 현상이 있으면 iOS는 높이만큼 padding, Android는 뷰의 height를 줄여줍니다.
안 보이는 현상이 있으면 웹뷰가 visualViewport의 onresize 핸들러를 추가하여 scroll 위치 조정합니다.
iOS CSS (flex gap) 호환
문제: iOS Safari 14.4 이하에서
flex gap
미지원해결책: 대체 CSS 사용(ex. margin)
브라우저 확대 방지
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no" />
<!-- 또는 -->
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0" />
App Scheme 동작 실패
문제: 2개를 호출했는데 1개만 동작하는 경우
원인
앱 스킴의 앱 동작 확인
웹페이지의 호출 코드 확인
location.href
사용 시 두 번째만 수행되는 문제
해결책
호출 사이에 지연시간을 준다
Javascript Interface방식으로 전환
문자열 Escape 동작 차이
문제: iOS, Android, JavaScript 간 문자열 처리 방식 차이
해결책
앱 → 웹 호출 시 문자열(
"
,'
,\\
) 적절한 인코딩/이스케이핑 필요플랫폼별 화이트스페이스 처리 규칙 통일 (공백,
+
등)
쿠키 용량 제한
문제 RFC 6265에 따른 쿠키 4KB 제한으로 웹뷰가 안 열리는 경우
해결책
쿠키 크기 모니터링
대용량 데이터는 다른 저장소 활용
휴리스틱 캐시 문제
문제: RFC 7234에 따른 웹엔진 임의 캐시 (lastModified의 1/10)
해결책
<!-- 캐시 방지 헤더 설정 -->
<meta http-equiv="Cache-Control" content="no-cache, must-revalidate, max-age=0">
웹 페이지가 동작하지 않는 경우(흰색 화면만)
iOS에서는
webViewWebContentProcessDidTerminate(_:)
Android에서는
onRenderProcessGone()
그 밖에는 에러 처리(reload)
앱과 웹이 통신하는 방법
App Scheme(
[scheme]://[host]/[path]?[query]
)Javascript Interface(브릿지)
사전에 정의한 JS를 호출하는 방법
호출 길이 제한(브라우저마다 다릅니다), URL 로딩이 없습니다.
웹 → 앱
App Scheme 방식
// 기본 호출
location.href = "[scheme]://[host]/[path]?title=webview";
// 콜백 함수와 함께 (XSS 주의)
location.href = "[scheme]://[host]/[path]?callback=<function_name>";
한계
Parameter 인코딩 시 URL 길이 제한
동일한 App Scheme 중복 호출 시 응답 특정 불가능
여러번 호출 시 누락 문제 발생 가능
JavaScript Interface 방식
Android
// 사전 정의된 네이티브 함수 호출(사전 정의된 이름 jsToNative)
window.Bridge.jsToNative(JSON.stringify({
data: {...},
callbackId: "callbackId"
}));
iOS
// WebKit Message Handler 사용(사전 정의된 이름 jsToNative)
window.webkit.messageHandlers.jsToNative.postMessage(JSON.stringify({
data: {...},
callbackId: "callbackId"
}));
통합 구현
// 사전 정의된 이름 jsToNative
function jsToNative(data, callback) {
const callbackId = {unique_id};
// 콜백 등록
window.addEventListener(callbackId, callback, { once: true });
if (isIos()) {
window.webkit?.messageHandlers.jsToNative.postMessage(
JSON.stringify({ data: {...data}, callbackId })
);
} else if (isAndroid()) {
window.Bridge?.jsToNative(
JSON.stringify({ data: {...data}, callbackId })
);
}
}
앱 → 웹
직접 함수 호출
웹
// 전역 함수 정의
function handleNativeEvent(data) {
console.log('Native event received:', data);
}
네이티브(Android, iOS)
webView.evaluateJavaScript("handleNativeEvent('test')")
JavaScript Custom Event
웹
window.addEventListener('nativeEvent', (event) => {
console.log('Event detail:', event.detail);
});
네이티브(Android, iOS)
let script = """
window.dispatchEvent(new CustomEvent('nativeEvent', {
detail: data
}));
"""
webView.evaluateJavaScript(script)
장점
앱: 예외처리 불필요
웹: eventListener 사용으로 별도 관리 리소스 불필요
디버깅 도구
iOS
iOS 16.4 이전: 개발 빌드에서만 인스펙터 사용 가능 (기기 등록 수 제한)
iOS 16.4 이후: 안드로이드와 같이 인스펙터 지원
Android
Chrome DevTools를 통한 원격 디버깅 지원
참고
Last updated