얼마 전 웹뷰를 연동하는 업무를 했다. (근데 일반적이지 않는 웹뷰였다.)
앱에 3D 이미지를 붙이기 위해서 유니티 WebGL 이라는 기술을 사용해서 웹뷰를 띄워보기로 했다.
유니티와 플러터..? 정말 듣기만 해도 너무 멋진 기술 통합 같았는데.. 결과는 좋지 않았다.
왜 그러한 사례가 없었는지를 생각해봤었어야 했다.
나는 웹뷰를 연동하면서 심각한 오류와 문제를 맞이하게 되었고 이를 기반으로 앱에 3D 이미지를 띄운다는 것은 정말 쉽지 않은 일이구나를 깨달았다.
아마 유니티 WebGL 이어서 더 쉽지 않은 일인 것일 수도 있다.. 왜냐하면 WebGL의 JavaScript 환경은 싱글 스레드이기에..
유니티의 경우, 게임 엔진이 멀티스레딩을 지원하여 물리 계산이나 그래픽 렌더링 등을 별도의 스레드에서 처리할 수 있지만, WebGL 플랫폼에서는 이러한 병렬 처리가 불가능하다. 결국, 멀티스레딩을 사용할 수 없어 복잡한 계산이나 렌더링 작업이 단일 스레드에서 처리되어야 하기에 더 메모리 제한을 받을 수 있을 것 같다고 생각한다.
(아래 공식 문서 참고)
1. 웹뷰 연동
나는 메인 웹뷰 컨트롤러를 글로벌 상태관리로 관리해주었다. (Getx, Provider, Riverpod 와 같은 상태관리)
왜냐하면 앱 메인 페이지에 계속 떠 있어야 했고 웹뷰 로드 속도가 느리면 ui/ux 측면에서 굉장히 느려보이기 때문에 웹뷰 URL 로드 부분은 init() 메서드를 따로 빼서 미리 로딩 페이지에서 로드를 해주었다.
// 글로벌 상태 관리 이용했지만 아래 코드에선 생략
// 아래 init() 메서드를 로딩 페이지에서 미리 로드
WebViewController? unityWebViewController;
Future<void> init() async {
unityWebViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.transparent)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
debugPrint('WebView is loading (progress : $progress%)');
},
onPageStarted: (String url) {
debugPrint('Page started loading: $url');
},
onPageFinished: (String url) {
debugPrint('Page finished loading: $url');
},
onNavigationRequest: (NavigationRequest request) {
if (request.url.startsWith('https://www.youtube.com/')) {
debugPrint('blocking navigation to ${request.url}');
return NavigationDecision.prevent;
}
debugPrint('allowing navigation to ${request.url}');
return NavigationDecision.navigate;
},
onUrlChange: (UrlChange change) {
debugPrint('url change to ${change.url}');
},
),
)..loadRequest(Uri.parse(Constants.unityWebViewUrl));
2. iOS 에서 웹뷰 띄울 시 흰 화면 현상 발생
메인 웹뷰를 띄우고 시간이 지난 후 위와 같이 아이폰에서 계속 흰 화면이 종종 발견되었다.
처음에 핸드폰 메모리를 의심하였다.
하지만 Unity WebGL 에 대해 공부해봤을 때 핸드폰 메모리가 아니라 웹뷰 메모리에 문제가 있다는 사실을 알게 되었다.
- Unity 자체 개발: PC, 콘솔 또는 모바일 플랫폼에서 유니티 게임을 개발할 때는 해당 플랫폼의 메모리 한도 내에서 작동한다.
- Unity WebGL: webGL 애플리케이션은 브라우저 내에서 실행되므로 브라우저가 관리하는 메모리 한계 내에서 작동해야 한다. 일반적으로 메모리 한계는 몇백 MB 에서 2GB 정도로 제한된다. (브라우저와 운영체제에 따라 다를 수 있다는 점을 명심해두자)
3. 웹뷰 메모리에 대한 이해
웹뷰 메모리에 대해 조사하다가 직접적인 관련은 없지만 아래 흥미로운 사례를 발견하게 되었다.
Ex. 컴퓨터의 메모리가 부족한 것이 아니다?
어떤 블로그 인용
“크롬을 사용하던 도중 저 놈의 out of memory 오류 코드가 뜨면서 브라우저가 다운되는 현상 때문에 딥빡이 친 적이 한 두번이 아니었다.
이해가 안가는 건 PC 성능을 실시간으로 띄워도 딱히 CPU 나 메모리가 부족하지 않고 심지어 넉넉한 상황에서도 크롬은 자꾸만 아웃오브 메모리를 외쳐대었다.”
해당 메시지는 크롬의 메모리가 부족하다는 것을 의미하는데, 이 오류 코드는 컴퓨터의 메모리(RAM)가 충분하더라도 발생한다.
이는 사용자의 PC 문제보다는 주로 크롬이 원인이다. 이어지는 단계를 하나씩 적용해주는 것으로 크롬 Out of Memory 문제를 해결할 수 있다.
1. 크롬 업데이트
2. 작업 관리자 활용: 크롬은 장시간 사용할 수록 메모리 점유율이 상승하게 된다. 그래서 주기적으로 작업 끝내기를 적용하는 것이 좋다.
3. 캐시 데이터 삭제
4. 확장 프로그램 비활성화
↓
아마 위와 같은 사례와 마찬가지로 모바일에서의 WebGL 도 웹 브라우저 메모리 한도 내에서 작동하는 것과 마찬가지이지 않을까 싶다.
유니티 webGL 과 Three.js 로 만든 webGL 애플리케이션 모두 동일한 웹 브라우저의 메모리 한계 내에서 작동해야 한다.
4. webViewWebContentProcessDidTerminate
iOS의 WKWebView는 웹뷰가 너무 많은 리소스를 사용할 경우 크래시가 일어나서 웹뷰 프로세스를 종료시켜버린다. 이 때 웹뷰 프로세스가 종료되면서 빈 하얀색 화면만이 뜨게 되는 것이다.
iOS 에서 위 해결 방법에 대해 찾아보니 webViewWebContentProcessDidTerminate 라는 이벤트를 받아서 처리할 수 있다고 한다. 패키지를 수정해야 하나 싶었지만 다행히도 webview_flutter 패키지에서 webViewWebContentProcessDidTerminate 이벤트를 받아 처리할 수 있었다.
unityWebViewController = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.transparent)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
debugPrint('WebView is loading (progress : $progress%)');
},
onPageStarted: (String url) {
debugPrint('Page started loading: $url');
},
onPageFinished: (String url) {
debugPrint('Page finished loading: $url');
},
onWebResourceError:(error) {
if (error.errorType == WebResourceErrorType.webContentProcessTerminated) {
debugPrint('unity webview error: WebResourceErrorType.webContentProcessTerminated');
}
},
onUrlChange: (UrlChange change) {
debugPrint('url change to ${change.url}');
},
참고로 webViewWebContentProcessDidTerminate 현상이 일어나면 process: 0%, url change to null 출력도 같이 찍혔다.
url 이 null 이 된다는 것은 url 이 about:blank 상태라는 것이다. (그래서 빈 흰 화면이 나타나게 되는 것)
이 현상은 실제로 테스트할 수 있는 방법은 웹뷰를 띄워놓고 앱을 hide 한 다음에 다른 앱들 10개 정도 키면 위 현상을 재현시킬 수 있다.
근데 시뮬레이터를 이용하면 더 간단히 위 현상을 재현시킬 수 있다.
↓
1. 아이폰 시뮬레이터를 킨다
2. 맥북에서 "활성 상태 보기"를 킨다
2. "활성 상태 보기" 창에서 "com.apple.WebKit.WebContent" 를 눌러서 종료시킨다
3. webViewWebContentProcessDidTerminate 발생
5. 웹뷰 안 뜨는 현상 해결?
webViewWebContentProcessDidTerminate 이벤트 발생 시 아래와 같이 웹뷰 컨트롤러를 다시 reload 하면 다시 웹뷰가 정상적으로 보일 것이다.
onWebResourceError:(error) {
if (error.errorType == WebResourceErrorType.webContentProcessTerminated) {
debugPrint('unity webview error: WebResourceErrorType.webContentProcessTerminated');
unityWebViewController?.reload();
}
},
이때까지만 해도 White Screen 현상을 완벽히 해결한 줄 알았다.
전보다 흰 화면 현상이 확실히 줄었긴 했지만 앱을 정말 오랫동안 백그라운드에 둔 뒤 다시 키면 역시 흰 화면을 재현시킬 수 있었다...
아무래도 백그라운드에서는 webViewWebContentProcessDidTerminate 이벤트가 제대로 작동하지 않는 것 같아, 다음과 같이 처리했다.
webViewWebContentProcessDidTerminate 이벤트가 발생하면 URL이 null(즉, about:blank 상태)로 설정되기 때문에, 앱이 다시 resume 상태가 되면 아래와 같이 reload하도록 구현했다.
@override
void didChangeAppLifecycleState(AppLifecycleState state) async {
switch (state) {
case AppLifecycleState.resumed:
if (await unityWebViewController?.currentUrl() == null) {
unityWebViewController?.reload(); // 동작 안됨
}
....
하지만 위 조건식이 제대로 동작하지 않았다.. (어느 부분이 문제인건지 잘 모르겠다)
어떻게 웹뷰 메모리 문제를 해결할 수 있을까 곰곰히 생각해보았는데 이때 "앱을 사용하지 않을 때에도 웹뷰를 백그라운드에서 띄워줄 필요가 있을까?" 라는 생각이 들었다.
문제는 앱을 오랫동안 백그라운드에 놔두다가 다시 앱을 Open 시킬 때 발생하는 것이었다.
앱을 사용 중일 때는 webViewWebContentProcessDidTerminate 이벤트가 잘 받아지고 웹뷰도 잘 reload 되어진다!
그래서 백그라운드에서의 웹뷰 처리는 아래처럼 방법을 바꾸어 적용해 보았다.
앱이 paused 되버리는 시점에 기존 웹뷰 컨트롤러를 null 로 할당해버린다.
그러면 이 웹뷰 컨트롤러는 이제 더 이상 웹뷰 이벤트를 갖지 않는 null 상태이다.
case AppLifecycleState.paused:
unityWebViewController = null;
그리고 앱이 resumed 가 되면 다시 WebViewController 를 할당한다.
case AppLifecycleState.resumed:
if (unityWebViewController == null) unityWebViewController.init();
위와 같이 함으로써 백그라운드에서 앱을 오랫동안 두고 다시 켜도 웹뷰가 정상적으로 보이기 시작한다!
근데 ui/ux 측면에서 앱을 hide 시킨 후 킬 때마다 메인화면 웹뷰가 자꾸 로딩되는 불편한 측면은 있다.
6. 결론
사실 유니티 WebGL 을 Flutter 웹뷰에 띄워보자는 생각 자체가 잘못된 접근이었다고 생각한다.
성능이 천차만별인 모바일 기기 웹뷰에 메모리 사용량이 큰 유니티 WebGL 기술을 사용하는 것은 맞지 않았다.
iOS 에서 시간이 지나면 물체가 검정색으로 변해버리는 문제는 여전히 해결하지 못했다. (아마 메모리 문제로 인해 렌더링이 지속되다가 결국 마지막 프레임을 그리지 못한 것 아닐까?)
앞으로 기술을 개발할 때 여러 기술을 혼합해 사용할 경우, 충분히 검토한 후 사용하는 것이 중요하다는 걸 깨달았다. 각 기술은 나름의 존재 이유가 있는 법이니까!
'🐦 Flutter' 카테고리의 다른 글
Flutter Unity 통합하기 2 (0) | 2024.12.18 |
---|---|
Flutter Unity 통합하기 (0) | 2024.12.01 |
[Flutter] Sliver TabBarView hide 이슈 (0) | 2024.03.31 |
Flutter NavigationController 에서 iOS 로 이동(push)해보기 (2) | 2024.01.13 |
Debounce, Throttle (2) | 2023.12.09 |