이번 글은 1년 전 하이브리드앱을 출시하며 겪은 앱 안정화의 과정에서 느꼈던 것들을 얘기하고자 한다.
하이브리드앱이기에 웹과 앱의 통신과정에서 발생한 에러들도 많았다. 안드로이드 개발자는 나 혼자뿐이었으므로 하이브리드앱의 설계를 처음부터 끝까지 혼자서 담당했고 디버깅하는 과정에서도 웹 개발자와도 소통을 많이 했었어야 했다.
소프트웨어 개발을 하는 곳이라면 어느 곳에서든 장애가 없을 수 없다. 하지만 체감상 거의 대부분은 상대적으로 빠르게 해결할 수 있는 것들이다. 큰 장애는 1년에 몇 번정도 발생한다던데(by 개발팀장님) 그 일들을 한꺼번에 겪으면서 '앱 안정화'란 Task를 직접 만들고 프로덕트 팀 내 에러 대응 프로세스 정립까지 한 이야기를 써보고자한다. 1편은 당시 내가 짧은시간에 한꺼번에 겪었던 에러들을 생각나는대로 적어봤다.
1. Meta tag 에러
웹뷰에서 났던 에러이다. 구글 태그 매니저를 통해 마케팅팀이 심은 facebook tool 에서 난 facebook의 에러였다. 웹뷰에서 난 에러 메세지 객체를 확인하기 위해 onReceivedError 를 출력해보았고, 에러가 났던 facebook의 config 파일을 열어 에러명이었던 'properties://brower/clickID' 를 검색해봤을 때 검색된 script 일부를 확인할 수 있었다. 소스코드상 Android 일 때 특정 메서드를 호출하게 되어있었는데 메서드를 찾지 못해서 JS가 터지게 되었고 웹뷰는 무한로딩이 돌았던 경우였다.
...
if(navigator.userAgent.indexOf("Android")!==-1){
var c=new XMLHttpRequest();
c.onload=function(){if(c.status>=200&&c.status<300)
{var a=c.responseText;a!==""&&d.trigger(a)}};c.open("GET","properties://browser/clickID");c.send()}})})})();return e.exports
}(a,b,c,d)});e.exports=f.getFbeventsModules("SignalsFBEvents.plugins.browserproperties");f.registerPlugin&&f.registerPlugin("fbevents.plugins.browserproperties",e.exports);
페이스북에서 작성한 코드 중 클래스명이 없거나 명칭을 잘못 접근했거나 쨌든 페이스북의 에러였고, config 파일의 version을 높여서 source code를 다시 열어보니 해당 프로퍼티는 삭제되어있었다. hot fix건으로 페이스북에서 해결했던 것이 아니었나 싶다.
facebook의 에러였지만 Android에서의 웹뷰 에러 처리도 아쉬웠다. 나는 WebView 에서 onError로 받는 모든 경우에 로딩바를 표시해주는 것으로 처리를 해놨기 때문에 이 경우에도 에러가 포함돼 무한로딩이 돌았던 것이다. 에러는 페이스북의 핫픽스로 곧 해결되었지만 원인모를 에러에 진땀을 흘렸던 기억이 있다. 그리고 이런 경우의 에러를 생각지도 못했었다.
그리고 나는 이번 계기로 웹뷰가 주가 되는 메인에서는 firebase Crashlyrics SDK를 설치하여 에러 케이스를 수집해보도록 했다. 수집된 에러들을 보니 script에서 잠깐 생성되는 에러들이 꽤 많았었고 network 에러같은 경우만 로딩바를 띄어주는 것으로 변경하고 다른 에러들은 흘려보내기로 변경했다.
2. Library version 충돌 에러
서비스에 채널톡 SDK를 사용하고 있었는데 동영상이 들어간 채널톡 캠페인을 발행했을 시 앱이 튕기는 현상이었다. 앱이 crash 되는 것이었기에 meta tag때 심어뒀던 crashlytics 덕분에 이상수치에 대한 에러를 빠르게 인식할 수 있었고 Android의 exoplayer 관련한 에러가 발생하고 있음을 바로 알게되었다.
com.zoyi.com.google.android.exoplayer2.SimpleExoPlayer.maybeNotifySurfaceSizeChanged (SimpleExoPlayer.java:1171)
com.zoyi.com.google.android.exoplayer2.SimpleExoPlayer.access$1500 (SimpleExoPlayer.java:67)
com.zoyi.com.google.android.exoplayer2.SimpleExoPlayer$ComponentListener.onSurfaceTextureAvailable (SimpleExoPlayer.java:1390)
채널톡 캠페인 발행을 즉시 중단해서 발생하고 있던 에러는 일단 막았었다. 하지만 해결을 하려고보니 테스트버전에서는 모두 정상적으로 실행이 되었었는데 릴리즈버전에서 에러가 난게 의아했었다. 당시에 채널톡으로 현상 문의를 해 다음과 같은 답변을 받았었다.
안녕하세요 오래 기다리셨습니다, 제품팀 OO라고 합니다
불편드려서 죄송합니다 해당 이슈는 다음과 같습니다.
https://github.com/MasayukiSuda/GPUVideo-android/issues/25#issuecomment-601202923
gradle.properties 쪽에 android.enableDexingArtifactTransform=false 옵션을 추가해주시면 될거 같습니다!
이러한 과정으로 시도해주실 수 있으신가요?
Comment on #25 The app is crashing on the release build with androidx
Similar problem (this solved my problem)
@bakhytk Add the following line to your gradle.properties file:
android.enableDexingArtifactTransform=false
The problem resides in a new feature of the gradle build tools 3.5 where it tries to optimize external dependencies as well and it strips default method implementations from exoplayer.
자세히 표시
<https://github.com/MasayukiSuda/GPUVideo-android|MasayukiSuda/GPUVideo-android>MasayukiSuda/GPUVideo-android | 2020년 3월 19일 | 봇이 추가한 GitHub
android.enableDexingArtifactTransform=false
Android 의 빌드도구인 Gradle 3.5버전에서 발생한 것으로 외부 종속성을 최적화하기 위해서 ExoPlayer라는 라이브러리에서 기본 메서드 구현을 제거한 것으로 보인다. 빌드할 때 Dexing 작업을 비활성화하는 작업으로 gradle properties에 추가하여 배포했었다. Android의 라이브러리 충돌문제였다. 그래서 실제로 각기 다른 서비스 플랫폼 2개에 동시에 캠페인을 발행했었는데 채널톡을 웹 script로 심어 실행한 한 서비스 플랫폼에서는 정상작동이 되었으나, SDK로 설치하여 실행했던 플랫폼에서는 에러가 발생했었다.
3. Android Target Version
Android 12이상에서 발생하는 메세지로, Firebase Cloud Messaging을 받고 인텐트를 통해 다른 액티비티로 이동하는 로직에서 유저가 푸쉬 메세지를 클릭할 시 앱이 튕기는 현상이었다.
java.lang.IllegalArgumentException: *.*.*.*: Targeting S+ (version 31 and above)
requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating
a PendingIntent. Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE
if some functionality depends on the PendingIntent being mutable, e.g.
if it needs to be used with inline replies or bubbles.
버전31이상부터는 PendingIntent를 사용할 때 FLAG_IMMUTABLE이나 FLAG_MUTABLE을 설정해줘야한다. Android 12를 타깃하는 경우에 PendingIntent 객체의 변경 가능 여부에 대하여 Flag 설정이 필수로 변경되었다고 한다. 이유는 앱의 보안성 강화를 위해서이다.
당시 서비스를 사용하는 유저들은 안드로이드 공기계를 많이 쓴다는 가설하에 버전이 낮은 안드로이드 기계에서 테스트를 했었는데 이를 놓친 우범함이었다. 만약 발현이 되었다하더라도 앞으로 target version을 올리는 과정에서도 주의를 해야겠다고 생각했다.
4. 웹과 앱의 Token 프로세스 불일치
당시 token 관리 프로세스는 이렇게 됐다. 유저는 웹뷰에서 로그인을 시작했고 네이티브는 token을 저장시켜야 했던 상황이었다.
1. 로그인 후 웹에서 auth code를 앱으로 Native Interface를 통해 전달하고, 네이티브에서 auth code로 accessToken과 refreshToken을 받는 API를 호출하여 네이티브 로컬 스토리지에 저장시킨다. 이를 통해 해당 메인 서비스에 접근할 수 있는지 없는지를 판별해줘야하기 때문이다.
2. 네이티브는 로컬 스토리지에 저장시키면서 웹의 로컬 스토리지에 저장시키는 Native interface를 실행하여 웹뷰를 로딩시킨다. 웹뷰는 대시보드/피드/주문 3개의 탭에서 각각 띄어주고 있었는데 React로 개발된 웹이었기에 웹의 localStorage가 동기화가 되었었다.
문제는 token이 만료되었을 때였다.
처음 앱을 실행시킬 때의 과정에서는 앱과 웹의 로컬스토리지가 가지고 있는 토큰들이 동기화가 되겠지만(정확한 동기화는 아니겠지만) accessToken이 만료되어 다음 accessToken을 불러와서 저장시켜야할 때 이 책임을 웹이 지도록 했었다. 웹 코드에는 이미 accessToken을 갱신하는 코드가 있었으므로 token이 불일치한 채로 웹은 실행되고 있었던 것이다. 만약 refreshToken까지 Invalid 되어 로그아웃되어야하는 상황이라면, 그 때의 Native Interface를 통해 웹으로부터 로그아웃시키라는 이벤트를 받아 앱에서도 로컬스토리를 삭제하고 로그아웃시키는 플로우였다. 그리고 다시 로그인을 하면 같은 플로우 반복.
웹과 앱의 token이 불일치하여 작동되긴 했지만 에러발생은 없었기때문에 token 갱신의 책임을 웹에게 지었었다. 하지만 당시 React bundle 용량의 문제로 대시보드/주문 탭을 Next.js로 분리하는 작업을 했었다.
여기서 Next.js와 React는 다른 도메인을 가지고 있었기에 3개의 탭이 가진 웹의 로컬스토리지가 공유되지도 않았고, 웹에서 토큰 갱신의 책임을 가지기로 했었는데 당시 Next.js의 토큰 갱신 로직 중이 잘못되어 있었기에 갱신을 해주지 못한 상황이 되어 웹의 로딩바가 도는 현상이 나타났었다. 그래서 테스트 당시에는 토큰이 만료되기 전에 테스트를 진행했었으므로 해당 케이스가 발현이 되지 않았었고, 이는 서비스를 이용하는 고객에게 발생이 되었었다.
Next.js 코드의 갱신 로직을 수정해서 네이티브의 배포없이 문제는 다행히 해결이 되었었지만 이 과정을 디버깅하는것이 까다로웠다. 토큰이 만료되는 경우의 케이스와 Next.js코드에서의 원인을 찾기는 어려웠기에 네이티브 개발자 나 혼자서만 디버깅해서 찾기는 어려웠었다.
웹 코드의 수정으로 해결은 되었으나, token 관리 프로세스를 다시 잡아야한다고 생각했다.
토큰의 책임을 앱으로 가져오고 전적으로 앱에서 관리하는 로직으로 수정했다. 앱에서 토큰이 만료됐을 경우 앱에서 갱신하는 API 호출을 하여 웹으로 계속 쏴주는 행위를 하면서 웹과 앱의 토큰을 일치화시키니 관리 소구 포인트가 앱으로 집중되고 디버깅하기도 훨씬 좋아졌다.
'💬 회고' 카테고리의 다른 글
2023년 회고. (1) | 2024.01.31 |
---|---|
나는 어떻게 장애를 전파하였는가 (2) (2) | 2023.10.08 |